ai-agent插件新版本 (#1311)

Co-authored-by: Kent Dong <ch3cho@qq.com>
This commit is contained in:
xingyunyang01
2024-09-18 10:52:23 +08:00
committed by GitHub
parent 59fe661cd2
commit 0471249e7f
5 changed files with 385 additions and 260 deletions

View File

@@ -5,14 +5,10 @@ description: AI Agent插件配置参考
---
## 功能说明
一个可定制化的 API AI Agent支持配置 http method 类型为 GET 与 POST 的 API目前只支持非流式模式。
一个可定制化的 API AI Agent支持配置 http method 类型为 GET 与 POST 的 API支持多轮对话,支持流式与非流式模式。
agent流程图如下
![ai-agent](https://github.com/user-attachments/assets/b0761a0c-1afa-496c-a98e-bb9f38b340f8)
## 运行属性
插件执行阶段:`默认阶段`
插件执行优先级:`20`
## 配置字段
@@ -46,18 +42,19 @@ agent流程图如下
`apiProvider`的配置字段说明如下:
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|-----------------|-----------|---------|--------|------------------------------------------|
| `apiKey` | object | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌。 |
| `serviceName` | string | 必填 | - | 访问外部 API 服务名 |
| `servicePort` | int | 必填 | - | 访问外部 API 服务端口 |
| `domain` | string | 必填 | - | 访访问外部 API 时域名 |
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|-------------------|-----------|---------|--------|------------------------------------------|
| `apiKey` | object | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌。 |
| `maxExecutionTime`| int | 必填 | 50000 | 每一次请求API的超时时间单位毫秒。 |
| `serviceName` | string | 必填 | - | 访问外部 API 服务 |
| `servicePort` | int | 必填 | - | 访问外部 API 服务端口 |
| `domain` | string | 必填 | - | 访访问外部 API 时域名 |
`apiKey`的配置字段说明如下:
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|-------------------|---------|------------|--------|-------------------------------------------------------------------------------|
| `in` | string | 非必填 | header | 在访问外部 API 服务时进行认证的令牌是放在 header 中还是放在 query 中,默认是 header
|-------------------|---------|------------|--------|-----------------------------------------------------------------------------------------|
| `in` | string | 非必填 | none | 在访问外部 API 服务时进行认证的令牌是放在 header 中还是放在 query 中,如果API没有令牌填none
| `name` | string | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌的名称。 |
| `value` | string | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌的值。 |
@@ -75,11 +72,8 @@ agent流程图如下
|-----------------|-----------|-----------|--------|---------------------------------------------|
| `question` | string | 非必填 | - | Agent ReAct 模板的 question 部分 |
| `thought1` | string | 非必填 | - | Agent ReAct 模板的 thought1 部分 |
| `actionInput` | string | 非必填 | - | Agent ReAct 模板的 actionInput 部分 |
| `observation` | string | 非必填 | - | Agent ReAct 模板的 observation 部分 |
| `thought2` | string | 非必填 | - | Agent ReAct 模板的 thought2 部分 |
| `finalAnswer` | string | 非必填 | - | Agent ReAct 模板的 finalAnswer 部分 |
| `begin` | string | 非必填 | - | Agent ReAct 模板的 begin 部分 |
## 用法示例
@@ -325,6 +319,21 @@ curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
**请求示例**
```shell
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
-H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role": "user","content": "济南的天气如何?"},{ "role": "assistant","content": "目前济南市的天气为多云气温为24℃数据更新时间为2024年9月12日21时50分14秒。"},{"role": "user","content": "北京呢?"}],"presence_penalty":0,"temperature":0,"top_p":0}'
```
**响应示例**
```json
{"id":"ebd6ea91-8e38-9e14-9a5b-90178d2edea4","choices":[{"index":0,"message":{"role":"assistant","content":"目前北京市的天气为晴朗气温为19℃数据更新时间为2024年9月12日22时17分40秒。"},"finish_reason":"stop"}],"created":1723187991,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":999,"completion_tokens":76,"total_tokens":1075}}
```
**请求示例**
```shell
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
-H 'Accept: application/json, text/event-stream' \
@@ -351,4 +360,4 @@ curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
```json
{"id":"65dcf12c-61ff-9e68-bffa-44fc9e6070d5","choices":[{"index":0,"message":{"role":"assistant","content":" “九头蛇万岁!”的德语翻译为“Hoch lebe Hydra!”。"},"finish_reason":"stop"}],"created":1724043865,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":908,"completion_tokens":52,"total_tokens":960}}
```
```

View File

@@ -4,7 +4,7 @@ keywords: [ AI Gateway, AI Agent ]
description: AI Agent plugin configuration reference
---
## Functional Description
A customizable API AI Agent that supports configuring HTTP method types as GET and POST APIs. Currently, it only supports non-streaming mode.
A customizable API AI Agent that supports configuring HTTP method types as GET and POST APIs. Supports multiple dialogue rounds, streaming and non-streaming modes.
The agent flow chart is as follows:
![ai-agent](https://github.com/user-attachments/assets/b0761a0c-1afa-496c-a98e-bb9f38b340f8)
@@ -41,17 +41,18 @@ The configuration fields for `apis` are as follows:
| `api` | string | Required | - | OpenAPI documentation of the tool |
The configuration fields for `apiProvider` are as follows:
| Name | Data Type | Requirement | Default Value | Description |
|-----------------|-----------|-------------|---------------|--------------------------------------------------|
| `apiKey` | object | Optional | - | Token for authentication when accessing external API services. |
| `serviceName` | string | Required | - | Name of the external API service |
| `servicePort` | int | Required | - | Port of the external API service |
| `domain` | string | Required | - | Domain for accessing the external API |
| Name | Data Type | Requirement | Default Value | Description |
|-------------------|-----------|-------------|---------------|--------------------------------------------------|
| `apiKey` | object | Optional | - | Token for authentication when accessing external API services. |
| `maxExecutionTime`| int | Optional | 50000 | Timeout for each request to the API, in milliseconds|
| `serviceName` | string | Required | - | Name of the external API service |
| `servicePort` | int | Required | - | Port of the external API service |
| `domain` | string | Required | - | Domain for accessing the external API |
The configuration fields for `apiKey` are as follows:
| Name | Data Type | Requirement | Default Value | Description |
|-------------------|-----------|-------------|---------------|-------------------------------------------------------------------------------------|
| `in` | string | Optional | header | Whether the authentication token for accessing the external API service is in the header or in the query; default is header. |
| `in` | string | Optional | none | Whether the authentication token for accessing the external API service is in the header or in the query; If the API does not have a token, fill in none. |
| `name` | string | Optional | - | The name of the token for authentication when accessing the external API service. |
| `value` | string | Optional | - | The value of the token for authentication when accessing the external API service. |
@@ -67,11 +68,8 @@ The configuration fields for `chTemplate` and `enTemplate` are as follows:
|-----------------|-----------|-------------|---------------|---------------------------------------------------|
| `question` | string | Optional | - | The question part of the Agent ReAct template |
| `thought1` | string | Optional | - | The thought1 part of the Agent ReAct template |
| `actionInput` | string | Optional | - | The actionInput part of the Agent ReAct template |
| `observation` | string | Optional | - | The observation part of the Agent ReAct template |
| `thought2` | string | Optional | - | The thought2 part of the Agent ReAct template |
| `finalAnswer` | string | Optional | - | The finalAnswer part of the Agent ReAct template |
| `begin` | string | Optional | - | The begin part of the Agent ReAct template |
## Usage Example
**Configuration Information**
@@ -308,6 +306,17 @@ curl 'http://<replace with gateway public IP>/api/openai/v1/chat/completions' \
curl 'http://<replace with gateway public IP>/api/openai/v1/chat/completions' \
-H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"What is the current weather in Jinan?"},{"role":"assistant","content":" The current weather condition in Jinan is overcast, with a temperature of 31°C. This information was last updated on August 9, 2024, at 15:12 (Beijing time)."},{"role":"user","content":"BeiJing?"}],"presence_penalty":0,"temperature":0,"top_p":0}'
```
**Response Example**
```json
{"id":"ebd6ea91-8e38-9e14-9a5b-90178d2edea4","choices":[{"index":0,"message":{"role":"assistant","content":" The current weather condition in Beijing is overcast, with a temperature of 19°C. This information was last updated on Sep 12, 2024, at 22:17 (Beijing time)."},"finish_reason":"stop"}],"created":1723187991,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":999,"completion_tokens":76,"total_tokens":1075}}
```
**Request Example**
```shell
curl 'http://<replace with gateway public IP>/api/openai/v1/chat/completions' \
-H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"What is the current weather in Jinan? Please indicate in Fahrenheit and respond in Japanese."}],"presence_penalty":0,"temperature":0,"top_p":0}'
```
**Response Example**

View File

@@ -46,7 +46,7 @@ type Response struct {
}
// 用于存放拆解出来的工具相关信息
type Tool_Param struct {
type ToolsParam struct {
ToolName string `yaml:"toolName"`
Path string `yaml:"path"`
Method string `yaml:"method"`
@@ -56,10 +56,11 @@ type Tool_Param struct {
}
// 用于存放拆解出来的api相关信息
type APIParam struct {
APIKey APIKey `yaml:"apiKey"`
URL string `yaml:"url"`
Tool_Param []Tool_Param `yaml:"tool_Param"`
type APIsParam struct {
APIKey APIKey `yaml:"apiKey"`
URL string `yaml:"url"`
MaxExecutionTime int64 `yaml:"maxExecutionTime"`
ToolsParam []ToolsParam `yaml:"toolsParam"`
}
type Info struct {
@@ -153,7 +154,10 @@ type APIProvider struct {
ServicePort int64 `required:"true" yaml:"servicePort" json:"servicePort"`
// @Title zh-CN 服务域名
// @Description zh-CN 服务域名,例如 restapi.amap.com
Domin string `required:"true" yaml:"domain" json:"domain"`
Domain string `required:"true" yaml:"domain" json:"domain"`
// @Title zh-CN 每一次请求api的超时时间
// @Description zh-CN 每一次请求api的超时时间单位毫秒默认50000
MaxExecutionTime int64 `yaml:"maxExecutionTime" json:"maxExecutionTime"`
// @Title zh-CN 通义千问大模型服务的key
// @Description zh-CN 通义千问大模型服务的key
APIKey APIKey `required:"true" yaml:"apiKey" json:"apiKey"`
@@ -167,11 +171,8 @@ type APIs struct {
type Template struct {
Question string `yaml:"question" json:"question"`
Thought1 string `yaml:"thought1" json:"thought1"`
ActionInput string `yaml:"actionInput" json:"actionInput"`
Observation string `yaml:"observation" json:"observation"`
Thought2 string `yaml:"thought2" json:"thought2"`
FinalAnswer string `yaml:"finalAnswer" json:"finalAnswer"`
Begin string `yaml:"begin" json:"begin"`
}
type PromptTemplate struct {
@@ -189,7 +190,7 @@ type LLMInfo struct {
ServicePort int64 `required:"true" yaml:"servicePort" json:"servicePort"`
// @Title zh-CN 大模型服务域名
// @Description zh-CN 大模型服务域名,例如 dashscope.aliyuncs.com
Domin string `required:"true" yaml:"domin" json:"domin"`
Domain string `required:"true" yaml:"domain" json:"domain"`
// @Title zh-CN 大模型服务的key
// @Description zh-CN 大模型服务的key
APIKey string `required:"true" yaml:"apiKey" json:"apiKey"`
@@ -222,7 +223,7 @@ type PluginConfig struct {
// @Description zh-CN 用于存储llm使用信息
LLMInfo LLMInfo `required:"true" yaml:"llm" json:"llm"`
LLMClient wrapper.HttpClient `yaml:"-" json:"-"`
APIParam []APIParam `yaml:"-" json:"-"`
APIsParam []APIsParam `yaml:"-" json:"-"`
PromptTemplate PromptTemplate `yaml:"promptTemplate" json:"promptTemplate"`
}
@@ -260,11 +261,13 @@ func initAPIs(gjson gjson.Result, c *PluginConfig) error {
return errors.New("apiProvider domain is required")
}
apiKeyIn := item.Get("apiProvider.apiKey.in").String()
if apiKeyIn != "query" {
apiKeyIn = "header"
maxExecutionTime := item.Get("apiProvider.maxExecutionTime").Int()
if maxExecutionTime == 0 {
maxExecutionTime = 50000
}
apiKeyIn := item.Get("apiProvider.apiKey.in").String()
apiKeyName := item.Get("apiProvider.apiKey.name")
apiKeyValue := item.Get("apiProvider.apiKey.value")
@@ -289,13 +292,13 @@ func initAPIs(gjson gjson.Result, c *PluginConfig) error {
return err
}
var allTool_param []Tool_Param
var allTool_param []ToolsParam
//拆除服务下面的每个api的path
for path, pathmap := range apiStruct.Paths {
//拆解出每个api对应的参数
for method, submap := range pathmap {
//把参数列表存起来
var param Tool_Param
var param ToolsParam
param.Path = path
param.ToolName = submap.OperationID
if method == "get" {
@@ -319,13 +322,14 @@ func initAPIs(gjson gjson.Result, c *PluginConfig) error {
allTool_param = append(allTool_param, param)
}
}
apiParam := APIParam{
APIKey: APIKey{In: apiKeyIn, Name: apiKeyName.String(), Value: apiKeyValue.String()},
URL: apiStruct.Servers[0].URL,
Tool_Param: allTool_param,
apiParam := APIsParam{
APIKey: APIKey{In: apiKeyIn, Name: apiKeyName.String(), Value: apiKeyValue.String()},
URL: apiStruct.Servers[0].URL,
MaxExecutionTime: maxExecutionTime,
ToolsParam: allTool_param,
}
c.APIParam = append(c.APIParam, apiParam)
c.APIsParam = append(c.APIsParam, apiParam)
}
return nil
}
@@ -338,60 +342,36 @@ func initReActPromptTpl(gjson gjson.Result, c *PluginConfig) {
if c.PromptTemplate.Language == "EN" {
c.PromptTemplate.ENTemplate.Question = gjson.Get("promptTemplate.enTemplate.question").String()
if c.PromptTemplate.ENTemplate.Question == "" {
c.PromptTemplate.ENTemplate.Question = "the input question you must answer"
c.PromptTemplate.ENTemplate.Question = "input question to answer"
}
c.PromptTemplate.ENTemplate.Thought1 = gjson.Get("promptTemplate.enTemplate.thought1").String()
if c.PromptTemplate.ENTemplate.Thought1 == "" {
c.PromptTemplate.ENTemplate.Thought1 = "you should always think about what to do"
}
c.PromptTemplate.ENTemplate.ActionInput = gjson.Get("promptTemplate.enTemplate.actionInput").String()
if c.PromptTemplate.ENTemplate.ActionInput == "" {
c.PromptTemplate.ENTemplate.ActionInput = "the input to the action"
c.PromptTemplate.ENTemplate.Thought1 = "consider previous and subsequent steps"
}
c.PromptTemplate.ENTemplate.Observation = gjson.Get("promptTemplate.enTemplate.observation").String()
if c.PromptTemplate.ENTemplate.Observation == "" {
c.PromptTemplate.ENTemplate.Observation = "the result of the action"
c.PromptTemplate.ENTemplate.Observation = "action result"
}
c.PromptTemplate.ENTemplate.Thought1 = gjson.Get("promptTemplate.enTemplate.thought2").String()
if c.PromptTemplate.ENTemplate.Thought1 == "" {
c.PromptTemplate.ENTemplate.Thought1 = "I now know the final answer"
}
c.PromptTemplate.ENTemplate.FinalAnswer = gjson.Get("promptTemplate.enTemplate.finalAnswer").String()
if c.PromptTemplate.ENTemplate.FinalAnswer == "" {
c.PromptTemplate.ENTemplate.FinalAnswer = "the final answer to the original input question, please give the most direct answer directly in Chinese, not English, and do not add extra content."
}
c.PromptTemplate.ENTemplate.Begin = gjson.Get("promptTemplate.enTemplate.begin").String()
if c.PromptTemplate.ENTemplate.Begin == "" {
c.PromptTemplate.ENTemplate.Begin = "Begin! Remember to speak as a pirate when giving your final answer. Use lots of \"Arg\"s"
c.PromptTemplate.ENTemplate.Thought2 = gjson.Get("promptTemplate.enTemplate.thought2").String()
if c.PromptTemplate.ENTemplate.Thought2 == "" {
c.PromptTemplate.ENTemplate.Thought2 = "I know what to respond"
}
} else if c.PromptTemplate.Language == "CH" {
c.PromptTemplate.CHTemplate.Question = gjson.Get("promptTemplate.chTemplate.question").String()
if c.PromptTemplate.CHTemplate.Question == "" {
c.PromptTemplate.CHTemplate.Question = "你需要回答的输入问题"
c.PromptTemplate.CHTemplate.Question = "输入要回答的问题"
}
c.PromptTemplate.CHTemplate.Thought1 = gjson.Get("promptTemplate.chTemplate.thought1").String()
if c.PromptTemplate.CHTemplate.Thought1 == "" {
c.PromptTemplate.CHTemplate.Thought1 = "你应该总是思考该做什么"
}
c.PromptTemplate.CHTemplate.ActionInput = gjson.Get("promptTemplate.chTemplate.actionInput").String()
if c.PromptTemplate.CHTemplate.ActionInput == "" {
c.PromptTemplate.CHTemplate.ActionInput = "行动的输入必须出现在Action后"
c.PromptTemplate.CHTemplate.Thought1 = "考虑之前和之后的步骤"
}
c.PromptTemplate.CHTemplate.Observation = gjson.Get("promptTemplate.chTemplate.observation").String()
if c.PromptTemplate.CHTemplate.Observation == "" {
c.PromptTemplate.CHTemplate.Observation = "行动结果"
c.PromptTemplate.CHTemplate.Observation = "行动结果"
}
c.PromptTemplate.CHTemplate.Thought1 = gjson.Get("promptTemplate.chTemplate.thought2").String()
if c.PromptTemplate.CHTemplate.Thought1 == "" {
c.PromptTemplate.CHTemplate.Thought1 = "我现在知道最终答案"
}
c.PromptTemplate.CHTemplate.FinalAnswer = gjson.Get("promptTemplate.chTemplate.finalAnswer").String()
if c.PromptTemplate.CHTemplate.FinalAnswer == "" {
c.PromptTemplate.CHTemplate.FinalAnswer = "对原始输入问题的最终答案"
}
c.PromptTemplate.CHTemplate.Begin = gjson.Get("promptTemplate.chTemplate.begin").String()
if c.PromptTemplate.CHTemplate.Begin == "" {
c.PromptTemplate.CHTemplate.Begin = "再次重申,不要修改以上模板的字段名称,开始吧!"
c.PromptTemplate.CHTemplate.Thought2 = gjson.Get("promptTemplate.chTemplate.thought2").String()
if c.PromptTemplate.CHTemplate.Thought2 == "" {
c.PromptTemplate.CHTemplate.Thought2 = "我知道该回应什么"
}
}
}
@@ -400,7 +380,7 @@ func initLLMClient(gjson gjson.Result, c *PluginConfig) {
c.LLMInfo.APIKey = gjson.Get("llm.apiKey").String()
c.LLMInfo.ServiceName = gjson.Get("llm.serviceName").String()
c.LLMInfo.ServicePort = gjson.Get("llm.servicePort").Int()
c.LLMInfo.Domin = gjson.Get("llm.domain").String()
c.LLMInfo.Domain = gjson.Get("llm.domain").String()
c.LLMInfo.Path = gjson.Get("llm.path").String()
c.LLMInfo.Model = gjson.Get("llm.model").String()
c.LLMInfo.MaxIterations = gjson.Get("llm.maxIterations").Int()
@@ -419,6 +399,6 @@ func initLLMClient(gjson gjson.Result, c *PluginConfig) {
c.LLMClient = wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: c.LLMInfo.ServiceName,
Port: c.LLMInfo.ServicePort,
Host: c.LLMInfo.Domin,
Host: c.LLMInfo.Domain,
})
}

View File

@@ -17,6 +17,7 @@ import (
// 用于统计函数的递归调用次数
const ToolCallsCount = "ToolCallsCount"
const StreamContextKey = "Stream"
// react的正则规则
const ActionPattern = `Action:\s*(.*?)[.\n]`
@@ -53,7 +54,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrap
return types.ActionContinue
}
func firstReq(config PluginConfig, prompt string, rawRequest Request, log wrapper.Log) types.Action {
func firstReq(ctx wrapper.HttpContext, config PluginConfig, prompt string, rawRequest Request, log wrapper.Log) types.Action {
log.Debugf("[onHttpRequestBody] firstreq:%s", prompt)
var userMessage Message
@@ -62,13 +63,17 @@ func firstReq(config PluginConfig, prompt string, rawRequest Request, log wrappe
newMessages := []Message{userMessage}
rawRequest.Messages = newMessages
if rawRequest.Stream {
ctx.SetContext(StreamContextKey, struct{}{})
rawRequest.Stream = false
}
//replace old message and resume request qwen
newbody, err := json.Marshal(rawRequest)
if err != nil {
return types.ActionContinue
} else {
log.Debugf("[onHttpRequestBody] newRequestBody: ", string(newbody))
log.Debugf("[onHttpRequestBody] newRequestBody: %s", string(newbody))
err := proxywasm.ReplaceHttpRequestBody(newbody)
if err != nil {
log.Debug("替换失败")
@@ -87,18 +92,26 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte
var rawRequest Request
err := json.Unmarshal(body, &rawRequest)
if err != nil {
log.Debugf("[onHttpRequestBody] body json umarshal err: ", err.Error())
log.Debugf("[onHttpRequestBody] body json umarshal err: %s", err.Error())
return types.ActionContinue
}
log.Debugf("onHttpRequestBody rawRequest: %v", rawRequest)
//获取用户query
var query string
var history string
messageLength := len(rawRequest.Messages)
log.Debugf("[onHttpRequestBody] messageLength: %s\n", messageLength)
log.Debugf("[onHttpRequestBody] messageLength: %s", messageLength)
if messageLength > 0 {
query = rawRequest.Messages[messageLength-1].Content
log.Debugf("[onHttpRequestBody] query: %s\n", query)
log.Debugf("[onHttpRequestBody] query: %s", query)
if messageLength >= 3 {
for i := 0; i < messageLength-1; i += 2 {
history += "human: " + rawRequest.Messages[i].Content + "\nAI: " + rawRequest.Messages[i+1].Content
}
} else {
history = ""
}
} else {
return types.ActionContinue
}
@@ -111,8 +124,8 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte
//拼装agent prompt模板
tool_desc := make([]string, 0)
tool_names := make([]string, 0)
for _, apiParam := range config.APIParam {
for _, tool_param := range apiParam.Tool_Param {
for _, apisParam := range config.APIsParam {
for _, tool_param := range apisParam.ToolsParam {
tool_desc = append(tool_desc, fmt.Sprintf(prompttpl.TOOL_DESC, tool_param.ToolName, tool_param.Description, tool_param.Description, tool_param.Description, tool_param.Parameter), "\n")
tool_names = append(tool_names, tool_param.ToolName)
}
@@ -122,26 +135,22 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte
if config.PromptTemplate.Language == "CH" {
prompt = fmt.Sprintf(prompttpl.CH_Template,
tool_desc,
tool_names,
config.PromptTemplate.CHTemplate.Question,
config.PromptTemplate.CHTemplate.Thought1,
tool_names,
config.PromptTemplate.CHTemplate.ActionInput,
config.PromptTemplate.CHTemplate.Observation,
config.PromptTemplate.CHTemplate.Thought2,
config.PromptTemplate.CHTemplate.FinalAnswer,
config.PromptTemplate.CHTemplate.Begin,
history,
query)
} else {
prompt = fmt.Sprintf(prompttpl.EN_Template,
tool_desc,
tool_names,
config.PromptTemplate.ENTemplate.Question,
config.PromptTemplate.ENTemplate.Thought1,
tool_names,
config.PromptTemplate.ENTemplate.ActionInput,
config.PromptTemplate.ENTemplate.Observation,
config.PromptTemplate.ENTemplate.Thought2,
config.PromptTemplate.ENTemplate.FinalAnswer,
config.PromptTemplate.ENTemplate.Begin,
history,
query)
}
@@ -154,7 +163,7 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte
dashscope.MessageStore.AddForUser(prompt)
//开始第一次请求
ret := firstReq(config, prompt, rawRequest, log)
ret := firstReq(ctx, config, prompt, rawRequest, log)
return ret
}
@@ -168,7 +177,7 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig, log wra
func toolsCallResult(ctx wrapper.HttpContext, config PluginConfig, content string, rawResponse Response, log wrapper.Log, statusCode int, responseBody []byte) {
if statusCode != http.StatusOK {
log.Debugf("statusCode: %d\n", statusCode)
log.Debugf("statusCode: %d", statusCode)
}
log.Info("========函数返回结果========")
log.Infof(string(responseBody))
@@ -193,30 +202,36 @@ func toolsCallResult(ctx wrapper.HttpContext, config PluginConfig, content strin
//得到gpt的返回结果
var responseCompletion dashscope.CompletionResponse
_ = json.Unmarshal(responseBody, &responseCompletion)
log.Infof("[toolsCall] content: %s\n", responseCompletion.Choices[0].Message.Content)
log.Infof("[toolsCall] content: %s", responseCompletion.Choices[0].Message.Content)
if responseCompletion.Choices[0].Message.Content != "" {
retType := toolsCall(ctx, config, responseCompletion.Choices[0].Message.Content, rawResponse, log)
retType, actionInput := toolsCall(ctx, config, responseCompletion.Choices[0].Message.Content, rawResponse, log)
if retType == types.ActionContinue {
//得到了Final Answer
var assistantMessage Message
assistantMessage.Role = "assistant"
startIndex := strings.Index(responseCompletion.Choices[0].Message.Content, "Final Answer:")
if startIndex != -1 {
startIndex += len("Final Answer:") // 移动到"Final Answer:"之后的位置
extractedText := responseCompletion.Choices[0].Message.Content[startIndex:]
assistantMessage.Content = extractedText
}
if ctx.GetContext(StreamContextKey) == nil {
assistantMessage.Role = "assistant"
assistantMessage.Content = actionInput
rawResponse.Choices[0].Message = assistantMessage
newbody, err := json.Marshal(rawResponse)
if err != nil {
proxywasm.ResumeHttpResponse()
return
} else {
proxywasm.ReplaceHttpResponseBody(newbody)
rawResponse.Choices[0].Message = assistantMessage
newbody, err := json.Marshal(rawResponse)
if err != nil {
proxywasm.ResumeHttpResponse()
return
log.Debug("[onHttpResponseBody] response替换成功")
proxywasm.ResumeHttpResponse()
}
} else {
log.Infof("[onHttpResponseBody] newResponseBody: ", string(newbody))
proxywasm.ReplaceHttpResponseBody(newbody)
headers := [][2]string{{"content-type", "text/event-stream; charset=utf-8"}}
proxywasm.ReplaceHttpResponseHeaders(headers)
// Remove quotes from actionInput
actionInput = strings.Trim(actionInput, "\"")
returnStreamResponseTemplate := `data:{"id":"%s","choices":[{"index":0,"delta":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"%s","object":"chat.completion","usage":{"prompt_tokens":%d,"completion_tokens":%d,"total_tokens":%d}}` + "\n\ndata:[DONE]\n\n"
newbody := fmt.Sprintf(returnStreamResponseTemplate, rawResponse.ID, actionInput, rawResponse.Model, rawResponse.Usage.PromptTokens, rawResponse.Usage.CompletionTokens, rawResponse.Usage.TotalTokens)
log.Infof("[onHttpResponseBody] newResponseBody: ", newbody)
proxywasm.ReplaceHttpResponseBody([]byte(newbody))
log.Debug("[onHttpResponseBody] response替换成功")
proxywasm.ResumeHttpResponse()
@@ -232,121 +247,156 @@ func toolsCallResult(ctx wrapper.HttpContext, config PluginConfig, content strin
}
}
func toolsCall(ctx wrapper.HttpContext, config PluginConfig, content string, rawResponse Response, log wrapper.Log) types.Action {
func outputParser(response string, log wrapper.Log) (string, string) {
log.Debugf("Raw response:%s", response)
start := strings.Index(response, "```")
end := strings.LastIndex(response, "```")
var jsonStr string
if start != -1 && end != -1 {
jsonStr = strings.TrimSpace(response[start+3 : end])
} else {
jsonStr = response
}
log.Debugf("Extracted JSON string:%s", jsonStr)
var action map[string]interface{}
err := json.Unmarshal([]byte(jsonStr), &action)
if err == nil {
var actionName, actionInput string
for key, value := range action {
if strings.Contains(strings.ToLower(key), "input") {
actionInput = fmt.Sprintf("%v", value)
} else {
actionName = fmt.Sprintf("%v", value)
}
}
if actionName != "" && actionInput != "" {
return actionName, actionInput
}
}
log.Debugf("json parse err: %s", err.Error())
// Fallback to regex parsing if JSON unmarshaling fails
pattern := `\{\s*"action":\s*"([^"]+)",\s*"action_input":\s*((?:\{[^}]+\})|"[^"]+")\s*\}`
re := regexp.MustCompile(pattern)
match := re.FindStringSubmatch(jsonStr)
if len(match) == 3 {
action := match[1]
actionInput := match[2]
log.Debugf("Parsed action:%s, action_input:%s", action, actionInput)
return action, actionInput
}
log.Debug("No valid action and action_input found in the response")
return "", ""
}
func toolsCall(ctx wrapper.HttpContext, config PluginConfig, content string, rawResponse Response, log wrapper.Log) (types.Action, string) {
dashscope.MessageStore.AddForAssistant(content)
action, actionInput := outputParser(content, log)
//得到最终答案
regexPattern := regexp.MustCompile(FinalAnswerPattern)
finalAnswer := regexPattern.FindStringSubmatch(content)
if len(finalAnswer) > 1 {
return types.ActionContinue
if action == "Final Answer" {
return types.ActionContinue, actionInput
}
count := ctx.GetContext(ToolCallsCount).(int)
count++
log.Debugf("toolCallsCount:%d, config.LLMInfo.MaxIterations=%d\n", count, config.LLMInfo.MaxIterations)
log.Debugf("toolCallsCount:%d, config.LLMInfo.MaxIterations=%d", count, config.LLMInfo.MaxIterations)
//函数递归调用次数,达到了预设的循环次数,强制结束
if int64(count) > config.LLMInfo.MaxIterations {
ctx.SetContext(ToolCallsCount, 0)
return types.ActionContinue
return types.ActionContinue, ""
} else {
ctx.SetContext(ToolCallsCount, count)
}
//没得到最终答案
regexAction := regexp.MustCompile(ActionPattern)
regexActionInput := regexp.MustCompile(ActionInputPattern)
action := regexAction.FindStringSubmatch(content)
actionInput := regexActionInput.FindStringSubmatch(content)
var url string
var headers [][2]string
var apiClient wrapper.HttpClient
var method string
var reqBody []byte
var key string
var maxExecutionTime int64
if len(action) > 1 && len(actionInput) > 1 {
var url string
var headers [][2]string
var apiClient wrapper.HttpClient
var method string
var reqBody []byte
var key string
for i, apisParam := range config.APIsParam {
maxExecutionTime = apisParam.MaxExecutionTime
for _, tools_param := range apisParam.ToolsParam {
if action == tools_param.ToolName {
log.Infof("calls %s", tools_param.ToolName)
log.Infof("actionInput: %s", actionInput)
for i, apiParam := range config.APIParam {
for _, tool_param := range apiParam.Tool_Param {
if action[1] == tool_param.ToolName {
log.Infof("calls %s\n", tool_param.ToolName)
log.Infof("actionInput[1]: %s", actionInput[1])
//将大模型需要的参数反序列化
var data map[string]interface{}
if err := json.Unmarshal([]byte(actionInput[1]), &data); err != nil {
log.Debugf("Error: %s\n", err.Error())
return types.ActionContinue
}
method = tool_param.Method
//key or header组装
if apiParam.APIKey.Name != "" {
if apiParam.APIKey.In == "query" { //query类型的key要放到url中
headers = nil
key = "?" + apiParam.APIKey.Name + "=" + apiParam.APIKey.Value
} else if apiParam.APIKey.In == "header" { //header类型的key放在header中
headers = [][2]string{{"Content-Type", "application/json"}, {"Authorization", apiParam.APIKey.Name + " " + apiParam.APIKey.Value}}
}
}
if method == "GET" {
//query组装
var args string
for i, param := range tool_param.ParamName { //从参数列表中取出参数
if i == 0 && apiParam.APIKey.In != "query" {
args = "?" + param + "=%s"
args = fmt.Sprintf(args, data[param])
} else {
args = args + "&" + param + "=%s"
args = fmt.Sprintf(args, data[param])
}
}
//url组装
url = apiParam.URL + tool_param.Path + key + args
} else if method == "POST" {
reqBody = nil
//json参数组装
jsonData, err := json.Marshal(data)
if err != nil {
log.Debugf("Error: %s\n", err.Error())
return types.ActionContinue
}
reqBody = jsonData
//url组装
url = apiParam.URL + tool_param.Path + key
}
log.Infof("url: %s\n", url)
apiClient = config.APIClient[i]
break
//将大模型需要的参数反序列化
var data map[string]interface{}
if err := json.Unmarshal([]byte(actionInput), &data); err != nil {
log.Debugf("Error: %s", err.Error())
return types.ActionContinue, ""
}
}
}
if apiClient != nil {
err := apiClient.Call(
method,
url,
headers,
reqBody,
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
toolsCallResult(ctx, config, content, rawResponse, log, statusCode, responseBody)
}, 50000)
if err != nil {
log.Debugf("tool calls error: %s\n", err.Error())
proxywasm.ResumeHttpRequest()
method = tools_param.Method
// 组装 headers 和 key
headers = [][2]string{{"Content-Type", "application/json"}}
if apisParam.APIKey.Name != "" {
if apisParam.APIKey.In == "query" {
key = "?" + apisParam.APIKey.Name + "=" + apisParam.APIKey.Value
} else if apisParam.APIKey.In == "header" {
headers = append(headers, [2]string{"Authorization", apisParam.APIKey.Name + " " + apisParam.APIKey.Value})
}
}
// 组装 URL 和请求体
url = apisParam.URL + tools_param.Path + key
if method == "GET" {
queryParams := make([]string, 0, len(tools_param.ParamName))
for _, param := range tools_param.ParamName {
if value, ok := data[param]; ok {
queryParams = append(queryParams, fmt.Sprintf("%s=%v", param, value))
}
}
if len(queryParams) > 0 {
url += "&" + strings.Join(queryParams, "&")
}
} else if method == "POST" {
var err error
reqBody, err = json.Marshal(data)
if err != nil {
log.Debugf("Error marshaling JSON: %s", err.Error())
return types.ActionContinue, ""
}
}
log.Infof("url: %s", url)
apiClient = config.APIClient[i]
break
}
} else {
return types.ActionContinue
}
}
return types.ActionPause
if apiClient != nil {
err := apiClient.Call(
method,
url,
headers,
reqBody,
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
toolsCallResult(ctx, config, content, rawResponse, log, statusCode, responseBody)
}, uint32(maxExecutionTime))
if err != nil {
log.Debugf("tool calls error: %s", err.Error())
proxywasm.ResumeHttpRequest()
}
} else {
return types.ActionContinue, ""
}
return types.ActionPause, ""
}
// 从response接收到firstreq的大模型返回
@@ -361,11 +411,12 @@ func onHttpResponseBody(ctx wrapper.HttpContext, config PluginConfig, body []byt
log.Debugf("[onHttpResponseBody] body to json err: %s", err.Error())
return types.ActionContinue
}
log.Infof("first content: %s\n", rawResponse.Choices[0].Message.Content)
log.Infof("first content: %s", rawResponse.Choices[0].Message.Content)
//如果gpt返回的内容不是空的
if rawResponse.Choices[0].Message.Content != "" {
//进入agent的循环思考工具调用的过程中
return toolsCall(ctx, config, rawResponse.Choices[0].Message.Content, rawResponse, log)
retType, _ := toolsCall(ctx, config, rawResponse.Choices[0].Message.Content, rawResponse, log)
return retType
} else {
return types.ActionContinue
}

View File

@@ -13,81 +13,157 @@ Parameters:
Format the arguments as a JSON object.`
/*
Answer the following questions as best you can, but speaking as a pirate might speak. You have access to the following tools:
Respond to the human as helpfully and accurately as possible. You have access to the following tools:
%s
{{tools_desc}}
Use the following format:
Use a json blob to specify a tool by providing an action key (tool name) and an action_input key (tool input).
Valid "action" values: "Final Answer" or {{tool_names}}
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of %s
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question, please give the most direct answer directly in Chinese, not English, and do not add extra content.
Provide only ONE action per $JSON_BLOB, as shown:
Begin! Remember to speak as a pirate when giving your final answer. Use lots of "Arg"s
```
Question: %s
{
"action": $TOOL_NAME,
"action_input": $ACTION_INPUT
}
```
Follow this format:
Question: input question to answer
Thought: consider previous and subsequent steps
Action:
```
$JSON_BLOB
```
Observation: action result
... (repeat Thought/Action/Observation N times)
Thought: I know what to respond
Action:
```
{
"action": "Final Answer",
"action_input": "Final response to human"
}
```
Begin! Reminder to ALWAYS respond with a valid json blob of a single action. Use tools if necessary. Respond directly if appropriate. Format is Action:```$JSON_BLOB```then Observation:.
{{historic_messages}}
Question: {{query}}
*/
const EN_Template = `
Answer the following questions as best you can, but speaking as a pirate might speak. You have access to the following tools:
Respond to the human as helpfully and accurately as possible.You have access to the following tools:
%s
Use the following format:
Use a json blob to specify a tool by providing an action key (tool name) and an action_input key (tool input).
Valid "action" values: "Final Answer" or %s
Provide only ONE action per $JSON_BLOB, as shown:
` + "```" + `
{
"action": $TOOL_NAME,
"action_input": $ACTION_INPUT
}
` + "```" + `
Follow this format:
Question: %s
Thought: %s
Action: the action to take, should be one of %s
Action Input: %s
Observation: %s
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: %s
Final Answer: %s
Thought: %s
Action: ` + "```" + `$JSON_BLOB` + "```" + `
Observation: %s
... (repeat Thought/Action/Observation N times)
Thought: %s
Action:` + "```" + `
{
"action": "Final Answer",
"action_input": "Final response to human"
}
` + "```" + `
Begin! Reminder to ALWAYS respond with a valid json blob of a single action. Use tools if necessary. Respond directly if appropriate.Format is Action:` + "```" + `$JSON_BLOB` + "```" + `then Observation:.
%s
Question: %s
`
/*
你所能回答以下问题。可以使用以下工具:
可能帮助和准确地回答人的问题。可以使用以下工具:
%s
{tool_descs}
使用以下格式其中Action字段后必须跟着Action Input字段并且不要将Action Input替换成Input或者tool等字段不能出现格式以外的字段名每个字段在每个轮次只出现一次
Question: 你需要回答的输入问题
Thought: 你应该总是思考该做什么
Action: 要采取的动作,动作只能是%s中的一个 ,一定不要加入其它内容
Action Input: 行动的输入必须出现在Action后。
Observation: 行动的结果
...这个Thought/Action/Action Input/Observation可以重复N次
Thought: 我现在知道最终答案
Final Answer: 对原始输入问题的最终答案
使用 json blob通过提供 action key工具名称和 action_input key工具输入来指定工具。
有效的 "action"值为 "Final Answer"或 {tool_names}
再次重申,不要修改以上模板的字段名称,开始吧!
每个 $JSON_BLOB 只能提供一个操作,如图所示:
Question: %s
```
{{
"action": $TOOL_NAME,
"action_input": $ACTION_INPUT
}}
```
按照以下格式:
Question: 输入要回答的问题
Thought: 考虑之前和之后的步骤
Action:
```
$JSON_BLOB
```
Observation: 行动结果
...这个Thought/Action//Observation可以重复N次
Thought: 我知道该回应什么
Action:
```
{{
"action": "Final Answer",
"action_input": "Final response to human"
}}
```
开始!提醒您始终使用单个操作的有效 json blob 进行响应。必要时使用工具。如果合适,可直接响应。格式为 Action:```$JSON_BLOB```then Observation:.
{historic_messages}
Question: {input}
*/
const CH_Template = `
你所能回答以下问题。可以使用以下工具:
可能帮助和准确地回答人的问题。可以使用以下工具:
%s
使用以下格式其中Action字段后必须跟着Action Input字段并且不要将Action Input替换成Input或者tool等字段不能出现格式以外的字段名每个字段在每个轮次只出现一次
使用 json blob通过提供 action key工具名称和 action_input key工具输入来指定工具。
有效的 "action"值为 "Final Answer"或 %s
每个 $JSON_BLOB 只能提供一个操作,如图所示:
` + "```" + `
{
"action": $TOOL_NAME,
"action_input": $ACTION_INPUT
}
` + "```" + `
按照以下格式:
Question: %s
Thought: %s
Action: 要采取的动作,动作只能是%s中的一个 ,一定不要加入其它内容
Action Input: %s
Action: ` + "```" + `$JSON_BLOB` + "```" + `
Observation: %s
...这个Thought/Action/Action Input/Observation可以重复N次
...这个Thought/Action//Observation可以重复N次
Thought: %s
Final Answer: %s
Action:` + "```" + `
{
"action": "Final Answer",
"action_input": "Final response to human"
}
` + "```" + `
开始!提醒您始终使用单个操作的有效 json blob 进行响应。必要时使用工具。如果合适,可直接响应。格式为 Action:` + "```" + `$JSON_BLOB` + "```" + `then Observation:.
%s
Question: %s
`