add plugin: ai-transformer (#1035)

This commit is contained in:
rinfx
2024-06-18 17:51:38 +08:00
committed by GitHub
parent 174350d3fb
commit 4ca4bec2b5
5 changed files with 308 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
config.yaml
main.wasm
tmp/

View File

@@ -0,0 +1,81 @@
# 简介
低代码开发插件通过LLM对请求/响应的header以及body进行修改。
# 配置说明
| Name | Type | Requirement | Default | Description |
| :- | :- | :- | :- | :- |
| request.enable | bool | requried | - | 是否在request阶段开启转换 |
| request.prompt | string | requried | - | request阶段转换使用的prompt |
| response.enable | string | requried | - | 是否在response阶段开启转换 |
| response.prompt | string | requried | - | response阶段转换使用的prompt |
| provider.serviceName | string | requried | - | DNS类型的服务名目前仅支持通义千问 |
| provider.domain | string | requried | - | LLM服务域名 |
| provider.apiKey | string | requried | - | 阿里云dashscope服务的API Key |
# 配置示例
```yaml
request:
enable: false
prompt: "如果请求path是以/httpbin开头的帮我去掉/httpbin前缀其他的不要改。"
response:
enable: true
prompt: "帮我修改以下HTTP应答信息要求1. content-type修改为application/json2. body由xml转化为json3. 移除content-length。"
provider:
serviceName: qwen
domain: dashscope.aliyuncs.com
apiKey: xxxxxxxxxxxxx
```
访问原始的httbin的/xml接口结果为
```
<?xml version='1.0' encoding='us-ascii'?>
<!-- A SAMPLE set of slides -->
<slideshow
title="Sample Slide Show"
date="Date of publication"
author="Yours Truly"
>
<!-- TITLE SLIDE -->
<slide type="all">
<title>Wake up to WonderWidgets!</title>
</slide>
<!-- OVERVIEW -->
<slide type="all">
<title>Overview</title>
<item>Why <em>WonderWidgets</em> are great</item>
<item/>
<item>Who <em>buys</em> WonderWidgets</item>
</slide>
</slideshow>
```
使用以上配置通过网关访问httpbin的/xml接口结果为
```
{
"slideshow": {
"title": "Sample Slide Show",
"date": "Date of publication",
"author": "Yours Truly",
"slides": [
{
"type": "all",
"title": "Wake up to WonderWidgets!"
},
{
"type": "all",
"title": "Overview",
"items": [
"Why <em>WonderWidgets</em> are great",
"",
"Who <em>buys</em> WonderWidgets"
]
}
]
}
}
```

View File

@@ -0,0 +1,19 @@
module ai-transformer
go 1.18
require (
github.com/alibaba/higress/plugins/wasm-go v1.4.0
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc
github.com/tidwall/gjson v1.14.3
)
require (
github.com/google/uuid v1.3.0 // indirect
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
github.com/magefile/mage v1.14.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/resp v0.1.1 // indirect
github.com/tidwall/sjson v1.2.5
)

View File

@@ -0,0 +1,29 @@
github.com/alibaba/higress/plugins/wasm-go v1.3.5 h1:VOLL3m442IHCSu8mR5AZ4sc6LVT9X0w1hdqDI7oB9jY=
github.com/alibaba/higress/plugins/wasm-go v1.3.5/go.mod h1:kr3V9Ntbspj1eSrX8rgjBsdMXkGupYEf+LM72caGPQc=
github.com/alibaba/higress/plugins/wasm-go v1.4.0 h1:uFf+mbZ2iuRXJzRbmWBuxiHvNDMGf3PCBJ6TI86bopY=
github.com/alibaba/higress/plugins/wasm-go v1.4.0/go.mod h1:10jQXKsYFUF7djs+Oy7t82f4dbie9pISfP9FJwpPLuk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a h1:luYRvxLTE1xYxrXYj7nmjd1U0HHh8pUPiKfdZ0MhCGE=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc h1:t2AT8zb6N/59Y78lyRWedVoVWHNRSCBh0oWCC+bluTQ=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -0,0 +1,176 @@
package main
import (
"errors"
"net/http"
"strings"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
func main() {
wrapper.SetCtx(
"ai-transformer",
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
wrapper.ProcessResponseBodyBy(onHttpResponseBody),
)
}
type AITransformerConfig struct {
client wrapper.HttpClient
requestTransformEnable bool
requestTransformPrompt string
responseTransformEnable bool
responseTransformPrompt string
providerAPIKey string
}
const llmRequestTemplate = `{
"model": "qwen-max",
"input":{
"messages":[
{
"role": "system",
"content": "假设你是一个http 1.1协议专家你的回答应该只包含http报文除此之外不要有任何其他内容。"
},
{
"role": "system",
"content": ""
},
{
"role": "user",
"content": ""
}
]
}
}`
func parseConfig(json gjson.Result, config *AITransformerConfig, log wrapper.Log) error {
config.requestTransformEnable = json.Get("request.enable").Bool()
config.requestTransformPrompt = json.Get("request.prompt").String()
config.responseTransformEnable = json.Get("response.enable").Bool()
config.responseTransformPrompt = json.Get("response.prompt").String()
config.providerAPIKey = json.Get("provider.apiKey").String()
config.client = wrapper.NewClusterClient(wrapper.DnsCluster{
ServiceName: json.Get("provider.serviceName").String(),
Port: 443,
Domain: json.Get("provider.domain").String(),
})
return nil
}
func getSplitPos(header string) int {
for i, ch := range header {
if ch == ':' && i != 0 {
return i
}
}
return -1
}
func extraceHttpFrame(frame string) ([][2]string, []byte, error) {
pos := strings.Index(frame, "\n\n")
headers := [][2]string{}
for _, header := range strings.Split(frame[:pos], "\n") {
splitPos := getSplitPos(header)
if splitPos == -1 {
return nil, nil, errors.New("invalid http frame.")
}
headers = append(headers, [2]string{header[:splitPos], header[splitPos+1:]})
}
body := []byte(frame[pos+2:])
return headers, body, nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config AITransformerConfig, log wrapper.Log) types.Action {
log.Info("onHttpRequestHeaders")
if !config.requestTransformEnable || config.requestTransformPrompt == "" {
ctx.DontReadRequestBody()
return types.ActionContinue
} else {
return types.HeaderStopIteration
}
}
func onHttpRequestBody(ctx wrapper.HttpContext, config AITransformerConfig, body []byte, log wrapper.Log) types.Action {
log.Info("onHttpRequestBody")
headers, err := proxywasm.GetHttpRequestHeaders()
if err != nil {
log.Error("Failed to get http response headers.")
return types.ActionContinue
}
headerStr := ""
for _, hd := range headers {
headerStr += hd[0] + ":" + hd[1] + "\n"
}
var llmRequestBody string
llmRequestBody, _ = sjson.Set(llmRequestTemplate, "input.messages.1.content", config.requestTransformPrompt)
llmRequestBody, _ = sjson.Set(llmRequestBody, "input.messages.2.content", headerStr+"\n"+string(body))
hds := [][2]string{{"Authorization", "Bearer " + config.providerAPIKey}, {"Content-Type", "application/json"}}
log.Info(headerStr + "\n" + string(body))
config.client.Post(
"/api/v1/services/aigc/text-generation/generation",
hds,
[]byte(llmRequestBody),
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
newHeaders, newBody, err := extraceHttpFrame(gjson.GetBytes(responseBody, "output.text").String())
if err == nil {
proxywasm.ReplaceHttpRequestHeaders(newHeaders)
proxywasm.ReplaceHttpRequestBody(newBody)
}
proxywasm.ResumeHttpRequest()
},
50000,
)
return types.ActionPause
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config AITransformerConfig, log wrapper.Log) types.Action {
if !config.responseTransformEnable || config.responseTransformPrompt == "" {
ctx.DontReadResponseBody()
return types.ActionContinue
} else {
return types.HeaderStopIteration
}
}
func onHttpResponseBody(ctx wrapper.HttpContext, config AITransformerConfig, body []byte, log wrapper.Log) types.Action {
headers, err := proxywasm.GetHttpResponseHeaders()
if err != nil {
log.Error("Failed to get http response headers.")
return types.ActionContinue
}
headerStr := ""
for _, hd := range headers {
headerStr += hd[0] + ":" + hd[1] + "\n"
}
var llmRequestBody string
llmRequestBody, _ = sjson.Set(llmRequestTemplate, "input.messages.1.content", config.responseTransformPrompt)
llmRequestBody, _ = sjson.Set(llmRequestBody, "input.messages.2.content", headerStr+"\n"+string(body))
hds := [][2]string{{"Authorization", "Bearer " + config.providerAPIKey}, {"Content-Type", "application/json"}}
log.Info(headerStr + "\n" + string(body))
config.client.Post(
"/api/v1/services/aigc/text-generation/generation",
hds,
[]byte(llmRequestBody),
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
newHeaders, newBody, err := extraceHttpFrame(gjson.GetBytes(responseBody, "output.text").String())
if err == nil {
proxywasm.ReplaceHttpResponseHeaders(newHeaders)
proxywasm.ReplaceHttpResponseBody(newBody)
}
proxywasm.ResumeHttpResponse()
},
50000,
)
return types.ActionPause
}