Skip to content

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

yaml
# 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 助手的每一次请求都会携带两个当前界面组件的信息。pathstack 属性。path 是 neo 助手发送命令时界面的 url 地址,stack 是 xgen 界面组件在界面上的层次关系。

json
{ "Stack": "Table-Page-pet", "Path": "/x/Table/pet" }

命令的模糊匹配

这两个参数会跟所有 cmd.yml 中配置的 path 与 stack 属性进行比较。可以使用通配符*,命令中如果没有配置两个参数是匹配所有请求。

把所有的匹配到的命令列表的名称 name 与描述 description,还有用户的请求消息一起提交给 ChatGPT 作判断。如果匹配成功,返回处理命令 cmd 的 id。

所以,一个命令是否匹配的上,取决于 3 个因素

  • cmd.yml 中配置的pathstack属性与请求中的pathstack属性的匹配度。
  • 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 后端执行。整个动作会被定义成一个新的名称为ExecCommandAction。这个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 里绑定处理器返回的内容。

yaml
process: studio.html.Page
actions:
  - name: Redirect to the generated page
    type: 'Common.historyPush'
    payload:
      pathname: '{{ iframe }}' #绑定处理器studio.html.Page返回的内容
      public: false

示例代码

sh

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 脚本检测并加工处理内容。
  • 提示词设置需要一些技巧与遵循一定的规则。