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
+}