# ai-tool-agent(WIP)

AI Agent Script is a framework for defining AI Agents, their properties, and behaviors for interactive conversations. This document provides an overview of the script structure, functions, and event handling mechanisms used in AI Agent Scripts.

The AI Agent using LLM(Large Language Model) to resolve the target task.

The base class uses to manage all agents and abstract agent.

## AIScript

轻型智能代理脚本引擎

AIScript可以根据`YAML`文档中的脚本参数和指令来执行流程，其中包含大模型工具的调用、模板替换和结果流处理的管道功能。

* `exec(data?:any)` 返回执行结果
* `run(data?:any)` 返回执行完成后的runtime,结果在`runtime.result`上

智能代理脚本的扩展名是`.ai.yaml` or `.ai.yml`.

### 使用方法

按照数组顺序依次执行.

#### 提示词消息

每一行字符串或单值对象都是新增的提示词消息,
该行如果是字符串,那么表示用户(人类)消息,也可以是单值对象,指明角色,表示`[role]: message`:

```yaml
"hi, my assistant." # 纯文本表示用户(人类)消息,等同于: `user: "hi, my assistant."`
assistant: "hi, {{user}}" # key assistant 是 role. 后面的值是角色消息
```

提示词消息也可以被定义为模板,如,`{{user}}`. 模板具体替换数据是定义在prompt参数中,可以由`$prompt`设置或直接在`FRONT-MATTER`中定义:

```yaml
---
prompt:
  add_generation_prompt: true # 默认为true,表示如果提示词最后消息的角色不是`assistant`的时候,会自动添加一个角色为`assistant`提示词消息.
  user: Mike
---
"hi, my assistant."
$prompt: # 以$打头的为指令
  user: Mike
```

#### 提示参数

使用`$prompt`定义提示参数，供提示词模板使用.

```yaml
$prompt:
  add_generation_prompt: true
  user: Mike
```

#### 设置模型参数

用`$parameters`设置模型参数或在`FRONT-MATTER`中定义.

```yaml
---
parameters:
  max_tokens: 512
  temperature: 0.01
---
$parameters:
  temperature: 0.01
```

详细参数设置参考后面"Front-matter 初始化配置参数"章节所述.

### 脚本指令

#### 工具

用`$tool`标签使用注册的所有工具。

##### llm 大模型工具

`$AI` 是`$tool:llm`别名,直接调用大模型工具,默认是将结果追加到`prompt.messages`中,通过设置`shouldAppendResponse:false`关闭组加.

```yaml
$AI:
  max_tokens: 512
  temperature: 0.7
  pushMessage: true # 默认为true,表示将大模型工具返回的结果追加到prompt.messages中.
  shouldAppendResponse: null  # 仅当 pushMessage 为true时候有效, 默认为 undefined.
                              # 当 undefined/null时, 当 `matchedResponse` 或 `add_generation_prompt` 或没有 lastMsg.content 会追加,否则会替换最后一条消息的正文
                              # 当为true时,强制追加一条助手消息,false时,为强制替换最后一条消息的正文.
  block-llm-evt: true         # 阻止llm事件被触发
  block-llmParams-evt: true   # 阻止llmParams事件被触发
  block-llmBefore-evt: true   # 阻止llmBefore事件被触发
  block-all-evt: true         # 阻止所有LLM事件(除了llmStream事件)被触发。
$tool:
  name: llm # 也许这个弄一个简化的别名: !llm
  ...       # 其它named参数

llm: $tool  # 或者这样定义
|- max_tokens: 512    # 当没有 line indicator '- ' 的时候, 必须加上"|-" 表示为连接上一个对象.
|- temperature: 0.7

- llm: $tool  # 或者这样定义
    max_tokens: 512
    temperature: 0.7
```

大模型参数还可以在`front-matter`中配置:

```yaml
---
output:
  type: "object"
  properties:
    intent:
      type: "string"
    categories:
      type: "array"
      items:
        type: "string"
    reason:
      type: "string"
  required: ["intent", "categories", "reason"]
parameters:
  max_tokens: 512  # 别太大,也别太小, 建议512, 默认2048, 用处是当模型响应无限不能停止时,这个就能控制大模型最多返回的token长度.
  continueOnLengthLimit: true
  maxRetry: 7    # 当大模型的响应不全时, 因受到max_tokens的限制,这个是自动再次执行LLM的次数,默认7次.
  stream: true # 就是默认启用大模型流式响应,高于llmStream优先级.
  timeout: 30000 # 设置响应30秒(单位是ms)超时,不设置就默认为120秒.
  response_format:
    type: json_object
  minTailRepeatCount: 7 # 最少尾部重复次数, 默认为7, For stream mode only, 当检测到大模型响应返回的尾部序列连续重复4次,就停止响应. 设置为0则不检测.
llmStream: true # 是否默认启用大模型流式响应
---
- $AI # 执行大模型, 此处可以省略,因为默认当存在消息,并且到执行脚本结束时一次也没有调用过大模型,那么会自动执行大模型. 设置`autoRunLLMIfPromptAvailable: false`可以关闭该功能.
```

支持流式输出(在ai-agent脚本执行器中是默认开启的), 当启用`llmStream`(也可以在调用参数中设置`stream: true`), 大模型会返回流式响应, 通过事件`llm-stream`触发,
事件处理器的参数是`(event, part: AIResult, content: string)`, `part`是当前大模型返回的响应对象, `content`是当前大模型返回的响应中的内容的累积.

如果初始化数据中存在`prompt.messages`,并且在脚本中没有手动调用`$AI`,那么会在结束时自动调用. 这个可以通过设置`autoRunLLMIfPromptAvailable: false`关闭.

如果`response_format.type="response_format"` 和 `output`同时存在,那么返回结果会是`output`中的JSON Object内容,而不是大模型直接返回的内容.
你可以通过设置`forceJson: false`强制关闭.

新增功能: 当最后一个消息是未完成的时候,并且没有设置`add_generation_prompt`,并且在最后的消息中不存在响应的结果模板替换,那么
大模型返回结果不会添加到新的`assistant`消息,而是在最后一条消息上补全. 这个功能可以通过配置`shouldAppendResponse: true`关闭.

如果没有定义输出变量,那么默认输出为`"RESPONSE"`在`prompt`中.
也可以在消息中定义工具的输出替换:

```yaml
- "Greet Jacky:[[GREETINGS]]\n"
- $AI: # 或者不需要手动调用,只要发现存在`[[]]`就自动调用?
  stop_words: '.'
  aborter: ?= new AbortController() # 如果没有设置,则使用系统的AbortController. 可以使用$abort指令随时停止大模型调用.
  ... # 其它named参数
```

这样,会在`prompt`中定义`GREETINGS`变量, 工具的结果被放入.
当logLevel 设为 info, 会显示消息的结果:

```bash
Greet Jacky: GREETINGS Hi there Jacky! It's nice to meet you.
```

```bash
🚀 [info]: { role: "user", content: "a simple joke without new line: [[JOKE]] Haha." }
🚀 [info]: a simple joke without new line: Why don't scientists trust atoms?

Because they make up everything. [[JOKE]] Haha. { role: "user" }
```

有时候返回并没有遵循指令,需要先处理返回结果. 比如这里需要replace `\n` to ' '.

已经可以通过事件来处理, llm 工具会在完成后,触发 `llm` 事件,只需要在该事件返回新的结果:

```yaml
---
prompt:
  add_generation_prompt: true
parameters:
  max_tokens: 5
  continueOnLengthLimit: true
---
!fn |-
  trimLLMResult(event, result) {
    return result.content.replace(/[\\n\\r]+/g, ' ')
  }
"a simple joke without new line: [[JOKE]] Haha."
$on:
  event: llm
  callback: $trimLLMResult
$tool: llm
```

#### 管道

`$pipe`将上一次的结果传递给下一个, 支持缩写`$|func`

```yaml
- toolId: $tool
# 上一个函数的返回结果传递到`func1|print`, 如果pipe没有参数,就传递给下一个数组元素.如果下一个元素本身是对象,就合并.
- |
- $func1
- $pipe
- $print
```

```yaml
- llm: $tool
- $|func1
- $|print
```

#### 定义函数

`!fn` 使用自定义 tag 定义函数

```yaml
!fn |-
  function func1 ({arg1, arg2}) {
  }
# function 可以省略:
!fn |-
  func1 ({arg1, arg2}) {
  }
```

定义函数中可以使用 `async require(moduleFilename)` 加载本地 esm 文件.

在函数中,可以使用`this`来获取当前脚本的runtime的所有方法.

所有的自定义函数都必须通过`$`打头引用. 如,在上面例子定义了`func1`,那么调用的时候必须用`$func1`:

```yaml
$func1:
  arg1: 1
  arg2: 2
```

#### 定义模板函数

`!fn#` 使用自定义 tag 定义模板函数,模板函数可以可以在默认的JinJa模板中使用函数

```yaml
---
content:
  a: 1
  b: 2
---
!fn# |-
  function toString(value) {
    return JSON.stringify(value)
  }
$format: "{{toString(content)}}"
```

#### 事件

`$on` 函数支持事件监听, `$once`函数支持事件监听一次, `$emit`函数支持触发事件
`$off`函数支持取消事件监听

##### `$on` 和 `$once` 事件监听函数

参数如下:

- event: 事件名称
- callback: 回调函数或表达式

函数作为回调函数:

```yaml
!fn |-
  onTest (event, arg1) { return {...arg1, event: event.type}}
$on:
  event: test
  callback: onTest # 命名函数监听,可以取消事件监听
$once:             # 触发一次后自动取消事件监听
  event: test
  callback: !fn |- # 匿名函数监听, 不能取消事件监听
    (event, arg1) { return {...arg1, event: event.type}}
$emit:             # 触发事件
  event: test
  args:
    a: 1
    b: 2
$off:
  event: test
  callback: onTest
```

表达式作为回调函数, 表达式中的参数如下:

- event: 事件实例
  - event.type: 事件名称
  - event.target: 事件源,也就是当前脚本runtime
- arg1: 事件监听函数传入的第一个参数值
- args: 事件监听函数传入的剩下参数值列表,如果有的话

这些参数等同于回调函数中的: `(event, arg1, ...args) => void|any`

```yaml
$on:
  event: test
  callback: "?={...arg1, event: event.type}" # 无法取消事件监听
```

##### `$emit` 触发事件函数

参数如下:

- event: 事件类型,字符串,如: `test`
- args: 事件监听函数传入的参数值或参数值列表,如果有的话

```yaml
$emit:
  event: test
  args: # 一个对象参数
    a: 1
    b: 2
$emit:
  event: test
  args: # 表示两个对象参数
    - a: 1
    - b: 2

```

##### 脚本事件类型

- `beforeCall`: 在函数调用前触发
  - 回调参数: `(event, name, params, fn) => void|params`
  - 当回调函数返回值的时候, 则表示修改参数.
- `afterCall`: 在函数返回结果前触发
  - 回调参数: `(event, name, params, result, fn) => void|result`
  - 当回调函数返回值的时候, 则表示修改返回结果.
- `llmParams`: 事件在大模型调用前触发,可用于修改传递给大模型的参数.
  - 回调参数: `(event, params: any) => void|result<any>`
- `llmBefore`: 事件在大模型调用前触发,不可修改参数，仅作为通知.
  - 回调参数: `(event, params: any) => void`
- `llm`: 事件在大模型返回结果前触发,用于修改大模型返回结果.
  - 回调参数: `(event, result: string) => void|result<string>`
- `llmStream`: 当大模型以流式返回结果的时候触发
  - 回调参数: `(event, chunk: AIResult, content: string, retryCount: number) => void`
    - chunk: 当前流块内容
    - content: 当前得到的所有chunk的字符串内容
    - retryCount: 当因为达到`max_token`后自动调用llm的重试次数
- `llmRequest`: 事件在需要得到大模型结果的时候触发,用于通过事件调用大模型,得到大模型结果. `[[RESPONSE]]`模板会触发该事件
  - 回调参数: `(event, messages: AIChatMessage[], options?) => void|result<string>`
  - 使用开关`disableLlmRequest: true`禁用该事件.
- `ready`: 脚本交互准备完成后触发,可以通过`$ready()`函数强制设置是否处于准备状态.
  - 回调参数: `(event, isReady: boolean) => void`
- `load-chats`: 加载聊天记录时触发.
  - 回调参数: `(event, filename: string) => AIChatMessage[]|void`
  - 当回调函数返回值的时候, 则表示加载的聊天记录.
- `save-chats`: 保存聊天记录时触发.
  - 回调参数: `(event, messages: AIChatMessage[], filename?: string) => void`

注意: 事件中的`event`参数是`Event`对象, 是在脚本中触发才会被添加的参数, `this`为脚本runtime;
如果直接在js中触发,则没有该参数,这个时候`this`就是`Event`对象, `this.target`才是脚本runtime. 而如果要返回结果则必须放在`this.result`上.

#### if 函数

`$if` 函数支持条件判断

```yaml
$set:
  a: 1
- $if: "a == 1" # 表达式判断
  then: # then 函数
    $echo: Ok
  else: # "else 函数"
    $echo: Not OK

!fn |-
  isOk(ok) {return ok}
- $if:
    $isOK: true # 函数判断
  then: # then 函数
    $echo: Ok
  else: # "else 函数"
    $echo: Not OK
```

#### $format 函数

`$format` 函数使用Jinja2模板格式化字符串,消息的格式化也是使用的Jinja2模板,这也是HuggingFace大模型支持的模板格式.

#### $exec 调用外部脚本函数

`$exec`

```yaml
$exec:
  id: 'script id'
  filename: 'script filename' # 脚本文件名与id只能选一个
  args: # 调用脚本参数
```

#### $set/$get 变量操作

```yaml
$set:
  testVar: 124
  var2: !fn (key) { return key + ' hi' }
$get:
  - testVar
  - var2
```

#### 表达式

`?=<expression>`

```yaml
- $echo: ?=23+5
```

#### Front-matter 初始化配置参数

可以初始化配置: 大模型参数`parameters`和提示词变量`prompt`.

```yaml
---
autoRunLLMIfPromptAvailable: true # 默认true, 是否在退出脚本时如果存在提示消息,并且从未调用过大模型,自动调用大模型
forceJson: null                   # 默认`undefined` 当`undefined`|`null`时会自动根据 output 和 response_format.type 判断
disableLlmRequest: false         # 默认 false, 是否禁用`llm-request`事件
completion_delimiter: ''         # 可选的参数, 用于在提示词中指示输出结束的标记, 如果使用,该结束标记会自动加入到 stop_words 中 默认无

prompt:
  add_generation_prompt: true
input:
  conversation: "messages[1].content"
output:
  type: "object"
  properties:
    intent:
      type: "string"
    reason:
      type: "string"
  required: ["intent", "reason"]
parameters:
  continueOnLengthLimit: true
  timeout: 120000      # LLM max timeout, defaults to 120000ms(2min)
  maxRetry: 6
  max_tokens: 5        # 表示模型最多生成 5 个 token 作为输出。
  temperature: 0.7     # 数值越高，生成的文本就越有创意和多样性。
  top_k: 40            # 从一堆词里挑出前 k 个最可能的词，然后再从这 k 个词里随机选一个, 默认40
  top_p: 0.95          # 设定一个概率门槛, 让模型只从最有可能的词汇中挑选，直到累积概率达到设定的阈值.这样能让生成的文本既合理又有变化。
  min_p: 0.05          # 设定一个最低概率门槛，确保模型至少考虑那些概率不低于这个门槛的词汇，以保证生成的文本质量和多样性。
  seed: 4294967295     # 种子，确保每次使用相同的 seed 时，模型都能生成相同的结果，方便复现和调试。
  tfs_z: 1             # 一种温度缩放策略（Temperature-Free Sampling with z）的参数，它帮助模型在生成文本时更加注重高质量的词汇选择，同时保持一定的多样性。
  typical_p: 1         # 设定一个标准，让模型只从那些既常见又合理的词汇中选择，这样既能保证文本质量又能增加多样性。
  repeat_last_n: 64    # 记住最后 64 个token，别重复。” 这样可以避免生成的文本中有过多重复的内容。
  repeat_penalty: 1    # 模型设定了一个惩罚机制，让它尽量避免重复使用相同的词汇，这样生成的文本会更加丰富多样。
  presence_penalty: 0  # 告诉模型：“如果某个词已经出现过，就少用它。” 这样可以让生成的文本更加多样，减少重复。
  frequency_penalty: 0 # 告诉模型：“如果某个词用得太多，下次就少用它。” 这样可以减少重复，让生成的文本更加多样化。
  messages:
    - role: system
      content: Carefully Think about the intent of following The CONVERSATION user provided. Output the json object with the Intent Category and Reason.
    - role: user
      content: |-
        The CONVERSATION:
        {{ conversation }}
  response_format:
    type: json_object # 配合output再加上它会强制把text转为json对象,否则是字符串的json.
  stop_words: []      # 停止词列表, 默认为空

---
```

### ID

由两部分构成，`<name>[|<附加名称|default>]`, 第二部分`附加名称`可选，`附加名称`一般和大模型相关， `default` 表示不匹配的大模型时候才用的。

## AIScriptServer

* [AiScriptServer]
  * AiScriptServer.load 加载编译脚本文件
  * 自动加载(`$loadChats(filename?: string)`)历史记录如果设置了`chatsDir`以及存在`id`参数, 保存(`$saveChats(filename?: string)`)要手动调用.

## AI Character Agent Script Type

An AI Character Agent Script defines AI characters, their properties, and behaviors. Set `type` to `char` to indicate an AI character script.

```yaml
---
type: char
---
```

Characters can store both character and user information. Use isBot to differentiate between users and AI characters.
