From 4ca4bec2b5a0c88566eeac2e7301ec81b5af422d Mon Sep 17 00:00:00 2001 From: rinfx <893383980@qq.com> Date: Tue, 18 Jun 2024 17:51:38 +0800 Subject: [PATCH] add plugin: ai-transformer (#1035) --- .../extensions/ai-transformer/.gitignore | 3 + .../extensions/ai-transformer/README.md | 81 ++++++++ .../wasm-go/extensions/ai-transformer/go.mod | 19 ++ .../wasm-go/extensions/ai-transformer/go.sum | 29 +++ .../wasm-go/extensions/ai-transformer/main.go | 176 ++++++++++++++++++ 5 files changed, 308 insertions(+) create mode 100644 plugins/wasm-go/extensions/ai-transformer/.gitignore create mode 100644 plugins/wasm-go/extensions/ai-transformer/README.md create mode 100644 plugins/wasm-go/extensions/ai-transformer/go.mod create mode 100644 plugins/wasm-go/extensions/ai-transformer/go.sum create mode 100644 plugins/wasm-go/extensions/ai-transformer/main.go diff --git a/plugins/wasm-go/extensions/ai-transformer/.gitignore b/plugins/wasm-go/extensions/ai-transformer/.gitignore new file mode 100644 index 000000000..c9f9dc52b --- /dev/null +++ b/plugins/wasm-go/extensions/ai-transformer/.gitignore @@ -0,0 +1,3 @@ +config.yaml +main.wasm +tmp/ \ No newline at end of file diff --git a/plugins/wasm-go/extensions/ai-transformer/README.md b/plugins/wasm-go/extensions/ai-transformer/README.md new file mode 100644 index 000000000..9b098a14f --- /dev/null +++ b/plugins/wasm-go/extensions/ai-transformer/README.md @@ -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/json;2. body由xml转化为json;3. 移除content-length。" +provider: + serviceName: qwen + domain: dashscope.aliyuncs.com + apiKey: xxxxxxxxxxxxx +``` + +访问原始的httbin的/xml接口,结果为: +``` + + + + + + + + + Wake up to WonderWidgets! + + + + + Overview + Why WonderWidgets are great + + Who buys WonderWidgets + + + +``` + +使用以上配置,通过网关访问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 WonderWidgets are great", + "", + "Who buys WonderWidgets" + ] + } + ] + } +} +``` diff --git a/plugins/wasm-go/extensions/ai-transformer/go.mod b/plugins/wasm-go/extensions/ai-transformer/go.mod new file mode 100644 index 000000000..2d1410fa7 --- /dev/null +++ b/plugins/wasm-go/extensions/ai-transformer/go.mod @@ -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 +) diff --git a/plugins/wasm-go/extensions/ai-transformer/go.sum b/plugins/wasm-go/extensions/ai-transformer/go.sum new file mode 100644 index 000000000..494ae4115 --- /dev/null +++ b/plugins/wasm-go/extensions/ai-transformer/go.sum @@ -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= diff --git a/plugins/wasm-go/extensions/ai-transformer/main.go b/plugins/wasm-go/extensions/ai-transformer/main.go new file mode 100644 index 000000000..dce13933f --- /dev/null +++ b/plugins/wasm-go/extensions/ai-transformer/main.go @@ -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 +}