mirror of
https://github.com/alibaba/higress.git
synced 2026-03-07 10:00:48 +08:00
add plugin: ai-transformer (#1035)
This commit is contained in:
3
plugins/wasm-go/extensions/ai-transformer/.gitignore
vendored
Normal file
3
plugins/wasm-go/extensions/ai-transformer/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
config.yaml
|
||||
main.wasm
|
||||
tmp/
|
||||
81
plugins/wasm-go/extensions/ai-transformer/README.md
Normal file
81
plugins/wasm-go/extensions/ai-transformer/README.md
Normal 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/json;2. body由xml转化为json;3. 移除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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
19
plugins/wasm-go/extensions/ai-transformer/go.mod
Normal file
19
plugins/wasm-go/extensions/ai-transformer/go.mod
Normal 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
|
||||
)
|
||||
29
plugins/wasm-go/extensions/ai-transformer/go.sum
Normal file
29
plugins/wasm-go/extensions/ai-transformer/go.sum
Normal 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=
|
||||
176
plugins/wasm-go/extensions/ai-transformer/main.go
Normal file
176
plugins/wasm-go/extensions/ai-transformer/main.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user