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