2018-08-15 01:24:25 +08:00
# 接入图灵机器人
2018-08-21 00:30:39 +08:00
到目前为止我们已经编写了一个相对完整的天气查询插件,包括命令和自然语言处理器,除此之外,使用同样的方法,还可以编写更多功能的插件。
但这样的套路存在一个问题,如果我们不是专业的 NLP 工程师,开放话题的智能聊天仍然是我们无法自己完成的事情,用户只能通过特定插件所支持的句式来使用相应的功能,当用户试图使用我们暂时没有开发的功能时,我们的机器人显得似乎有些无能为力。
不过还是有解决方案的,市面上有一些提供智能聊天机器人接口的厂商,本章我们以 [图灵机器人 ](http://www.tuling123.com/ ) 为例,因为它的使用比较广泛,接入也比较简单,不过缺点是免费调用次数比较少。
::: tip 提示
2018-12-27 20:23:45 +08:00
本章的完整代码可以在 [awesome-bot-4 ](https://github.com/richardchien/nonebot/tree/master/docs/guide/code/awesome-bot-4 ) 查看。
2018-08-21 00:30:39 +08:00
:::
## 注册图灵机器人账号
首先前往 [图灵机器人官网 ](http://www.tuling123.com/ ) 注册账号,然后在「机器人管理」页根据它的提示创建机器人,可以设置机器人名字、属性、技能、语料库等。
注册完成后先放一边,或者如果有兴趣的话,在网页上的聊天窗口和它聊几句看看效果。
## 编写图灵机器人插件
新建 `awesome/plugins/tuling.py` 文件,编写如下内容:
```python
import json
from typing import Optional
import aiohttp
from aiocqhttp.message import escape
2018-12-27 20:23:45 +08:00
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, NLPResult
from nonebot.helpers import context_id, render_expression
2018-08-21 00:30:39 +08:00
# 定义无法获取图灵回复时的「表达( Expression) 」
EXPR_DONT_UNDERSTAND = (
'我现在还不太明白你在说什么呢,但没关系,以后的我会变得更强呢!',
'我有点看不懂你的意思呀,可以跟我聊些简单的话题嘛',
'其实我不太明白你的意思……',
'抱歉哦,我现在的能力还不能够明白你在说什么,但我会加油的~'
)
# 注册一个仅内部使用的命令,不需要 aliases
@on_command ('tuling')
async def tuling(session: CommandSession):
# 获取可选参数,这里如果没有 message 参数, 命令不会被中断, message 变量会是 None
message = session.get_optional('message')
# 通过封装的函数获取图灵机器人的回复
reply = await call_tuling_api(session, message)
if reply:
# 如果调用图灵机器人成功,得到了回复,则转义之后发送给用户
# 转义会把消息中的某些特殊字符做转换,以避免酷 Q 将它们理解为 CQ 码
await session.send(escape(reply))
else:
# 如果调用失败,或者它返回的内容我们目前处理不了,发送无法获取图灵回复时的「表达」
2018-12-22 18:09:57 +08:00
# 这里的 render_expression() 函数会将一个「表达」渲染成一个字符串消息
await session.send(render_expression(EXPR_DONT_UNDERSTAND))
2018-08-21 00:30:39 +08:00
@on_natural_language
async def _(session: NLPSession):
# 以置信度 60.0 返回 tuling 命令
# 确保任何消息都在且仅在其它自然语言处理器无法理解的时候使用 tuling 命令
return NLPResult(60.0, 'tuling', {'message': session.msg_text})
async def call_tuling_api(session: CommandSession, text: str) -> Optional[str]:
# 调用图灵机器人的 API 获取回复
if not text:
return None
url = 'http://openapi.tuling123.com/openapi/api/v2'
# 构造请求数据
payload = {
'reqType': 0,
'perception': {
'inputText': {
'text': text
}
},
'userInfo': {
'apiKey': session.bot.config.TULING_API_KEY,
'userId': context_id(session.ctx, use_hash=True)
}
}
group_unique_id = context_id(session.ctx, mode='group', use_hash=True)
if group_unique_id:
payload['userInfo']['groupId'] = group_unique_id
try:
# 使用 aiohttp 库发送最终的请求
async with aiohttp.ClientSession() as sess:
async with sess.post(url, json=payload) as response:
if response.status != 200:
# 如果 HTTP 响应状态码不是 200, 说明调用失败
return None
resp_payload = json.loads(await response.text())
if resp_payload['results']:
for result in resp_payload['results']:
if result['resultType'] == 'text':
# 返回文本类型的回复
return result['values']['text']
except (aiohttp.ClientError, json.JSONDecodeError, KeyError):
# 抛出上面任何异常,说明调用失败
return None
```
上面这段代码比较长,而且有一些新出现的函数和概念,我们后面会慢慢地详解,不过现在先在 `config.py` 中添加一项:
```python
TULING_API_KEY = ''
```
`TULING_API_KEY` 的值填图灵机器人的「机器人设置」页面最下方提供的 API Key。
配置完成后来运行 NoneBot, 尝试给机器人随便发送一条消息, 看看它是不是正确地获取了图灵机器人的回复。
## 理解自然语言处理器
我们先来理解代码中最简单的部分:
```python {3}
@on_natural_language
async def _(session: NLPSession):
return NLPResult(60.0, 'tuling', {'message': session.msg_text})
```
根据我们前面一章中已经知道的用法,这里就是直接返回置信度为 60.0 的 `tuling` 命令。之所以返回置信度 60.0,是因为自然语言处理器所返回的结果最终会按置信度排序,取置信度最高且大于等于 60.0 的结果来执行。把置信度设为 60.0 可以保证一条消息无法被其它自然语言处理器理解的时候 fallback 到 `tuling` 命令。
## 理解图灵机器人接口的 HTTP 调用
图灵机器人接口的调用也非常简单,虽然看起来代码挺多,但新的概念并不多。
```python {7-23,26-37}
async def call_tuling_api(session: CommandSession, text: str) -> Optional[str]:
if not text:
return None
url = 'http://openapi.tuling123.com/openapi/api/v2'
# 构造请求数据
payload = {
'reqType': 0,
'perception': {
'inputText': {
'text': text
}
},
'userInfo': {
'apiKey': session.bot.config.TULING_API_KEY,
'userId': context_id(session.ctx, use_hash=True)
}
}
group_unique_id = context_id(session.ctx, mode='group', use_hash=True)
if group_unique_id:
payload['userInfo']['groupId'] = group_unique_id
try:
async with aiohttp.ClientSession() as sess:
async with sess.post(url, json=payload) as response:
if response.status != 200:
# 如果 HTTP 响应状态码不是 200, 说明调用失败
return None
resp_payload = json.loads(await response.text())
if resp_payload['results']:
for result in resp_payload['results']:
if result['resultType'] == 'text':
# 返回文本类型的回复
return result['values']['text']
except (aiohttp.ClientError, json.JSONDecodeError, KeyError):
# 抛出上面任何异常,说明调用失败
return None
```
这里的代码主要需要参考 [图灵机器人的官方 API 文档 ](https://www.kancloud.cn/turing/www-tuling123-com/718227 )。
### 构造请求数据
2018-08-21 00:35:35 +08:00
第一段高亮部分是根据图灵机器人的文档构造请求数据,其中有几个需要注意的地方:第 16、17 和 21 行。
2018-08-21 00:30:39 +08:00
2018-12-27 20:23:45 +08:00
第 16 行通过 `session.bot.config` 访问了 NoneBot 的配置对象,`session.bot` 就是当前正在运行的 NoneBot 对象,你在其它任何地方都可以这么用(前提是已经调用过 `nonebot.init()` )。
2018-08-21 00:30:39 +08:00
2018-12-27 20:23:45 +08:00
第 17 和 21 行调用了 `context_id()` 函数,这是 `nonebot.helpers` 模块中提供的一个函数,用于计算 Context 的独特 ID, 有三种模式可以选择( 通过 `mode` 参数传入):`default`、`group`、`user`,默认 `default` ,它们的效果如下表:
2018-08-21 00:30:39 +08:00
| 模式 | 效果 |
| ------------ | --- |
| `default` | 每个用户在每个群、讨论组和私聊都对应不同的 ID |
| `group` | 每个群或讨论组内的成员共用一个 ID, 私聊仍按用户区分 |
| `user` | 每个用户对应不同的 ID, 但不区分用户是在私聊还是群或讨论组 |
`context_id()` 函数还提供 `use_hash` 参数可选地将计算出的 ID 进行 MD5 哈希,以适应某些应用场景。
### 发送请求
第二段高亮的代码是使用 [aiohttp ](https://aiohttp.readthedocs.io/en/stable/ ) 发送 HTTP POST 请求给图灵机器人,并获取它的回复,这段其实没有什么跟 NoneBot 有关的东西,请参考前面给出的图灵机器人的官方 API 文档,里面详细解释了每个返回字段的含义。
## 理解命令处理器
命令处理器这部分虽然代码比较少,但引入了不少新的概念。
2018-08-21 23:51:32 +08:00
```python {1,3-8,13,16,18}
from aiocqhttp.message import escape
EXPR_DONT_UNDERSTAND = (
'我现在还不太明白你在说什么呢,但没关系,以后的我会变得更强呢!',
'我有点看不懂你的意思呀,可以跟我聊些简单的话题嘛',
'其实我不太明白你的意思……',
'抱歉哦,我现在的能力还不能够明白你在说什么,但我会加油的~'
)
2018-08-21 00:30:39 +08:00
@on_command ('tuling')
async def tuling(session: CommandSession):
message = session.get_optional('message')
reply = await call_tuling_api(session, message)
if reply:
await session.send(escape(reply))
else:
2018-12-22 18:09:57 +08:00
await session.send(render_expression(EXPR_DONT_UNDERSTAND))
2018-08-21 00:30:39 +08:00
```
2018-08-21 23:51:32 +08:00
### 可选参数
首先看第 13 行,`session.get_optional()` 可用于获取命令的可选参数,也就是说,从 `session.args` 中尝试获取一个参数,如果没有,返回 `None` ,但并不会中断命令的执行,比较类似于 `dict.get()` 方法。
### 消息转义
再看第 16 行,在调用 `session.send()` 之前先对 `reply` 调用了 `escape()` ,这个 `escape()` 函数是 `aiocqhttp.message` 模块提供的,用于将字符串中的某些特殊字符进行转义。具体来说,这些特殊字符是酷 Q 看作是 CQ 码的一部分的那些字符,包括 `&` 、`[`、`]`、`,`。
CQ 码是酷 Q 用来表示非文本消息的一种表示方法,形如 `[CQ:image,file=ABC.jpg]` 。具体的格式规则,请参考酷 Q 文档的 [CQ 码 ](https://d.cqp.me/Pro/CQ%E7%A0%81 ) 和 CoolQ HTTP API 插件文档的 [CQ 码 ](https://cqhttp.cc/docs/#/CQCode )。
### 发送 Expression
第 18 行使用了 NoneBot 中 Expression 这个概念,或称为「表达」。
2018-12-22 18:09:57 +08:00
Expression 可以是一个 `str` 、元素类型是 `str` 的序列(一般为 `list` 或 `tuple` )或返回类型为 `str` 的 `Callable` 。
2018-08-21 23:51:32 +08:00
2018-12-22 18:09:57 +08:00
`render_expression()` 函数用于将 Expression 渲染成字符串。它首先判断 Expression 的类型,如果 Expression 是一个序列,则首先随机取其中的一个元素,如果是一个 `Callable` ,则调用函数获取返回值。拿到最终的 `str` 类型的 Expression 之后,对它调用 `str.format()` 方法,格式化参数传入 `render_expression()` 函数的命名参数(`**kwargs`),最后返回格式化后的结果。特别地,如果 Expression 是个 `Callable` ,在调用它获取返回值的时候,也会传入 `**kwargs` ,以便函数根据参数来构造字符串。
2018-08-21 23:51:32 +08:00
你可以通过使用序列或 `Callable` 类型的 Expression 来让机器人的回复显得更加自然,甚至,可以利用更高级的人工智能技术来生成对话。