Neo 命令
在最新的代码中,yao 实现了 neo 命令模式。
Neo 命令模式,是指在用户与 Neo 助手聊天过程中切入到业务操作模式,在命令模式下,用户可以向 Yao 发出业务操作指令,Yao 会响应指令并作出合适的响应,在这过程中 Yao 会调用 ChatGPT 进行消息处理与回复。比如,用户在 neo 聊天框中输入处理命令"帮我生成 10 条测试数据",yao 就调用 ChatGPT 智能的生成 10 条测试数据。
优势与特点:
- 对用户友好,对用户来说,不再需要记住具体的功能菜单入口,只需要在聊天框中自然的描述他的想法。
- Yao 融合了命令与聊天功能,ChatGPT 与 yao 无缝集成,在聊天的过程上随时可以调用后端命令。
- 交互性好,前后端的接口使用 SSE 技术,信息的及时性有保证,并且集成了上下文对话功能。
- 扩展性好,yao 把命令的定义接口留给用户,用户可以根据自己的实际需求扩展自己的功能。
整个 neo 命令的执行流程如下:
- ->定义命令
- ->调用命令模板处理器 prepare,读取用户配置的提示词模板
- ->格式化提示词
- ->调用 ChatGPT API 接口
- ->检查返回聊天消息
- ->从聊天消息中解析出处理器参数
- ->让用户确认[可选]
- ->调用数据回调处理器,可以在处理器向前端写入预览消息
- ->浏览器执行 Action(刷新界面)
配置
定义命令
一个 Neo 命令需要包含以下的内容。
- process 回调处理器,处理 ai 返回的数据,在用户确认后调用的处理器或是直接执行的处理器。
- actions 定义,在 xgen 界面上的回调操作。
- prepare 处理器,准备与 ai 交互的提示词,尽可能准确的描述的你的目的。
定义 Neo 命令的方法是在目录/neo 下创建后缀为.cmd.yml
配置文件。
示例:
/neo/table/data.cmd.yml
# Generate test data for the table
#
# yao run neo.table.Data 帮我生成20条测试数据
# 命令的名称 用于匹配用户的提示请求
name: Generate test data for the table
# neo 命令的快捷引用方式,比如在neo助手中输入/data,直接引用这个命令,这样的好处是更快更准确的引用命令
use: data
# 连接器定义
connector: gpt-3_5-turbo
# 用户确认后再执行,或是无需确认,直接执行的处理器。
process: scripts.table.Data
# 处理器的参数类型与说明
args:
- name: data # name 用于筛选ChatGPT返回的json数据,并作为处理器的参数
type: Array
description: The data sets to generate
required: true # 表示参数是必须的,如果不存在会报错
default: []
# 命令执行成功后,在xgen上执行的回调命令,比如这里数据生成后,在xgen界面上自动刷新。
actions:
- name: TableSearch
type: 'Table.search'
payload: {}
# 命令执行的准备处理器
prepare:
# 在before阶段,处理器可以根据xgen传入的上下文参数生成与ai交互的命令
before: scripts.table.DataBefore
# 在after阶段,把ai返回的数据进行格式化处理,在处理器里可以输出到xgen neo助手对话框界面。
after: scripts.table.DataAfter
option:
temperature: 0.8
# 与ChatGPT交互的提示词,角色是system
prompts:
- role: system
# prepare.before处理器返回的json数据{template:''}
content: '{{ template }}'
- role: system
# prepare.before处理器返回的json数据{explain:''}
content: '{{ explain }}'
#让gpt不要解析结果内容,并返回指定的数据内容
- role: system
content: |
- According to my description, according to the template given to you, generate a similar JSON data.
- The Data is what I want to generate by template.
- Reply to me with a message in JSON format only: {"data": "<Replace it with the test data generated by the template>"}
- Do not explain your answer, and do not use punctuation.
# 命令的描述 用于匹配用户的提示请求
description: |
Generate test data for the table
optional:
#命令是否需要用户确认,会在neo助手上显示"执行"按钮
confirm: true
autopilot: true
配置聊天 api guard
参考neo 聊天助手
neo 助手初始化过程
- 检查内置的聊天记录表是否存在,如果不存在创建新表,默认的表名是 yao_neo_conversation。这个表可以在 neo.yml 配置文件中进行修改。
- 初始化聊天机器人的驱动,模型根据 neo.yml 配置的 connector,默认是使用 ChatGPT 的 gpt-3_5-turbo 模型。
- 加载用户的命令列表
*.cmd.yml, *.cmd.yaml
到内存中。
API 响应用户请求
- api guard 中解析出
__sid
作为聊天上下文 id。 - 根据 sid 查找用户的聊天历史,查找表 yao_neo_conversation,聊天历史可以通过配置控制长度。如果是新的会话,聊天历史会是空的。
- 在聊天消息历史中合并用户最新的提问内容,比如,“帮我生成一条数据”
- 根据用户最后的输入消息中是否包含了命令,使用 ChatGPT 进行检查。
命令模式与聊天模式
命令模式
默认情况下,Neo 助手是处于聊天模式,用户与 Neo 的对话基于 ChatGPT 文本处理。
当用户的消息模糊匹配到后端配置的命令列表,或是使用精确命令时,会进入"命令对话模式"。在这个模式中,所有的用户对话都会作为命令的上下文。
在 Neo 助手界面上,会显示一个"退出"按钮。
比如发消息让 AI 生成模型。"/module 请生成销售订单模型"。YAO 会匹配到创建模型的命令,直接进入命令交互模式。首先 AI 会返回一个初步的处理结果。但是你对这个结果还想进行修正或是作补充,你可以再向 AI 发送消息
"请增加总金额字段"
这时候,因为还在命令模式中,AI 会在继续在之前的结果上进行补充。
退出命令模式后就会再次进入聊天模式。
在 Neo 助手界面上,点击"退出"按钮,退出命令模式。
用户命令的匹配过程如下:
检查请求与命令匹配与过滤。
neo 助手的每一次请求都会携带两个当前界面组件的信息。path
与 stack
属性。path 是 neo 助手发送命令时界面的 url 地址,stack 是 xgen 界面组件在界面上的层次关系。
{ "Stack": "Table-Page-pet", "Path": "/x/Table/pet" }
命令的模糊匹配
这两个参数会跟所有 cmd.yml 中配置的 path 与 stack 属性进行比较。可以使用通配符*
,命令中如果没有配置两个参数是匹配所有请求。
把所有的匹配到的命令列表的名称 name 与描述 description,还有用户的请求消息一起提交给 ChatGPT 作判断。如果匹配成功,返回处理命令 cmd 的 id。
所以,一个命令是否匹配的上,取决于 3 个因素
- cmd.yml 中配置的
path
与stack
属性与请求中的path
与stack
属性的匹配度。 - cmd.yml 中名称与描述与用户请求消息的匹配度。
- ChatGPT 的判断。
命令的精确匹配
完全使用 ChatGPT 来匹配命令有可能会失败。如果需要精确匹配,可以在命令中配置 Use 属性,这样就能直接在聊天对话框中使用 Use 命令,比如配置了Use:data
在聊天中就能使用/data 生成数据
定位命令。
命令的名称只能包含大小写字母。聊天消息可以是只包含命令/Command
或是聊天消息以命令作为前缀/Command
,命令后需要有空格。
命令执行过程
成功匹配到命令后,会进入命令处理环节。
准备与 ChatGPT 交互的提示词。
调用处理器 prepareBefore,获取用户定义的模板内容,返回的内容用于填充 prepare.prompts。
js/** * Command neo.table.data * Prepare Hook: Before * @param {*} context 上下文,包含stack/path * @param {*} messages 聊天消息历史 */ function DataBefore(context, messages) { // console.log("DataBefore:", context, messages); context = context || { stack: '-', path: '-' }; messages = messages || []; const { path } = context; if (path === undefined) { done('Error: path not found.\n'); return false; } const tpl = Templates[path]; if (tpl === undefined) { done(`Error: ${path} template not found.\n`); return false; } ssWrite(`Found the ${path} generate rules\n`); return { template: tpl.data, explain: tpl.explain }; }
处理器 prepareBefore 返回的内容与命令定义中的
cmd.prepare
属性中的 prompt 模板进行合并成新的提示模板。这里可以使用语法绑定。
提示模板中所有的的提示词的角色都会被设置成
system
,而用户提问消息的角色会被设置成user
,Yao 结合两部分的内容后,向 ChatGPT 提交请求,并返回请求结果。调用后继处理器 prepareAfter。后继处理的作用是检查,格式化 ChatGPT 返回的消息。如果有必要也可以使用全局函数 ssWrite 写入 neo 助手的聊天对话框。
示例:
js/** * Command neo.table.data * Prepare Hook: After * @param {*} content ChatGPT返回消息 */ function DataAfter(content) { // console.log("DataAfter:", content); const response = JSON.parse(content); const data = response.data || []; if (data.length > 0) { // Print data preview //ssWrite向客户端发送sse消息 ssWrite(`\n`); ssWrite(`| name | type | status | mode | stay | cost | doctor_id |\n`); ssWrite(`| ---- | ---- | ------ | ---- | ---- | ---- | --------- |\n`); data.forEach((item) => { message = `| ${item.name} | ${item.type} | ${item.status} | ${item.mode} | ${item.stay} | ${item.cost} | ${item.doctor_id}|\n`; ssWrite(message); }); ssWrite(` \n\n`); //返回新的消息 return response; } throw new Exception('Error: data is empty.', 500); }
- 校验 ChatGPT 返回的数据并生成处理器参数。经过上面 ChatGPT 与后继处理器的处理后,得到一个初步的结果,这些结果将会作为命令处理器的参数。在这一步里会根据配置的命令参数配置进行参数检查。参数
Command.Args
配置了哪些参数是必输项,参数名是什么,根据参数名筛选上面返回的结果作为命令处理器的参数。
js// validate the args if req.Command.Args != nil && len(req.Command.Args) > 0 { for _, arg := range req.Command.Args { v, ok := data[arg.Name] if arg.Required && !ok { err := fmt.Errorf("\nMissing required argument: %s", arg.Name) return nil, err } // @todo: validate the type args = append(args, v) } }
- 如果配置了
Command.Optional.Confirm
,说明这个命令是需要用户进行确认的,Yao 会给用户返回一个确认的指令,等用户确认后再执行操作。这里比较绕,它的操作是把前面操作得到的结果作为参数与处理器再次封装一个 json 数据,返回给浏览器客户端,等用户确认后再把 json 数据提交到 yao 后端执行。整个动作会被定义成一个新的名称为ExecCommand
的Action
。这个Action
的默认类型会被设置成Service.__neo
,用户确认命令后,会调用一个 Yao 的内部的服务方法Service.__neo
。
go//yao/neo/command/request.go // confirm the command func (req *Request) confirm(args []interface{}, cb func(msg *message.JSON) int) { payload := map[string]interface{}{ "method": "ExecCommand", "args": []interface{}{ req.id, req.Command.Process, args, map[string]interface{}{"stack": req.ctx.Stack, "path": req.ctx.Path}, }, } msg := req.msg(). Action("ExecCommand", "Service.__neo", payload, ""). Confirm(). Done() if req.Actions != nil && len(req.Actions) > 0 { for _, action := range req.Actions { msg.Action(action.Name, action.Type, action.Payload, action.Next) } } cb(msg) }
像这种需要用户确认命令的场景,为什么不直接调用 process,而是需要中间多一层 service 函数。因为在 xgen 上是无法直接调用后端的 process 处理器,需要使用 service 函数(云函数)作为中间层。
如果配置了其它的
Command.Actions
,将会合并在一起,并通过 sse 全局函数发送到客户端。
用户确认命令
经过上面的处理,在 xgen 的 neo 助手界面上会显示提示消息:"消息包含业务指令,是否执行?"。当用户点击执行后,会依次调用上面配置的 actions。
调用 action
Service.__neo
,服务端的Service.__neo
方法会调用Command.Process
,处理用户数据。调用用户自定义的 action,比如
Table.search
,刷新 table 界面,显示最新的 table 数据。如果没有配置
Command.Optional.Confirm
,说明这个命令是可以直接在后台执行,不需要用户确认。Yao 会直接调用处理器req.Command.Process
进行处理。
处理器执行
如果是直接执行的处理器,可以在 actions 里绑定处理器返回的内容。
process: studio.html.Page
actions:
- name: Redirect to the generated page
type: 'Common.historyPush'
payload:
pathname: '{{ iframe }}' #绑定处理器studio.html.Page返回的内容
public: false
示例代码
git clone https://github.com/YaoApp/yao-neo-dev.git
git clone https://github.com/YaoApp/yao-dev-app.git
注意点
整个命令的定义过程与步骤内容比较多。
- ai 并不一定一次就能百分百匹配到命令,可以使用 Use 属性解决这个问题。
- ai 返回的结果不一定十分准确,解决方法是,1 使用准确的提示词,2 让用户确认内容,3 脚本检测并加工处理内容。
- 提示词设置需要一些技巧与遵循一定的规则。