mirror of
https://github.com/alibaba/higress.git
synced 2026-03-07 01:50:51 +08:00
1836 lines
48 KiB
Go
1836 lines
48 KiB
Go
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"testing"
|
||
|
||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||
"github.com/higress-group/wasm-go/pkg/test"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
// 测试配置:完整配置
|
||
var completeConfig = func() json.RawMessage {
|
||
data, _ := json.Marshal(map[string]interface{}{
|
||
"returnResponseTemplate": `{"id":"error","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`,
|
||
"llm": map[string]interface{}{
|
||
"apiKey": "test-api-key",
|
||
"serviceName": "llm-service",
|
||
"servicePort": 8080,
|
||
"domain": "llm.example.com",
|
||
"path": "/v1/chat/completions",
|
||
"model": "qwen-turbo",
|
||
"maxIterations": 20,
|
||
"maxExecutionTime": 60000,
|
||
"maxTokens": 2000,
|
||
},
|
||
"apis": []map[string]interface{}{
|
||
{
|
||
"apiProvider": map[string]interface{}{
|
||
"serviceName": "api-service",
|
||
"servicePort": 9090,
|
||
"domain": "api.example.com",
|
||
"maxExecutionTime": 30000,
|
||
"apiKey": map[string]interface{}{
|
||
"in": "header",
|
||
"name": "Authorization",
|
||
"value": "Bearer test-token",
|
||
},
|
||
},
|
||
"api": `openapi: 3.0.0
|
||
info:
|
||
title: Test API
|
||
version: 1.0.0
|
||
servers:
|
||
- url: https://api.example.com
|
||
paths:
|
||
/weather:
|
||
get:
|
||
operationId: getWeather
|
||
summary: Get weather information
|
||
description: Retrieve current weather data
|
||
parameters:
|
||
- name: city
|
||
in: query
|
||
required: true
|
||
schema:
|
||
type: string
|
||
- name: date
|
||
in: query
|
||
required: false
|
||
schema:
|
||
type: string
|
||
/translate:
|
||
post:
|
||
operationId: translateText
|
||
summary: Translate text
|
||
description: Translate text to target language
|
||
requestBody:
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required:
|
||
- text
|
||
- targetLang
|
||
properties:
|
||
text:
|
||
type: string
|
||
sourceLang:
|
||
type: string
|
||
targetLang:
|
||
type: string`,
|
||
},
|
||
},
|
||
"promptTemplate": map[string]interface{}{
|
||
"language": "EN",
|
||
"enTemplate": map[string]interface{}{
|
||
"question": "What is your question?",
|
||
"thought1": "Let me think about this",
|
||
"observation": "Based on the observation",
|
||
"thought2": "Now I understand",
|
||
},
|
||
"chTemplate": map[string]interface{}{
|
||
"question": "你的问题是什么?",
|
||
"thought1": "让我思考一下",
|
||
"observation": "基于观察结果",
|
||
"thought2": "现在我明白了",
|
||
},
|
||
},
|
||
"jsonResp": map[string]interface{}{
|
||
"enable": true,
|
||
"jsonSchema": map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{
|
||
"answer": map[string]interface{}{
|
||
"type": "string",
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
return data
|
||
}()
|
||
|
||
// 测试配置:最小配置(使用默认值)
|
||
var minimalConfig = func() json.RawMessage {
|
||
data, _ := json.Marshal(map[string]interface{}{
|
||
"llm": map[string]interface{}{
|
||
"apiKey": "test-api-key",
|
||
"serviceName": "llm-service",
|
||
"servicePort": 8080,
|
||
"domain": "llm.example.com",
|
||
"path": "/v1/chat/completions",
|
||
"model": "qwen-turbo",
|
||
},
|
||
"apis": []map[string]interface{}{
|
||
{
|
||
"apiProvider": map[string]interface{}{
|
||
"serviceName": "api-service",
|
||
"servicePort": 9090,
|
||
"domain": "api.example.com",
|
||
"apiKey": map[string]interface{}{
|
||
"in": "query",
|
||
"name": "api_key",
|
||
"value": "test-token",
|
||
},
|
||
},
|
||
"api": `openapi: 3.0.0
|
||
info:
|
||
title: Test API
|
||
version: 1.0.0
|
||
servers:
|
||
- url: https://api.example.com
|
||
paths:
|
||
/simple:
|
||
get:
|
||
operationId: simpleGet
|
||
summary: Simple GET endpoint
|
||
parameters:
|
||
- name: id
|
||
in: query
|
||
required: true
|
||
schema:
|
||
type: string`,
|
||
},
|
||
},
|
||
})
|
||
return data
|
||
}()
|
||
|
||
// 测试配置:中文提示模板
|
||
var chinesePromptConfig = func() json.RawMessage {
|
||
data, _ := json.Marshal(map[string]interface{}{
|
||
"llm": map[string]interface{}{
|
||
"apiKey": "test-api-key",
|
||
"serviceName": "llm-service",
|
||
"servicePort": 8080,
|
||
"domain": "llm.example.com",
|
||
"path": "/v1/chat/completions",
|
||
"model": "qwen-turbo",
|
||
},
|
||
"apis": []map[string]interface{}{
|
||
{
|
||
"apiProvider": map[string]interface{}{
|
||
"serviceName": "api-service",
|
||
"servicePort": 9090,
|
||
"domain": "api.example.com",
|
||
"apiKey": map[string]interface{}{
|
||
"in": "header",
|
||
"name": "X-API-Key",
|
||
"value": "test-token",
|
||
},
|
||
},
|
||
"api": `openapi: 3.0.0
|
||
info:
|
||
title: Test API
|
||
version: 1.0.0
|
||
servers:
|
||
- url: https://api.example.com
|
||
paths:
|
||
/test:
|
||
post:
|
||
operationId: testPost
|
||
summary: Test POST endpoint
|
||
requestBody:
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required:
|
||
- data
|
||
properties:
|
||
data:
|
||
type: string`,
|
||
},
|
||
},
|
||
"promptTemplate": map[string]interface{}{
|
||
"language": "CH",
|
||
},
|
||
})
|
||
return data
|
||
}()
|
||
|
||
// 测试配置:缺少必需字段
|
||
var missingRequiredFieldsConfig = func() json.RawMessage {
|
||
data, _ := json.Marshal(map[string]interface{}{
|
||
"llm": map[string]interface{}{
|
||
"apiKey": "test-api-key",
|
||
// 缺少 serviceName, servicePort, domain, path, model
|
||
},
|
||
// 缺少 apis
|
||
})
|
||
return data
|
||
}()
|
||
|
||
// 测试配置:空APIs数组
|
||
var emptyAPIsConfig = func() json.RawMessage {
|
||
data, _ := json.Marshal(map[string]interface{}{
|
||
"llm": map[string]interface{}{
|
||
"apiKey": "test-api-key",
|
||
"serviceName": "llm-service",
|
||
"servicePort": 8080,
|
||
"domain": "llm.example.com",
|
||
"path": "/v1/chat/completions",
|
||
"model": "qwen-turbo",
|
||
},
|
||
"apis": []map[string]interface{}{},
|
||
})
|
||
return data
|
||
}()
|
||
|
||
// 测试配置:缺少API提供者信息
|
||
var missingAPIProviderConfig = func() json.RawMessage {
|
||
data, _ := json.Marshal(map[string]interface{}{
|
||
"llm": map[string]interface{}{
|
||
"apiKey": "test-api-key",
|
||
"serviceName": "llm-service",
|
||
"servicePort": 8080,
|
||
"domain": "llm.example.com",
|
||
"path": "/v1/chat/completions",
|
||
"model": "qwen-turbo",
|
||
},
|
||
"apis": []map[string]interface{}{
|
||
{
|
||
"api": `openapi: 3.0.0
|
||
info:
|
||
title: Test API
|
||
version: 1.0.0
|
||
servers:
|
||
- url: https://api.example.com
|
||
paths:
|
||
/test:
|
||
get:
|
||
operationId: testGet
|
||
summary: Test endpoint`,
|
||
},
|
||
},
|
||
})
|
||
return data
|
||
}()
|
||
|
||
// 测试配置:用于HTTP请求测试的简化配置
|
||
var httpTestConfig = func() json.RawMessage {
|
||
data, _ := json.Marshal(map[string]interface{}{
|
||
"returnResponseTemplate": `{"id":"error","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`,
|
||
"llm": map[string]interface{}{
|
||
"apiKey": "test-api-key",
|
||
"serviceName": "llm-service",
|
||
"servicePort": 8080,
|
||
"domain": "llm.example.com",
|
||
"path": "/v1/chat/completions",
|
||
"model": "qwen-turbo",
|
||
},
|
||
"apis": []map[string]interface{}{
|
||
{
|
||
"apiProvider": map[string]interface{}{
|
||
"serviceName": "api-service",
|
||
"servicePort": 9090,
|
||
"domain": "api.example.com",
|
||
"apiKey": map[string]interface{}{
|
||
"in": "header",
|
||
"name": "Authorization",
|
||
"value": "Bearer test-token",
|
||
},
|
||
},
|
||
"api": `openapi: 3.0.0
|
||
info:
|
||
title: Test API
|
||
version: 1.0.0
|
||
servers:
|
||
- url: https://api.example.com
|
||
paths:
|
||
/weather:
|
||
get:
|
||
operationId: getWeather
|
||
summary: Get weather information
|
||
parameters:
|
||
- name: city
|
||
in: query
|
||
required: true
|
||
schema:
|
||
type: string`,
|
||
},
|
||
},
|
||
"promptTemplate": map[string]interface{}{
|
||
"language": "EN",
|
||
},
|
||
})
|
||
return data
|
||
}()
|
||
|
||
func TestParseConfig(t *testing.T) {
|
||
test.RunGoTest(t, func(t *testing.T) {
|
||
|
||
// 测试最小配置解析(使用默认值)
|
||
t.Run("minimal config with defaults", func(t *testing.T) {
|
||
host, status := test.NewTestHost(minimalConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
configRaw, err := host.GetMatchConfig()
|
||
require.NoError(t, err)
|
||
require.NotNil(t, configRaw)
|
||
|
||
config, ok := configRaw.(*PluginConfig)
|
||
require.True(t, ok, "config should be of type *PluginConfig")
|
||
|
||
// 验证默认响应模板
|
||
require.Contains(t, config.ReturnResponseTemplate, "gpt-4o")
|
||
|
||
// 验证LLM默认值
|
||
require.Equal(t, int64(15), config.LLMInfo.MaxIterations)
|
||
require.Equal(t, int64(50000), config.LLMInfo.MaxExecutionTime)
|
||
require.Equal(t, int64(1000), config.LLMInfo.MaxTokens)
|
||
|
||
// 验证API默认值
|
||
require.Equal(t, int64(50000), config.APIsParam[0].MaxExecutionTime)
|
||
|
||
// 验证提示模板默认值
|
||
require.Equal(t, "EN", config.PromptTemplate.Language)
|
||
require.Equal(t, "input question to answer", config.PromptTemplate.ENTemplate.Question)
|
||
require.Equal(t, "consider previous and subsequent steps", config.PromptTemplate.ENTemplate.Thought1)
|
||
require.Equal(t, "action result", config.PromptTemplate.ENTemplate.Observation)
|
||
require.Equal(t, "I know what to respond", config.PromptTemplate.ENTemplate.Thought2)
|
||
|
||
// 验证JSON响应默认值
|
||
require.False(t, config.JsonResp.Enable)
|
||
})
|
||
|
||
// 测试中文提示模板配置
|
||
t.Run("chinese prompt template config", func(t *testing.T) {
|
||
host, status := test.NewTestHost(chinesePromptConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
configRaw, err := host.GetMatchConfig()
|
||
require.NoError(t, err)
|
||
require.NotNil(t, configRaw)
|
||
|
||
config, ok := configRaw.(*PluginConfig)
|
||
require.True(t, ok, "config should be of type *PluginConfig")
|
||
|
||
// 验证中文提示模板
|
||
require.Equal(t, "CH", config.PromptTemplate.Language)
|
||
require.Equal(t, "输入要回答的问题", config.PromptTemplate.CHTemplate.Question)
|
||
require.Equal(t, "考虑之前和之后的步骤", config.PromptTemplate.CHTemplate.Thought1)
|
||
require.Equal(t, "行动结果", config.PromptTemplate.CHTemplate.Observation)
|
||
require.Equal(t, "我知道该回应什么", config.PromptTemplate.CHTemplate.Thought2)
|
||
})
|
||
|
||
// 测试缺少必需字段的配置
|
||
t.Run("missing required fields config", func(t *testing.T) {
|
||
host, status := test.NewTestHost(missingRequiredFieldsConfig)
|
||
defer host.Reset()
|
||
// 由于缺少必需字段(apis),配置应该失败
|
||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||
})
|
||
|
||
// 测试空APIs数组配置
|
||
t.Run("empty APIs config", func(t *testing.T) {
|
||
host, status := test.NewTestHost(emptyAPIsConfig)
|
||
defer host.Reset()
|
||
// 空APIs数组应该导致配置解析失败
|
||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||
})
|
||
|
||
// 测试缺少API提供者信息的配置
|
||
t.Run("missing API provider config", func(t *testing.T) {
|
||
host, status := test.NewTestHost(missingAPIProviderConfig)
|
||
defer host.Reset()
|
||
// 缺少API提供者信息应该导致配置解析失败
|
||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||
})
|
||
})
|
||
}
|
||
|
||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||
test.RunTest(t, func(t *testing.T) {
|
||
t.Run("basic request headers", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// onHttpRequestHeaders应该返回ActionContinue
|
||
require.Equal(t, types.ActionContinue, action)
|
||
})
|
||
})
|
||
}
|
||
|
||
func TestOnHttpRequestBody(t *testing.T) {
|
||
test.RunTest(t, func(t *testing.T) {
|
||
t.Run("valid request body with single message", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 构造有效的请求体
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [
|
||
{
|
||
"role": "user",
|
||
"content": "今天天气怎么样?"
|
||
}
|
||
],
|
||
"stream": false
|
||
}`
|
||
|
||
// 调用请求体处理
|
||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 应该返回ActionContinue,因为需要等待LLM响应
|
||
require.Equal(t, types.ActionContinue, action)
|
||
|
||
// 验证请求体是否被正确修改
|
||
modifiedBody := host.GetRequestBody()
|
||
require.NotNil(t, modifiedBody)
|
||
|
||
// 解析修改后的请求体
|
||
var modifiedRequest Request
|
||
err := json.Unmarshal(modifiedBody, &modifiedRequest)
|
||
require.NoError(t, err)
|
||
|
||
// 验证消息是否被正确设置
|
||
require.Len(t, modifiedRequest.Messages, 1)
|
||
require.Equal(t, "user", modifiedRequest.Messages[0].Role)
|
||
require.Contains(t, modifiedRequest.Messages[0].Content, "今天天气怎么样?")
|
||
|
||
// 验证stream是否被设置为false
|
||
require.False(t, modifiedRequest.Stream)
|
||
})
|
||
|
||
t.Run("request body with conversation history", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 构造包含对话历史的请求体
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [
|
||
{
|
||
"role": "user",
|
||
"content": "你好"
|
||
},
|
||
{
|
||
"role": "assistant",
|
||
"content": "你好!有什么可以帮助你的吗?"
|
||
},
|
||
{
|
||
"role": "user",
|
||
"content": "今天天气怎么样?"
|
||
}
|
||
],
|
||
"stream": false
|
||
}`
|
||
|
||
// 调用请求体处理
|
||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 应该返回ActionContinue
|
||
require.Equal(t, types.ActionContinue, action)
|
||
|
||
// 验证请求体是否被正确修改
|
||
modifiedBody := host.GetRequestBody()
|
||
require.NotNil(t, modifiedBody)
|
||
|
||
// 解析修改后的请求体
|
||
var modifiedRequest Request
|
||
err := json.Unmarshal(modifiedBody, &modifiedRequest)
|
||
require.NoError(t, err)
|
||
|
||
// 验证消息是否被正确设置
|
||
require.Len(t, modifiedRequest.Messages, 1)
|
||
require.Equal(t, "user", modifiedRequest.Messages[0].Role)
|
||
require.Contains(t, modifiedRequest.Messages[0].Content, "今天天气怎么样?")
|
||
})
|
||
|
||
t.Run("stream request body", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 构造流式请求体
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [
|
||
{
|
||
"role": "user",
|
||
"content": "流式响应测试"
|
||
}
|
||
],
|
||
"stream": true
|
||
}`
|
||
|
||
// 调用请求体处理
|
||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 应该返回ActionContinue
|
||
require.Equal(t, types.ActionContinue, action)
|
||
|
||
// 验证请求体是否被正确修改
|
||
modifiedBody := host.GetRequestBody()
|
||
require.NotNil(t, modifiedBody)
|
||
|
||
// 解析修改后的请求体
|
||
var modifiedRequest Request
|
||
err := json.Unmarshal(modifiedBody, &modifiedRequest)
|
||
require.NoError(t, err)
|
||
|
||
// 验证stream是否被设置为false
|
||
require.False(t, modifiedRequest.Stream)
|
||
})
|
||
|
||
t.Run("empty messages array", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 构造空消息数组的请求体
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [],
|
||
"stream": false
|
||
}`
|
||
|
||
// 调用请求体处理
|
||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 应该返回ActionContinue,因为没有消息需要处理
|
||
require.Equal(t, types.ActionContinue, action)
|
||
})
|
||
|
||
t.Run("invalid JSON request body", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 构造无效JSON的请求体
|
||
invalidJSON := []byte(`{"model": "qwen-turbo", "messages": [{"role": "user", "content": "test"}`)
|
||
|
||
// 调用请求体处理
|
||
action := host.CallOnHttpRequestBody(invalidJSON)
|
||
|
||
// 应该返回ActionContinue,因为JSON解析失败
|
||
require.Equal(t, types.ActionContinue, action)
|
||
})
|
||
|
||
t.Run("empty content in message", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 构造空内容的请求体
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [
|
||
{
|
||
"role": "user",
|
||
"content": ""
|
||
}
|
||
],
|
||
"stream": false
|
||
}`
|
||
|
||
// 调用请求体处理
|
||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 应该返回ActionContinue,因为内容为空
|
||
require.Equal(t, types.ActionContinue, action)
|
||
})
|
||
})
|
||
}
|
||
|
||
func TestOnHttpResponseBody(t *testing.T) {
|
||
test.RunTest(t, func(t *testing.T) {
|
||
t.Run("valid LLM response with content", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 先调用请求体处理来初始化上下文
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [
|
||
{
|
||
"role": "user",
|
||
"content": "今天天气怎么样?"
|
||
}
|
||
],
|
||
"stream": false
|
||
}`
|
||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 构造有效的LLM响应体
|
||
responseBody := `{
|
||
"id": "chatcmpl-123",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "{\"action\": \"getWeather\", \"action_input\": \"{\\\"city\\\": \\\"北京\\\"}\"}"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652288,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 10,
|
||
"completion_tokens": 20,
|
||
"total_tokens": 30
|
||
}
|
||
}`
|
||
|
||
// 调用响应体处理
|
||
action := host.CallOnHttpResponseBody([]byte(responseBody))
|
||
|
||
// 应该返回ActionPause,因为需要等待工具调用结果
|
||
require.Equal(t, types.ActionPause, action)
|
||
|
||
// 模拟API工具调用的响应
|
||
apiResponse := `{"temperature": 25, "condition": "晴朗", "humidity": 60}`
|
||
host.CallOnHttpCall([][2]string{
|
||
{"Content-Type", "application/json"},
|
||
{":status", "200"},
|
||
}, []byte(apiResponse))
|
||
|
||
// 模拟LLM对工具调用结果的响应(Final Answer)
|
||
llmFinalResponse := `{
|
||
"id": "chatcmpl-124",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "Final Answer: 今天北京天气晴朗,温度25度,湿度60%"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652289,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 15,
|
||
"completion_tokens": 25,
|
||
"total_tokens": 40
|
||
}
|
||
}`
|
||
|
||
// 模拟LLM客户端的响应
|
||
host.CallOnHttpCall([][2]string{
|
||
{"Content-Type", "application/json"},
|
||
{":status", "200"},
|
||
}, []byte(llmFinalResponse))
|
||
|
||
// 完成HTTP请求
|
||
host.CompleteHttp()
|
||
})
|
||
|
||
t.Run("LLM response with Final Answer", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 先调用请求体处理来初始化上下文
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [
|
||
{
|
||
"role": "user",
|
||
"content": "今天天气怎么样?"
|
||
}
|
||
],
|
||
"stream": false
|
||
}`
|
||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 构造包含Final Answer的LLM响应体
|
||
responseBody := `{
|
||
"id": "chatcmpl-123",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "Final Answer: 今天北京天气晴朗,温度25度"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652288,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 10,
|
||
"completion_tokens": 20,
|
||
"total_tokens": 30
|
||
}
|
||
}`
|
||
|
||
// 调用响应体处理
|
||
action := host.CallOnHttpResponseBody([]byte(responseBody))
|
||
|
||
// 应该返回ActionContinue,因为得到了Final Answer
|
||
require.Equal(t, types.ActionContinue, action)
|
||
})
|
||
|
||
t.Run("LLM response with empty content", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 先调用请求体处理来初始化上下文
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [
|
||
{
|
||
"role": "user",
|
||
"content": "今天天气怎么样?"
|
||
}
|
||
],
|
||
"stream": false
|
||
}`
|
||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 构造空内容的LLM响应体
|
||
responseBody := `{
|
||
"id": "chatcmpl-123",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": ""
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652288,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 10,
|
||
"completion_tokens": 0,
|
||
"total_tokens": 10
|
||
}
|
||
}`
|
||
|
||
// 调用响应体处理
|
||
action := host.CallOnHttpResponseBody([]byte(responseBody))
|
||
|
||
// 应该返回ActionContinue,因为内容为空
|
||
require.Equal(t, types.ActionContinue, action)
|
||
})
|
||
|
||
t.Run("invalid LLM response JSON", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 先调用请求体处理来初始化上下文
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [
|
||
{
|
||
"role": "user",
|
||
"content": "今天天气怎么样?"
|
||
}
|
||
],
|
||
"stream": false
|
||
}`
|
||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 构造无效JSON的响应体
|
||
invalidJSON := []byte(`{"id": "chatcmpl-123", "choices": [{"index": 0, "message": {"role": "assistant", "content": "test"}`)
|
||
|
||
// 调用响应体处理
|
||
action := host.CallOnHttpResponseBody(invalidJSON)
|
||
|
||
// 应该返回ActionContinue,因为JSON解析失败
|
||
require.Equal(t, types.ActionContinue, action)
|
||
})
|
||
|
||
t.Run("complete ReAct loop flow", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 先调用请求体处理来初始化上下文
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [
|
||
{
|
||
"role": "user",
|
||
"content": "查询北京和上海的天气"
|
||
}
|
||
],
|
||
"stream": false
|
||
}`
|
||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 第一次LLM响应,要求调用工具查询北京天气
|
||
llmResponse1 := `{
|
||
"id": "chatcmpl-123",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "{\"action\": \"getWeather\", \"action_input\": \"{\\\"city\\\": \\\"北京\\\"}\"}"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652288,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 10,
|
||
"completion_tokens": 20,
|
||
"total_tokens": 30
|
||
}
|
||
}`
|
||
|
||
// 调用响应体处理,这会触发toolsCall
|
||
action := host.CallOnHttpResponseBody([]byte(llmResponse1))
|
||
|
||
// 应该返回ActionPause,因为需要等待工具调用结果
|
||
require.Equal(t, types.ActionPause, action)
|
||
|
||
// 模拟API工具调用的响应(北京天气)
|
||
apiResponse1 := `{"temperature": 25, "condition": "晴朗", "humidity": 60}`
|
||
host.CallOnHttpCall([][2]string{
|
||
{"Content-Type", "application/json"},
|
||
{":status", "200"},
|
||
}, []byte(apiResponse1))
|
||
|
||
// 第二次LLM响应,要求调用工具查询上海天气
|
||
llmResponse2 := `{
|
||
"id": "chatcmpl-124",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "{\"action\": \"getWeather\", \"action_input\": \"{\\\"city\\\": \\\"上海\\\"}\"}"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652289,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 15,
|
||
"completion_tokens": 25,
|
||
"total_tokens": 40
|
||
}
|
||
}`
|
||
|
||
// 模拟LLM客户端的响应
|
||
host.CallOnHttpCall([][2]string{
|
||
{"Content-Type", "application/json"},
|
||
{":status", "200"},
|
||
}, []byte(llmResponse2))
|
||
|
||
// 模拟API工具调用的响应(上海天气)
|
||
apiResponse2 := `{"temperature": 28, "condition": "多云", "humidity": 70}`
|
||
host.CallOnHttpCall([][2]string{
|
||
{"Content-Type", "application/json"},
|
||
{":status", "200"},
|
||
}, []byte(apiResponse2))
|
||
|
||
// 第三次LLM响应,给出Final Answer
|
||
llmResponse3 := `{
|
||
"id": "chatcmpl-125",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "Final Answer: 北京今天天气晴朗,温度25度,湿度60%;上海今天天气多云,温度28度,湿度70%"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652290,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 20,
|
||
"completion_tokens": 30,
|
||
"total_tokens": 50
|
||
}
|
||
}`
|
||
|
||
// 模拟LLM客户端的响应
|
||
host.CallOnHttpCall([][2]string{
|
||
{"Content-Type", "application/json"},
|
||
{":status", "200"},
|
||
}, []byte(llmResponse3))
|
||
|
||
// 完成HTTP请求
|
||
host.CompleteHttp()
|
||
})
|
||
})
|
||
}
|
||
|
||
func TestFirstReq(t *testing.T) {
|
||
test.RunTest(t, func(t *testing.T) {
|
||
t.Run("successful request body replacement", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 构造原始请求
|
||
originalRequest := Request{
|
||
Model: "qwen-turbo",
|
||
Messages: []Message{
|
||
{
|
||
Role: "user",
|
||
Content: "原始消息",
|
||
},
|
||
},
|
||
Stream: true,
|
||
}
|
||
|
||
// 调用firstReq(通过onHttpRequestBody间接调用)
|
||
requestBody, _ := json.Marshal(originalRequest)
|
||
action := host.CallOnHttpRequestBody(requestBody)
|
||
|
||
// 应该返回ActionContinue
|
||
require.Equal(t, types.ActionContinue, action)
|
||
|
||
// 验证请求体是否被正确修改
|
||
modifiedBody := host.GetRequestBody()
|
||
require.NotNil(t, modifiedBody)
|
||
|
||
// 解析修改后的请求体
|
||
var modifiedRequest Request
|
||
err := json.Unmarshal(modifiedBody, &modifiedRequest)
|
||
require.NoError(t, err)
|
||
|
||
// 验证stream是否被设置为false
|
||
require.False(t, modifiedRequest.Stream)
|
||
|
||
// 验证消息是否被正确设置
|
||
require.Len(t, modifiedRequest.Messages, 1)
|
||
require.Equal(t, "user", modifiedRequest.Messages[0].Role)
|
||
require.Contains(t, modifiedRequest.Messages[0].Content, "原始消息")
|
||
})
|
||
})
|
||
}
|
||
|
||
func TestToolsCall(t *testing.T) {
|
||
test.RunTest(t, func(t *testing.T) {
|
||
t.Run("GET tool call with complete flow", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 先调用请求体处理来初始化上下文
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [
|
||
{
|
||
"role": "user",
|
||
"content": "今天天气怎么样?"
|
||
}
|
||
],
|
||
"stream": false
|
||
}`
|
||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 模拟LLM响应,要求调用GET工具
|
||
llmResponse := `{
|
||
"id": "chatcmpl-123",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "{\"action\": \"getWeather\", \"action_input\": \"{\\\"city\\\": \\\"北京\\\"}\"}"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652288,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 10,
|
||
"completion_tokens": 20,
|
||
"total_tokens": 30
|
||
}
|
||
}`
|
||
|
||
// 调用响应体处理,这会触发toolsCall
|
||
action := host.CallOnHttpResponseBody([]byte(llmResponse))
|
||
|
||
// 应该返回ActionPause,因为需要等待工具调用结果
|
||
require.Equal(t, types.ActionPause, action)
|
||
|
||
// 模拟API工具调用的响应
|
||
apiResponse := `{"temperature": 25, "condition": "晴朗"}`
|
||
host.CallOnHttpCall([][2]string{
|
||
{"Content-Type", "application/json"},
|
||
{":status", "200"},
|
||
}, []byte(apiResponse))
|
||
|
||
// 模拟LLM对工具调用结果的响应(Final Answer)
|
||
llmFinalResponse := `{
|
||
"id": "chatcmpl-124",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "Final Answer: 今天北京天气晴朗,温度25度,湿度60%"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652289,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 15,
|
||
"completion_tokens": 25,
|
||
"total_tokens": 40
|
||
}
|
||
}`
|
||
|
||
// 模拟LLM客户端的响应
|
||
host.CallOnHttpCall([][2]string{
|
||
{"Content-Type", "application/json"},
|
||
{":status", "200"},
|
||
}, []byte(llmFinalResponse))
|
||
|
||
// 完成HTTP请求
|
||
host.CompleteHttp()
|
||
})
|
||
|
||
t.Run("POST tool call with complete flow", func(t *testing.T) {
|
||
// 创建一个支持POST工具的配置
|
||
postToolConfig := func() json.RawMessage {
|
||
data, _ := json.Marshal(map[string]interface{}{
|
||
"returnResponseTemplate": `{"id":"error","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`,
|
||
"llm": map[string]interface{}{
|
||
"apiKey": "test-api-key",
|
||
"serviceName": "llm-service",
|
||
"servicePort": 8080,
|
||
"domain": "llm.example.com",
|
||
"path": "/v1/chat/completions",
|
||
"model": "qwen-turbo",
|
||
},
|
||
"apis": []map[string]interface{}{
|
||
{
|
||
"apiProvider": map[string]interface{}{
|
||
"serviceName": "api-service",
|
||
"servicePort": 9090,
|
||
"domain": "api.example.com",
|
||
"apiKey": map[string]interface{}{
|
||
"in": "header",
|
||
"name": "Authorization",
|
||
"value": "Bearer test-token",
|
||
},
|
||
},
|
||
"api": `openapi: 3.0.0
|
||
info:
|
||
title: Test API
|
||
version: 1.0.0
|
||
servers:
|
||
- url: https://api.example.com
|
||
paths:
|
||
/translate:
|
||
post:
|
||
operationId: translateText
|
||
summary: Translate text
|
||
requestBody:
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required:
|
||
- text
|
||
- targetLang
|
||
properties:
|
||
text:
|
||
type: string
|
||
targetLang:
|
||
type: string`,
|
||
},
|
||
},
|
||
"promptTemplate": map[string]interface{}{
|
||
"language": "EN",
|
||
},
|
||
})
|
||
return data
|
||
}()
|
||
|
||
host, status := test.NewTestHost(postToolConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 先调用请求体处理来初始化上下文
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [
|
||
{
|
||
"role": "user",
|
||
"content": "翻译这段文字"
|
||
}
|
||
],
|
||
"stream": false
|
||
}`
|
||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 模拟LLM响应,要求调用POST工具
|
||
llmResponse := `{
|
||
"id": "chatcmpl-123",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "{\"action\": \"translateText\", \"action_input\": \"{\\\"text\\\": \\\"Hello\\\", \\\"targetLang\\\": \\\"zh\\\"}\"}"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652288,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 10,
|
||
"completion_tokens": 20,
|
||
"total_tokens": 30
|
||
}
|
||
}`
|
||
|
||
// 调用响应体处理,这会触发toolsCall
|
||
action := host.CallOnHttpResponseBody([]byte(llmResponse))
|
||
|
||
// 应该返回ActionPause,因为需要等待工具调用结果
|
||
require.Equal(t, types.ActionPause, action)
|
||
|
||
// 模拟API工具调用的响应
|
||
apiResponse := `{"translatedText": "你好", "sourceLang": "en", "targetLang": "zh"}`
|
||
host.CallOnHttpCall([][2]string{
|
||
{"Content-Type", "application/json"},
|
||
{":status", "200"},
|
||
}, []byte(apiResponse))
|
||
|
||
// 模拟LLM对工具调用结果的响应(Final Answer)
|
||
llmFinalResponse := `{
|
||
"id": "chatcmpl-124",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "Final Answer: Hello翻译成中文是:你好"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652289,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 15,
|
||
"completion_tokens": 25,
|
||
"total_tokens": 40
|
||
}
|
||
}`
|
||
|
||
// 模拟LLM客户端的响应
|
||
host.CallOnHttpCall([][2]string{
|
||
{"Content-Type", "application/json"},
|
||
{":status", "200"},
|
||
}, []byte(llmFinalResponse))
|
||
|
||
// 完成HTTP请求
|
||
host.CompleteHttp()
|
||
})
|
||
|
||
t.Run("Final Answer response", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 先调用请求体处理来初始化上下文
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [
|
||
{
|
||
"role": "user",
|
||
"content": "今天天气怎么样?"
|
||
}
|
||
],
|
||
"stream": false
|
||
}`
|
||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 模拟LLM响应,直接给出Final Answer
|
||
llmResponse := `{
|
||
"id": "chatcmpl-123",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "Final Answer: 今天北京天气晴朗,温度25度"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652288,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 10,
|
||
"completion_tokens": 20,
|
||
"total_tokens": 30
|
||
}
|
||
}`
|
||
|
||
// 调用响应体处理,这会触发toolsCall
|
||
action := host.CallOnHttpResponseBody([]byte(llmResponse))
|
||
|
||
// 应该返回ActionContinue,因为得到了Final Answer
|
||
require.Equal(t, types.ActionContinue, action)
|
||
})
|
||
|
||
t.Run("unknown tool name", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 先调用请求体处理来初始化上下文
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [
|
||
{
|
||
"role": "user",
|
||
"content": "调用一个工具"
|
||
}
|
||
],
|
||
"stream": false
|
||
}`
|
||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 模拟LLM响应,要求调用未知工具
|
||
llmResponse := `{
|
||
"id": "chatcmpl-123",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "{\"action\": \"unknownTool\", \"action_input\": \"{\\\"param\\\": \\\"value\\\"}\"}"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652288,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 10,
|
||
"completion_tokens": 20,
|
||
"total_tokens": 30
|
||
}
|
||
}`
|
||
|
||
// 调用响应体处理,这会触发toolsCall
|
||
action := host.CallOnHttpResponseBody([]byte(llmResponse))
|
||
|
||
// 应该返回ActionContinue,因为工具名称未知
|
||
require.Equal(t, types.ActionContinue, action)
|
||
})
|
||
|
||
t.Run("tool call with max iterations", func(t *testing.T) {
|
||
// 创建一个设置最大迭代次数为2的配置
|
||
maxIterConfig := func() json.RawMessage {
|
||
data, _ := json.Marshal(map[string]interface{}{
|
||
"returnResponseTemplate": `{"id":"error","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`,
|
||
"llm": map[string]interface{}{
|
||
"apiKey": "test-api-key",
|
||
"serviceName": "llm-service",
|
||
"servicePort": 8080,
|
||
"domain": "llm.example.com",
|
||
"path": "/v1/chat/completions",
|
||
"model": "qwen-turbo",
|
||
"maxIterations": 2,
|
||
},
|
||
"apis": []map[string]interface{}{
|
||
{
|
||
"apiProvider": map[string]interface{}{
|
||
"serviceName": "api-service",
|
||
"servicePort": 9090,
|
||
"domain": "api.example.com",
|
||
"apiKey": map[string]interface{}{
|
||
"in": "header",
|
||
"name": "Authorization",
|
||
"value": "Bearer test-token",
|
||
},
|
||
},
|
||
"api": `openapi: 3.0.0
|
||
info:
|
||
title: Test API
|
||
version: 1.0.0
|
||
servers:
|
||
- url: https://api.example.com
|
||
paths:
|
||
/weather:
|
||
get:
|
||
operationId: getWeather
|
||
summary: Get weather information
|
||
parameters:
|
||
- name: city
|
||
in: query
|
||
required: true
|
||
schema:
|
||
type: string`,
|
||
},
|
||
},
|
||
"promptTemplate": map[string]interface{}{
|
||
"language": "EN",
|
||
},
|
||
})
|
||
return data
|
||
}()
|
||
|
||
host, status := test.NewTestHost(maxIterConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 先调用请求体处理来初始化上下文
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [
|
||
{
|
||
"role": "user",
|
||
"content": "今天天气怎么样?"
|
||
}
|
||
],
|
||
"stream": false
|
||
}`
|
||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 第一次LLM响应,要求调用工具
|
||
llmResponse1 := `{
|
||
"id": "chatcmpl-123",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "{\"action\": \"getWeather\", \"action_input\": \"{\\\"city\\\": \\\"北京\\\"}\"}"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652288,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 10,
|
||
"completion_tokens": 20,
|
||
"total_tokens": 30
|
||
}
|
||
}`
|
||
|
||
// 调用响应体处理,这会触发toolsCall
|
||
action := host.CallOnHttpResponseBody([]byte(llmResponse1))
|
||
|
||
// 应该返回ActionPause,因为需要等待工具调用结果
|
||
require.Equal(t, types.ActionPause, action)
|
||
|
||
// 模拟API工具调用的响应
|
||
apiResponse := `{"temperature": 25, "condition": "晴朗"}`
|
||
host.CallOnHttpCall([][2]string{
|
||
{"Content-Type", "application/json"},
|
||
{":status", "200"},
|
||
}, []byte(apiResponse))
|
||
|
||
// 第二次LLM响应,再次要求调用工具
|
||
llmResponse2 := `{
|
||
"id": "chatcmpl-124",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "{\"action\": \"getWeather\", \"action_input\": \"{\\\"city\\\": \\\"上海\\\"}\"}"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652289,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 15,
|
||
"completion_tokens": 25,
|
||
"total_tokens": 40
|
||
}
|
||
}`
|
||
|
||
// 模拟LLM客户端的响应
|
||
host.CallOnHttpCall([][2]string{
|
||
{"Content-Type", "application/json"},
|
||
{":status", "200"},
|
||
}, []byte(llmResponse2))
|
||
|
||
// 第三次LLM响应,应该因为达到最大迭代次数而返回ActionContinue
|
||
llmResponse3 := `{
|
||
"id": "chatcmpl-125",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "{\"action\": \"getWeather\", \"action_input\": \"{\\\"city\\\": \\\"广州\\\"}\"}"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652290,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 20,
|
||
"completion_tokens": 30,
|
||
"total_tokens": 50
|
||
}
|
||
}`
|
||
|
||
// 模拟LLM客户端的响应
|
||
host.CallOnHttpCall([][2]string{
|
||
{"Content-Type", "application/json"},
|
||
{":status", "200"},
|
||
}, []byte(llmResponse3))
|
||
|
||
// 完成HTTP请求
|
||
host.CompleteHttp()
|
||
})
|
||
})
|
||
}
|
||
|
||
func TestEdgeCases(t *testing.T) {
|
||
test.RunTest(t, func(t *testing.T) {
|
||
t.Run("max iterations exceeded", func(t *testing.T) {
|
||
// 创建一个设置最大迭代次数为1的配置
|
||
maxIterConfig := func() json.RawMessage {
|
||
data, _ := json.Marshal(map[string]interface{}{
|
||
"returnResponseTemplate": `{"id":"error","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`,
|
||
"llm": map[string]interface{}{
|
||
"apiKey": "test-api-key",
|
||
"serviceName": "llm-service",
|
||
"servicePort": 8080,
|
||
"domain": "llm.example.com",
|
||
"path": "/v1/chat/completions",
|
||
"model": "qwen-turbo",
|
||
"maxIterations": 1,
|
||
},
|
||
"apis": []map[string]interface{}{
|
||
{
|
||
"apiProvider": map[string]interface{}{
|
||
"serviceName": "api-service",
|
||
"servicePort": 9090,
|
||
"domain": "api.example.com",
|
||
"apiKey": map[string]interface{}{
|
||
"in": "header",
|
||
"name": "Authorization",
|
||
"value": "Bearer test-token",
|
||
},
|
||
},
|
||
"api": `openapi: 3.0.0
|
||
info:
|
||
title: Test API
|
||
version: 1.0.0
|
||
servers:
|
||
- url: https://api.example.com
|
||
paths:
|
||
/weather:
|
||
get:
|
||
operationId: getWeather
|
||
summary: Get weather information
|
||
parameters:
|
||
- name: city
|
||
in: query
|
||
required: true
|
||
schema:
|
||
type: string`,
|
||
},
|
||
},
|
||
"promptTemplate": map[string]interface{}{
|
||
"language": "EN",
|
||
},
|
||
})
|
||
return data
|
||
}()
|
||
|
||
host, status := test.NewTestHost(maxIterConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 先调用请求体处理来初始化上下文
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [
|
||
{
|
||
"role": "user",
|
||
"content": "今天天气怎么样?"
|
||
}
|
||
],
|
||
"stream": false
|
||
}`
|
||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 模拟LLM响应,要求调用工具
|
||
llmResponse := `{
|
||
"id": "chatcmpl-123",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "{\"action\": \"getWeather\", \"action_input\": \"{\\\"city\\\": \\\"北京\\\"}\"}"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652288,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 10,
|
||
"completion_tokens": 20,
|
||
"total_tokens": 30
|
||
}
|
||
}`
|
||
|
||
// 调用响应体处理,这会触发toolsCall
|
||
action := host.CallOnHttpResponseBody([]byte(llmResponse))
|
||
|
||
// 应该返回ActionPause,因为需要等待工具调用结果
|
||
require.Equal(t, types.ActionPause, action)
|
||
|
||
// 模拟API工具调用的响应
|
||
apiResponse := `{"temperature": 25, "condition": "晴朗"}`
|
||
host.CallOnHttpCall([][2]string{
|
||
{"Content-Type", "application/json"},
|
||
{":status", "200"},
|
||
}, []byte(apiResponse))
|
||
|
||
// 模拟LLM对工具调用结果的响应,再次要求调用工具
|
||
llmResponse2 := `{
|
||
"id": "chatcmpl-124",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "{\"action\": \"getWeather\", \"action_input\": \"{\\\"city\\\": \\\"上海\\\"}\"}"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652289,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 15,
|
||
"completion_tokens": 25,
|
||
"total_tokens": 40
|
||
}
|
||
}`
|
||
|
||
// 模拟LLM客户端的响应
|
||
host.CallOnHttpCall([][2]string{
|
||
{"Content-Type", "application/json"},
|
||
{":status", "200"},
|
||
}, []byte(llmResponse2))
|
||
|
||
// 完成HTTP请求
|
||
host.CompleteHttp()
|
||
})
|
||
|
||
t.Run("invalid action input JSON", func(t *testing.T) {
|
||
host, status := test.NewTestHost(httpTestConfig)
|
||
defer host.Reset()
|
||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||
|
||
// 设置请求头
|
||
host.CallOnHttpRequestHeaders([][2]string{
|
||
{":authority", "example.com"},
|
||
{":path", "/api/chat"},
|
||
{":method", "POST"},
|
||
{"content-type", "application/json"},
|
||
})
|
||
|
||
// 先调用请求体处理来初始化上下文
|
||
requestBody := `{
|
||
"model": "qwen-turbo",
|
||
"messages": [
|
||
{
|
||
"role": "user",
|
||
"content": "今天天气怎么样?"
|
||
}
|
||
],
|
||
"stream": false
|
||
}`
|
||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||
|
||
// 模拟LLM响应,包含无效的Action Input JSON
|
||
llmResponse := `{
|
||
"id": "chatcmpl-123",
|
||
"choices": [
|
||
{
|
||
"index": 0,
|
||
"message": {
|
||
"role": "assistant",
|
||
"content": "{\"action\": \"getWeather\", \"action_input\": {invalid json"
|
||
},
|
||
"finish_reason": "stop"
|
||
}
|
||
],
|
||
"created": 1677652288,
|
||
"model": "qwen-turbo",
|
||
"object": "chat.completion",
|
||
"usage": {
|
||
"prompt_tokens": 10,
|
||
"completion_tokens": 20,
|
||
"total_tokens": 30
|
||
}
|
||
}`
|
||
|
||
// 调用响应体处理,这会触发toolsCall
|
||
action := host.CallOnHttpResponseBody([]byte(llmResponse))
|
||
|
||
// 应该返回ActionContinue,因为Action Input JSON无效
|
||
require.Equal(t, types.ActionContinue, action)
|
||
})
|
||
})
|
||
}
|