feat: remove log print (#1)

This commit is contained in:
Dark-qy
2025-04-15 12:59:17 +08:00
committed by GitHub
commit 34bc703a97
38 changed files with 3553 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
1
+21
View File
@@ -0,0 +1,21 @@
The MIT License
Copyright (c) Travis Cline <[email protected]>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+54
View File
@@ -0,0 +1,54 @@
# This file contains convenience targets for the project.
# It is not intended to be used as a build system.
# See the README for more information.
.PHONY: test
test:
go test ./...
.PHONY: lint
lint: lint-deps
golangci-lint run --color=always --sort-results ./...
.PHONY: lint-exp
lint-exp:
golangci-lint run --fix --config .golangci-exp.yaml ./...
.PHONY: lint-fix
lint-fix:
golangci-lint run --fix --skip-dirs=./exp ./...
.PHONY: lint-all
lint-all:
golangci-lint run --color=always --sort-results ./...
.PHONY: lint-deps
lint-deps:
@command -v golangci-lint >/dev/null 2>&1 || { \
echo >&2 "golangci-lint not found. Installing..."; \
go install github.com/golangci/golangci-lint/cmd/[email protected]; \
}
.PHONY: test-race
test-race:
go run test -race ./...
.PHONY: test-cover
test-cover:
go run test -cover ./...
.PHONY: run-pkgsite
run-pkgsite:
go run golang.org/x/pkgsite/cmd/pkgsite@latest
.PHONY: clean
clean: clean-lint-cache
.PHONY: clean-lint-cache
clean-lint-cache:
golangci-lint cache clean
.PHONY: add-go-work
add-go-work:
go work init .
go work use -r .
+342
View File
@@ -0,0 +1,342 @@
### dashscopego
forked from devinyf/dashscopego
阿里云平台 dashscope api 的 golang 封装 (非官方)
[开通DashScope并创建API-KEY](https://help.aliyun.com/zh/dashscope/developer-reference/activate-dashscope-and-create-an-api-key)
#### Examples:
* [通义千问](#通义千问)
* [通义千问VL(视觉理解模型)](#通义千问VL视觉理解模型)
* [通义千问Audio(音频语言模型)](#通义千问Audio音频语言模型)
* [通义万相(图像生成)](#通义万相图像生成)
* [Paraformer(语音识别)](#Paraformer语音识别)
* 模型插件调用 TODO
* langchaingo Agent TODO
开发中...
### 通义千问
```go
import (
"context"
"fmt"
"os"
"github.com/eswulei/dashscope-go"
"github.com/eswulei/dashscope-go/qwen"
)
func main() {
model := qwen.QwenTurbo
token := os.Getenv("DASHSCOPE_API_KEY")
if token == "" {
panic("token is empty")
}
cli := dashscopego.NewTongyiClient(model, token)
content := qwen.TextContent{Text: "讲个冷笑话"}
input := dashscopego.TextInput{
Messages: []dashscopego.TextMessage{
{Role: "user", Content: &content},
},
}
// (可选 SSE开启) 需要流式输出时 通过该 Callback Function 获取结果
streamCallbackFn := func(ctx context.Context, chunk []byte) error {
fmt.Print(string(chunk))
return nil
}
req := &dashscopego.TextRequest{
Input: input,
StreamingFn: streamCallbackFn,
}
ctx := context.TODO()
resp, err := cli.CreateCompletion(ctx, req)
if err != nil {
panic(err)
}
fmt.Println("\nnon-stream result: ")
fmt.Println(resp.Output.Choices[0].Message.Content.ToString())
}
```
### 通义万相(图像生成)
- [x] 文本生成图像
- [ ] 人像风格重绘
- [ ] 图像背景生成
```go
func main() {
model := wanx.WanxV1
token := os.Getenv("DASHSCOPE_API_KEY")
if token == "" {
panic("token is empty")
}
cli := dashscopego.NewTongyiClient(model, token)
req := &wanx.ImageSynthesisRequest{
// Model: "wanx-v1",
Model: model,
Input: wanx.ImageSynthesisInput{
Prompt: "画一只松鼠",
},
Params: wanx.ImageSynthesisParams{
N: 1,
},
Download: true // 从 URL 下载图片
}
ctx := context.TODO()
imgBlobs, err := cli.CreateImageGeneration(ctx, req)
if err != nil {
panic(err)
}
for _, blob := range imgBlobs {
// blob.Data 会在 request 中设置了 Download: true 时下载
// 否则使用 blob.ImgURL
saveImg2Desktop(blob.ImgType, blob.Data)
}
}
func saveImg2Desktop(fileType string, data []byte) {
buf := bytes.NewBuffer(data)
img, _, err := image.Decode(buf)
if err != nil {
log.Fatal(err)
}
usr, err := user.Current()
if err != nil {
panic(err)
}
f, err := os.Create(filepath.Join(usr.HomeDir, "Desktop", "wanx_image.png"))
if err != nil {
panic(err)
}
defer f.Close()
if err := png.Encode(f, img); err != nil {
panic(err)
}
}
```
### 通义千问VL(视觉理解模型)
* Image 也可以直接使用 图片本地路径 或 图片URL链接的, 参照了 dashscope python 库的实现步骤 临时上传到 oss
* 其中上传图片到 oss 的步骤 在开发文档中还没有看到HTTP调用的例子, 所以后续可能会做变更
```go
func main() {
model := qwen.QwenVLPlus
token := os.Getenv("DASHSCOPE_API_KEY")
if token == "" {
panic("token is empty")
}
cli := dashscopego.NewTongyiClient(model, token)
sysContent := qwen.VLContentList{
{
Text: "You are a helpful assistant.",
},
}
userContent := qwen.VLContentList{
{
Text: "用唐诗体描述一下这张图片中的内容",
},
{
// 官方文档的例子, oss 下载
Image: "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg",
// 使用 图片URL链接
// Image: "https://pic.ntimg.cn/20140113/8800276_184351657000_2.jpg",
// 本地图片
// Image: "file:///Users/xxxx/xxxx.png",
},
}
input := dashscopego.VLInput{
Messages: []dashscopego.VLMessage{
{Role: "system", Content: &sysContent},
{Role: "user", Content: &userContent},
},
}
// (可选 SSE开启)需要流式输出时 通过该 Callback Function 获取结果
streamCallbackFn := func(ctx context.Context, chunk []byte) error {
fmt.Print(string(chunk))
return nil
}
req := &dashscopego.VLRequest{
Input: input,
StreamingFn: streamCallbackFn,
}
ctx := context.TODO()
resp, err := cli.CreateVLCompletion(ctx, req)
if err != nil {
panic(err)
}
fmt.Println("\nnon-stream result: ")
fmt.Println(resp.Output.Choices[0].Message.Content.ToString())
}
```
### 通义千问Audio(音频语言模型)
* 同 QwenVL, 如果使用本地音频文件会临时上传 oss, 之后可能会有变动
```go
func main() {
model := qwen.QwenAudioTurbo
token := os.Getenv("DASHSCOPE_API_KEY")
if token == "" {
panic("token is empty")
}
cli := dashscopego.NewTongyiClient(model, token)
sysContent := qwen.AudioContentList{
{
Text: "You are a helpful assistant.",
},
}
userContent := qwen.AudioContentList{
{
Text: "该段对话表达了什么观点? 详细分析该讲话者的语气,展现出什么样的情绪", //nolint:gosmopolitan
},
{
// 使用本地音频文件
// Audio: "file:///Users/xxx/Desktop/hello_world_female2.wav",
// 官方文档中的例子
Audio: "https://dashscope.oss-cn-beijing.aliyuncs.com/audios/2channel_16K.wav",
},
}
input := dashscopego.AudioInput{
Messages: []dashscopego.AudioMessage{
{Role: "system", Content: &sysContent},
{Role: "user", Content: &userContent},
},
}
// callback function: print stream result
streamCallbackFn := func(ctx context.Context, chunk []byte) error {
log.Print(string(chunk))
return nil
}
req := &dashscopego.AudioRequest{
Input: input,
StreamingFn: streamCallbackFn,
}
ctx := context.TODO()
resp, err := cli.CreateAudioCompletion(ctx, req)
if err != nil {
panic(err)
}
log.Println("\nnon-stream result: ")
log.Println(resp.Output.Choices[0].Message.Content.ToString())
}
```
### Paraformer(语音识别)
- [x] 实时语音识别API
- [ ] 录音文件识别API
Experimental:
* 开发文档中 还没有看到 HTTP调用说明, 参照 dashscope python 库中的步骤实现, 将来可能会有变更
* 参数中的: SampleRate 好像目前仅支持 16000, 使用真实录音要留意录音设备的 sample_rate 是与之否匹配
```go
package main
import (
"bufio"
"context"
"fmt"
"os"
"os/user"
"path/filepath"
"time"
"github.com/eswulei/dashscope-go"
"github.com/eswulei/dashscope-go/paraformer"
)
func main() {
model := paraformer.ParaformerRealTimeV1
token := os.Getenv("DASHSCOPE_API_KEY")
if token == "" {
panic("token is empty")
}
cli := dashscopego.NewTongyiClient(model, token)
streamCallbackFn := func(ctx context.Context, chunk []byte) error {
fmt.Print(string(chunk))
return nil
}
headerPara := paraformer.ReqHeader{
Streaming: "duplex",
TaskID: paraformer.GenerateTaskID(),
Action: "run-task",
}
payload := paraformer.PayloadIn{
Parameters: paraformer.Parameters{
// seems like only support 16000 sample-rate.
SampleRate: 16000,
Format: "pcm",
},
Input: map[string]interface{}{},
Task: "asr",
TaskGroup: "audio",
Function: "recognition",
}
req := &paraformer.Request{
Header: headerPara,
Payload: payload,
StreamingFn: streamCallbackFn,
}
// 声音获取 实际使用时请替换成实时音频流.
voiceReader := readAudioFromDesktop()
reader := bufio.NewReader(voiceReader)
cli.CreateSpeechToTextGeneration(context.TODO(), req, reader)
// 等待语音识别结果输出
time.Sleep(5 * time.Second)
}
// 读取音频文件中的录音 模拟实时语音流. 这里下载的官方文档中的示例音频文件.
// `https://dashscope.oss-cn-beijing.aliyuncs.com/samples/audio/paraformer/hello_world_male2.wav`.
func readAudioFromDesktop() *bufio.Reader {
usr, err := user.Current()
if err != nil {
panic(err)
}
voiceFilePath := filepath.Join(usr.HomeDir, "Desktop", "hello_world_female2.wav")
f, err := os.OpenFile(voiceFilePath, os.O_RDONLY, 0640)
if err != nil {
panic(err)
}
if err != nil {
panic(err)
}
reader := bufio.NewReader(f)
return reader
}
```
+5
View File
@@ -0,0 +1,5 @@
package dashscopego
const (
DashscopeTokenEnvName = "DASHSCOPE_API_KEY" //nolint:gosec
)
+59
View File
@@ -0,0 +1,59 @@
package embedding
import (
"context"
"github.com/casibase/dashscope-go-sdk/httpclient"
)
const (
embeddingURL = "https://dashscope.aliyuncs.com/api/v1/services/embeddings/text-embedding/text-embedding"
// defaultEmbeddingModel = "text-embedding-v1"
defaultEmbeddingModel = "text-embedding-v2"
)
type Request struct {
Model string `json:"model"`
Input struct {
Texts []string `json:"texts"`
} `json:"input"`
Params struct {
TextType string `json:"text_type"` // query or document
} `json:"parameters"`
}
type Embedding struct {
TextIndex int `json:"text_index"`
Embedding []float32 `json:"embedding"`
}
type Output struct {
Embeddings []Embedding `json:"embeddings"`
Usgae struct {
TotalTokens int `json:"total_tokens"`
} `json:"usage"`
RequestID string `json:"request_id"`
}
type Response struct {
Output Output `json:"output"`
}
//nolint:lll
func CreateEmbedding(ctx context.Context, req *Request, cli httpclient.IHttpClient, token string) (*Response, error) {
if req.Model == "" {
req.Model = defaultEmbeddingModel
}
if req.Params.TextType == "" {
req.Params.TextType = "document"
}
resp := Response{}
tokenOption := httpclient.WithTokenHeaderOption(token)
err := cli.Post(ctx, embeddingURL, req, &resp, tokenOption)
if err != nil {
return nil, err
}
return &resp, nil
}
+21
View File
@@ -0,0 +1,21 @@
package dashscopego
import "errors"
type WrapMessageError struct {
Message string
Cause error
}
func (e *WrapMessageError) Error() string {
if e.Cause == nil {
return e.Message
}
return e.Message + ": " + e.Cause.Error()
}
var (
ErrModelNotSet = errors.New("model is not set")
ErrEmptyResponse = errors.New("empty response")
ErrImageFilePrefix = errors.New("file prefix is not supported, must be one of: file://, https://, http://")
)
+85
View File
@@ -0,0 +1,85 @@
package main
import (
"bufio"
"context"
"log"
"os"
"os/user"
"path/filepath"
"time"
dashscopego "github.com/casibase/dashscope-go-sdk"
"github.com/casibase/dashscope-go-sdk/paraformer"
)
func main() {
model := paraformer.ParaformerRealTimeV1
token := os.Getenv("DASHSCOPE_API_KEY")
if token == "" {
panic("token is empty")
}
cli := dashscopego.NewTongyiClient(model, token)
streamCallbackFn := func(ctx context.Context, chunk []byte) error {
log.Print(string(chunk))
return nil
}
headerPara := paraformer.ReqHeader{
Streaming: "duplex",
TaskID: paraformer.GenerateTaskID(),
Action: "run-task",
}
payload := paraformer.PayloadIn{
Parameters: paraformer.Parameters{
// seems like only support 16000 sample-rate.
SampleRate: 16000,
Format: "pcm",
},
Input: map[string]interface{}{},
Task: "asr",
TaskGroup: "audio",
Function: "recognition",
}
req := &paraformer.Request{
Header: headerPara,
Payload: payload,
StreamingFn: streamCallbackFn,
}
// 声音获取 实际使用时请替换成实时音频流.
voiceReader := readAudioFromDesktop()
reader := bufio.NewReader(voiceReader)
if err := cli.CreateSpeechToTextGeneration(context.TODO(), req, reader); err != nil {
panic(err)
}
// 等待语音识别结果输出
time.Sleep(5 * time.Second)
}
// 读取音频文件中的录音 模拟实时语音流. 这里下载的官方文档中的示例音频文件.
// `https://dashscope.oss-cn-beijing.aliyuncs.com/samples/audio/paraformer/hello_world_male2.wav`.
func readAudioFromDesktop() *bufio.Reader {
usr, err := user.Current()
if err != nil {
panic(err)
}
voiceFilePath := filepath.Join(usr.HomeDir, "Desktop", "hello_world_female2.wav")
voice, err := os.OpenFile(voiceFilePath, os.O_RDONLY, 0o640) //nolint:gofumpt
if err != nil {
panic(err)
}
reader := bufio.NewReader(voice)
return reader
}
+61
View File
@@ -0,0 +1,61 @@
package main
import (
"context"
"log"
dashscopego "github.com/casibase/dashscope-go-sdk"
"github.com/casibase/dashscope-go-sdk/embedding"
//"os"
"github.com/casibase/dashscope-go-sdk/qwen"
)
func main() {
model := qwen.QwenTurbo
// token := os.Getenv("DASHSCOPE_API_KEY")
token := "sk-f038f63cb4e3446fb61ba2e9e93a06a8"
if token == "" {
panic("token is empty")
}
cli := dashscopego.NewTongyiClient(model, token)
// content := qwen.TextContent{Text: "tell me a joke"}
//input := dashscopego.TextInput{
// Messages: []dashscopego.TextMessage{
// {Role: "user", Content: &content},
// },
//}
// callback function: print stream result
//streamCallbackFn := func(ctx context.Context, chunk []byte) error {
// log.Print(string(chunk))
// return nil
//}
//req := &dashscopego.TextRequest{
// Input: input,
// StreamingFn: streamCallbackFn,
//}
req := &embedding.Request{
Input: struct {
Texts []string `json:"texts"`
}{
Texts: []string{"tell me a joke"}, // 输入文本
},
}
ctx := context.TODO()
// resp, err := cli.CreateCompletion(ctx, req)
resp, err := cli.CreateEmbedding(ctx, req)
if err != nil {
panic(err)
}
// log.Println("\nnon-stream result: ")
// log.Println(resp.Output.Choices[0].Message.Content.ToString())
log.Println(resp)
}
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"context"
"log"
"os"
dashscopego "github.com/casibase/dashscope-go-sdk"
"github.com/casibase/dashscope-go-sdk/qwen"
)
func main() {
model := qwen.QwenAudioTurbo
token := os.Getenv("DASHSCOPE_API_KEY")
if token == "" {
panic("token is empty")
}
cli := dashscopego.NewTongyiClient(model, token)
sysContent := qwen.AudioContentList{
{
Text: "You are a helpful assistant.",
},
}
userContent := qwen.AudioContentList{
{
Text: "该段对话表达了什么观点? 详细分析该讲话者的语气,展现出什么样的情绪", //nolint:gosmopolitan
},
{
// 使用本地音频文件
// Audio: "file:///Users/xxx/Desktop/hello_world_female2.wav",
// 官方文档中的例子
Audio: "https://dashscope.oss-cn-beijing.aliyuncs.com/audios/2channel_16K.wav",
},
}
input := dashscopego.AudioInput{
Messages: []dashscopego.AudioMessage{
{Role: "system", Content: &sysContent},
{Role: "user", Content: &userContent},
},
}
// callback function: print stream result
streamCallbackFn := func(ctx context.Context, chunk []byte) error {
log.Print(string(chunk))
return nil
}
req := &dashscopego.AudioRequest{
Input: input,
StreamingFn: streamCallbackFn,
}
ctx := context.TODO()
resp, err := cli.CreateAudioCompletion(ctx, req)
if err != nil {
panic(err)
}
log.Println("\nnon-stream result: ")
log.Println(resp.Output.Choices[0].Message.Content.ToString())
}
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"context"
"log"
"os"
dashscopego "github.com/casibase/dashscope-go-sdk"
"github.com/casibase/dashscope-go-sdk/qwen"
)
func main() {
model := qwen.QwenVLPlus
token := os.Getenv("DASHSCOPE_API_KEY")
if token == "" {
panic("token is empty")
}
cli := dashscopego.NewTongyiClient(model, token)
sysContent := qwen.VLContentList{
{
Text: "You are a helpful assistant.",
},
}
userContent := qwen.VLContentList{
{
// Text: "describe the image",
Text: "用唐诗体说明一下这张图片中的内容", //nolint:gosmopolitan
},
{
// Image: "https://pic.ntimg.cn/20140113/8800276_184351657000_2.jpg",
// Image: "file:///Users/xxxx/xxxx.png",
Image: "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg",
},
}
input := dashscopego.VLInput{
Messages: []dashscopego.VLMessage{
{Role: "system", Content: &sysContent},
{Role: "user", Content: &userContent},
},
}
// callback function: print stream result
streamCallbackFn := func(ctx context.Context, chunk []byte) error {
log.Print(string(chunk))
return nil
}
req := &dashscopego.VLRequest{
Input: input,
StreamingFn: streamCallbackFn,
}
ctx := context.TODO()
resp, err := cli.CreateVLCompletion(ctx, req)
if err != nil {
panic(err)
}
log.Println("\nnon-stream result: ")
log.Println(resp.Output.Choices[0].Message.Content.ToString())
}
+70
View File
@@ -0,0 +1,70 @@
package main
import (
"bytes"
"context"
"image"
"image/png"
"log"
"os"
"os/user"
"path/filepath"
dashscopego "github.com/casibase/dashscope-go-sdk"
"github.com/casibase/dashscope-go-sdk/wanx"
)
func main() {
model := wanx.WanxV1
token := os.Getenv("DASHSCOPE_API_KEY")
if token == "" {
panic("token is empty")
}
cli := dashscopego.NewTongyiClient(model, token)
req := &wanx.ImageSynthesisRequest{
// Model: "wanx-v1",
Model: model,
Input: wanx.ImageSynthesisInput{
Prompt: "A beautiful sunset",
},
Params: wanx.ImageSynthesisParams{
N: 1,
},
}
ctx := context.TODO()
imgBlobs, err := cli.CreateImageGeneration(ctx, req)
if err != nil {
panic(err)
}
for _, blob := range imgBlobs {
saveImg2Desktop(blob.ImgType, blob.Data)
}
}
func saveImg2Desktop(_ string, data []byte) {
buf := bytes.NewBuffer(data)
img, _, err := image.Decode(buf)
if err != nil {
log.Fatal(err)
}
usr, err := user.Current()
if err != nil {
panic(err)
}
f, err := os.Create(filepath.Join(usr.HomeDir, "Desktop", "wanx_image.png"))
if err != nil {
panic(err)
}
defer f.Close()
if err := png.Encode(f, img); err != nil {
panic(err)
}
}
+18
View File
@@ -0,0 +1,18 @@
module github.com/casibase/dashscope-go-sdk
go 1.21.0
require (
github.com/gabriel-vasile/mimetype v1.4.3
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.1
github.com/stretchr/testify v1.8.4
go.uber.org/mock v0.4.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.17.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+20
View File
@@ -0,0 +1,20 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+41
View File
@@ -0,0 +1,41 @@
package httpclient
import (
"errors"
"strconv"
)
var ErrNetwork = errors.New("network error")
type EmptyRequestBodyError struct {
Method string
}
func (e *EmptyRequestBodyError) Error() string {
return "POST or PUT request body cannot be empty for method " + e.Method
}
type WrapMessageError struct {
Message string
Cause error
}
func (e *WrapMessageError) Error() string {
if e.Cause == nil {
return e.Message
}
return e.Message + ": " + e.Cause.Error()
}
type HTTPRequestError struct {
Message string
Cause error
Code int
}
func (e *HTTPRequestError) Error() string {
if e.Cause == nil {
return e.Message + ": " + strconv.Itoa(e.Code)
}
return e.Message + ": " + strconv.Itoa(e.Code) + " " + e.Cause.Error()
}
+241
View File
@@ -0,0 +1,241 @@
package httpclient
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"image"
"image/png"
"io"
"net/http"
"net/url"
"os/exec"
"strings"
"time"
)
type HTTPOption func(c *HTTPCli)
//go:generate mockgen -destination=http_client_mock.go -package=httpclient . IHttpClient
type IHttpClient interface {
PostSSE(ctx context.Context, urll string, reqbody interface{}, options ...HTTPOption) (chan string, error)
Post(ctx context.Context, urll string, reqbody interface{}, resp interface{}, options ...HTTPOption) error
Get(ctx context.Context, urll string, params map[string]string, resp interface{}, options ...HTTPOption) error
GetImage(ctx context.Context, imgURL string, options ...HTTPOption) ([]byte, error)
}
type HTTPCli struct {
client http.Client
req *http.Request
sseStream chan string
}
var _ IHttpClient = (*HTTPCli)(nil)
func NewHTTPClient() *HTTPCli {
return &HTTPCli{
client: http.Client{},
sseStream: nil,
}
}
func (c *HTTPCli) Get(ctx context.Context, urll string, params map[string]string, respbody interface{}, options ...HTTPOption) error {
if params != nil {
var flag bool
for k, v := range params {
if !flag {
urll += "?"
flag = true
}
urll = fmt.Sprintf("%s%s=%s&", urll, k, url.QueryEscape(v))
}
urll = strings.TrimSuffix(urll, "&")
}
resp, err := c.httpInner(ctx, "GET", urll, nil, options...)
if err != nil {
return err
}
defer resp.Body.Close()
result, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
err = json.Unmarshal(result, &respbody)
if err != nil {
return &WrapMessageError{Message: "Unmarshal Json failed", Cause: err}
}
return nil
}
func (c *HTTPCli) GetImage(ctx context.Context, imgURL string, options ...HTTPOption) ([]byte, error) {
resp, err := c.httpInner(ctx, "GET", imgURL, nil, options...)
if err != nil {
return nil, err
}
defer resp.Body.Close()
img, _, err := image.Decode(resp.Body)
if err != nil {
return nil, &WrapMessageError{Message: "Decode image failed", Cause: err}
}
buf := new(bytes.Buffer)
err = png.Encode(buf, img)
if err != nil {
return nil, &WrapMessageError{Message: "Encode image topng failed", Cause: err}
}
return buf.Bytes(), nil
}
// nolint:lll
func (c *HTTPCli) PostSSE(ctx context.Context, urll string, reqbody interface{}, options ...HTTPOption) (chan string, error) {
if reqbody == nil {
err := &EmptyRequestBodyError{}
return nil, err
}
chanBuffer := 500
sseStream := make(chan string, chanBuffer)
c.sseStream = sseStream
options = append(options, WithStream(), WithHeader(HeaderMap{"content-type": "application/json"}))
errChan := make(chan error)
go func() {
resp, err := c.httpInner(ctx, "POST", urll, reqbody, options...)
if err != nil {
errChan <- err
} else {
errChan <- nil
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
c.sseStream <- line
}
close(c.sseStream)
}()
err := <-errChan
if err != nil {
return nil, err
}
return c.sseStream, nil
}
// nolint:lll
func (c *HTTPCli) Post(ctx context.Context, urll string, reqbody interface{}, respbody interface{}, options ...HTTPOption) error {
// options = append(options, WithHeader(HeaderMap{"content-type": "application/json"}))
// fmt.Printf("debug body111 : %+v\n ", reqbody)
if reqbody == nil {
err := &EmptyRequestBodyError{}
return err
}
resp, err := c.httpInner(ctx, "POST", urll, reqbody, options...)
if err != nil {
return err
}
defer func() {
if resp != nil {
resp.Body.Close()
}
}()
result, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if len(result) != 0 && respbody != nil {
err = json.Unmarshal(result, &respbody)
if err != nil {
return &WrapMessageError{Message: "Unmarshal Json failed", Cause: err}
}
}
return nil
}
func (c *HTTPCli) EncodeJSONBody(body interface{}) (*bytes.Buffer, error) {
if body != nil {
var bodyJSON []byte
switch body := body.(type) {
case *bytes.Buffer:
return body, nil
case []byte:
bodyJSON = body
default:
var err error
bodyJSON, err = json.Marshal(body)
if err != nil {
return nil, err
}
}
return bytes.NewBuffer(bodyJSON), nil
}
return bytes.NewBuffer(nil), nil
}
// nolint:lll
func (c *HTTPCli) httpInner(ctx context.Context, method, url string, body interface{}, options ...HTTPOption) (*http.Response, error) {
var err error
// fmt.Printf("debug... body: %+v\n", body)
bodyBuffer, err := c.EncodeJSONBody(body)
if err != nil {
return nil, err
}
c.req, err = http.NewRequestWithContext(ctx, method, url, bodyBuffer)
if err != nil {
return nil, err
}
for _, option := range options {
option(c)
}
resp, err := c.client.Do(c.req)
if err != nil {
return nil, err
}
if resp.StatusCode >= 300 || resp.StatusCode < 200 {
result, errIo := io.ReadAll(resp.Body)
if errIo != nil {
err = errIo
return resp, err
}
err = &HTTPRequestError{Message: "request Failed: " + string(result), Code: resp.StatusCode}
return resp, err
}
// Note: close rsp at outer function
return resp, nil
}
func NetworkStatus() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "ping", "-c", "1", "8.8.8.8")
_, err := cmd.CombinedOutput()
if err != nil {
return false, ErrNetwork
}
return true, nil
}
+117
View File
@@ -0,0 +1,117 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/eswulei/dashscope-go/httpclient (interfaces: IHttpClient)
//
// Generated by this command:
//
// mockgen -destination=http_client_mock.go -package=httpclient . IHttpClient
//
// Package httpclient is a generated GoMock package.
package httpclient
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockIHttpClient is a mock of IHttpClient interface.
type MockIHttpClient struct {
ctrl *gomock.Controller
recorder *MockIHttpClientMockRecorder
}
// MockIHttpClientMockRecorder is the mock recorder for MockIHttpClient.
type MockIHttpClientMockRecorder struct {
mock *MockIHttpClient
}
// NewMockIHttpClient creates a new mock instance.
func NewMockIHttpClient(ctrl *gomock.Controller) *MockIHttpClient {
mock := &MockIHttpClient{ctrl: ctrl}
mock.recorder = &MockIHttpClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockIHttpClient) EXPECT() *MockIHttpClientMockRecorder {
return m.recorder
}
// Get mocks base method.
func (m *MockIHttpClient) Get(arg0 context.Context, arg1 string, arg2 map[string]string, arg3 any, arg4 ...HTTPOption) error {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1, arg2, arg3}
for _, a := range arg4 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Get", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Get indicates an expected call of Get.
func (mr *MockIHttpClientMockRecorder) Get(arg0, arg1, arg2, arg3 any, arg4 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1, arg2, arg3}, arg4...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockIHttpClient)(nil).Get), varargs...)
}
// GetImage mocks base method.
func (m *MockIHttpClient) GetImage(arg0 context.Context, arg1 string, arg2 ...HTTPOption) ([]byte, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "GetImage", varargs...)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetImage indicates an expected call of GetImage.
func (mr *MockIHttpClientMockRecorder) GetImage(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetImage", reflect.TypeOf((*MockIHttpClient)(nil).GetImage), varargs...)
}
// Post mocks base method.
func (m *MockIHttpClient) Post(arg0 context.Context, arg1 string, arg2, arg3 any, arg4 ...HTTPOption) error {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1, arg2, arg3}
for _, a := range arg4 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Post", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Post indicates an expected call of Post.
func (mr *MockIHttpClientMockRecorder) Post(arg0, arg1, arg2, arg3 any, arg4 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1, arg2, arg3}, arg4...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MockIHttpClient)(nil).Post), varargs...)
}
// PostSSE mocks base method.
func (m *MockIHttpClient) PostSSE(arg0 context.Context, arg1 string, arg2 any, arg3 ...HTTPOption) (chan string, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1, arg2}
for _, a := range arg3 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "PostSSE", varargs...)
ret0, _ := ret[0].(chan string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PostSSE indicates an expected call of PostSSE.
func (mr *MockIHttpClientMockRecorder) PostSSE(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1, arg2}, arg3...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostSSE", reflect.TypeOf((*MockIHttpClient)(nil).PostSSE), varargs...)
}
+32
View File
@@ -0,0 +1,32 @@
package httpclient
import (
"time"
)
type HeaderMap map[string]string
func WithHeader(header HeaderMap) HTTPOption {
return func(c *HTTPCli) {
for k, v := range header {
c.req.Header.Set(k, v)
}
}
}
func WithTimeout(timeout time.Duration) HTTPOption {
return func(c *HTTPCli) {
c.client.Timeout = timeout
}
}
func WithStream() HTTPOption {
return func(c *HTTPCli) {
c.req.Header.Set("Accept", "text/event-stream")
}
}
func WithTokenHeaderOption(token string) HTTPOption {
m := map[string]string{"Authorization": "Bearer " + token, "Content-Type": "application/json"}
return WithHeader(m)
}
+191
View File
@@ -0,0 +1,191 @@
package httpclient
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/gorilla/websocket"
)
const (
// Timeout for establishing the connection and for reading/writing messages.
writeWait = 30 * time.Second
pongWait = 20 * time.Second
pingPeriod = (pongWait * 8) / 10
// Maximum message size allowed from peer.
maxMessageSize = 1024
// maxMessageSize = 512.
)
type IWsClient interface {
ConnClient(req interface{}) error
CloseClient() error
SendBinaryDates(data []byte)
ResultChans() (<-chan WsMessage, <-chan error)
}
// StartClient starts the client operation.
func (c *WsClient) ConnClient(req interface{}) error {
if err := c.connect(); err != nil {
return err
}
reqJSON, err := json.Marshal(req)
if err != nil {
return err
}
reqInput := WsMessage{
Type: websocket.TextMessage,
Data: reqJSON,
}
c.inputChan <- reqInput
err, ok := <-c.errChan
if ok && err != nil {
return err
}
return nil
}
func (c *WsClient) CloseClient() error {
close(c.inputChan)
close(c.outputChan)
close(c.errChan)
return c.Conn.Close()
}
func (c *WsClient) SendBinaryDates(data []byte) {
streamInput := WsMessage{
Type: websocket.BinaryMessage,
Data: data,
}
c.inputChan <- streamInput
}
func (c *WsClient) ResultChans() (<-chan WsMessage, <-chan error) {
return c.outputChan, c.errChan
}
type WsMessage struct {
// ws data type, e.g. websocket.TextMessage, websocket.BinaryMessage...
Type int
// ws data body
Data []byte
}
// Client represents a websocket client.
type WsClient struct {
URL string
Headers http.Header
Conn *websocket.Conn
inputChan chan WsMessage
outputChan chan WsMessage
errChan chan error
}
func NewWsClient(url string, headers http.Header) *WsClient {
return &WsClient{
URL: url,
Headers: headers,
}
}
// readPump pumps messages from the websocket connection to the hub.
func (c *WsClient) readPump() {
defer func() {
c.Conn.Close()
}()
pongDelay := time.Now().Add(pongWait)
pongFn := func(string) error {
if err := c.Conn.SetReadDeadline(pongDelay); err != nil {
return err
}
return nil
}
c.Conn.SetReadLimit(maxMessageSize)
if err := c.Conn.SetReadDeadline(pongDelay); err != nil {
c.errChan <- err
}
c.Conn.SetPongHandler(pongFn)
for {
_, message, err := c.Conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
c.errChan <- err
}
break
}
c.outputChan <- WsMessage{
Type: websocket.TextMessage,
Data: message,
}
}
}
// writePump pumps messages from the write channel to the websocket connection.
func (c *WsClient) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.Conn.Close()
}()
for {
select {
case message, ok := <-c.inputChan:
if !ok {
// The write channel is closed.
c.errChan <- fmt.Errorf("write channel is closed")
err := c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
if err != nil {
c.errChan <- err
}
return
}
err := c.Conn.SetWriteDeadline(time.Now().Add(writeWait))
if err != nil {
c.errChan <- err
}
if err := c.Conn.WriteMessage(message.Type, message.Data); err != nil {
c.errChan <- err
return
}
c.errChan <- nil
case <-ticker.C:
if err := c.Conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
c.errChan <- err
return
}
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
c.errChan <- err
return
}
}
}
}
// connect initializes the websocket connection and starts the read and write pumps.
func (c *WsClient) connect() error {
conn, resp, err := websocket.DefaultDialer.Dial(c.URL, c.Headers)
if err != nil {
return err
}
defer resp.Body.Close()
c.Conn = conn
c.inputChan = make(chan WsMessage, 100)
c.outputChan = make(chan WsMessage, 100)
c.errChan = make(chan error, 1)
go c.writePump()
go c.readPump()
return nil
}
+34
View File
@@ -0,0 +1,34 @@
package dashscopego
import (
"github.com/casibase/dashscope-go-sdk/qwen"
)
type (
TextInput = qwen.Input[*qwen.TextContent]
VLInput = qwen.Input[*qwen.VLContentList]
AudioInput = qwen.Input[*qwen.AudioContentList]
TextRequest = qwen.Request[*qwen.TextContent]
VLRequest = qwen.Request[*qwen.VLContentList]
AudioRequest = qwen.Request[*qwen.AudioContentList]
TextQwenResponse = qwen.OutputResponse[*qwen.TextContent]
VLQwenResponse = qwen.OutputResponse[*qwen.VLContentList]
AudioQwenResponse = qwen.OutputResponse[*qwen.AudioContentList]
TextMessage = qwen.Message[*qwen.TextContent]
VLMessage = qwen.Message[*qwen.VLContentList]
AudioMessage = qwen.Message[*qwen.AudioContentList]
)
func NewQwenMessage[T qwen.IQwenContent](role string, content T) *qwen.Message[T] {
if content == nil {
panic("content is nil")
}
return &qwen.Message[T]{
Role: role,
Content: content,
}
}
+74
View File
@@ -0,0 +1,74 @@
package paraformer
import "context"
const ParaformerWSURL = "wss://dashscope.aliyuncs.com/api-ws/v1/inference"
type Parameters struct {
SampleRate int `json:"sample_rate"`
Format string `json:"format"`
}
type StreamingFunc func(ctx context.Context, chunk []byte) error
type Request struct {
Header ReqHeader `json:"header"`
Payload PayloadIn `json:"payload"`
StreamingFn StreamingFunc `json:"-"`
}
type ReqHeader struct {
Streaming string `json:"streaming"`
TaskID string `json:"task_id"`
Action string `json:"action"`
}
type PayloadIn struct {
Model string `json:"model"`
Parameters Parameters `json:"parameters"`
Input map[string]interface{} `json:"input"`
Task string `json:"task"`
TaskGroup string `json:"task_group"`
Function string `json:"function"`
}
// ---------
// type Word struct {
// BeginTime int `json:"begin_time"`
// EndTime int `json:"end_time"`
// Text string `json:"text"`
// Punctuation string `json:"punctuation"`
// }
type Sentence struct {
BeginTime int `json:"begin_time"`
EndTime int `json:"end_time"`
Text string `json:"text"` // full text
// Words []Word `json:"words"`
}
type Output struct {
Sentence Sentence `json:"sentence"`
}
type Usage struct {
Duration int `json:"duration"`
}
type PayloadOut struct {
Output Output `json:"output"`
Usage Usage `json:"usage"`
}
type Attributes struct{}
type Header struct {
TaskID string `json:"task_id"`
Event string `json:"event"`
Attributes Attributes `json:"attributes"`
}
type RecognitionResult struct {
Header Header `json:"header"`
Payload PayloadOut `json:"payload"`
}
+73
View File
@@ -0,0 +1,73 @@
package paraformer
import (
"context"
"fmt"
"net/http"
"strings"
httpclient "github.com/casibase/dashscope-go-sdk/httpclient"
"github.com/google/uuid"
)
func ConnRecognitionClient(request *Request, token string) (*httpclient.WsClient, error) {
// Initialize the client with the necessary parameters.
header := http.Header{}
header.Add("Authorization", token)
client := httpclient.NewWsClient(ParaformerWSURL, header)
if err := client.ConnClient(request); err != nil {
return nil, err
}
return client, nil
}
func CloseRecognitionClient(cli *httpclient.WsClient) error {
return cli.CloseClient()
}
func SendRadioData(cli *httpclient.WsClient, bytesData []byte) {
cli.SendBinaryDates(bytesData)
}
type ResultWriter interface {
WriteResult(str string) error
}
func HandleRecognitionResult(ctx context.Context, cli *httpclient.WsClient, fn StreamingFunc) error {
outputChan, errChan := cli.ResultChans()
for {
select {
case output, ok := <-outputChan:
if !ok {
return fmt.Errorf("outputChan is closed")
}
// streaming callback func
if err := fn(ctx, output.Data); err != nil {
return err
}
case err := <-errChan:
if err != nil {
return err
}
case <-ctx.Done():
return ctx.Err()
}
}
}
// task_id length 32.
func GenerateTaskID() string {
u, err := uuid.NewUUID()
if err != nil {
panic(err)
}
uuid := strings.ReplaceAll(u.String(), "-", "")
return uuid
}
+13
View File
@@ -0,0 +1,13 @@
package paraformer
type ModelParaformer = string
const (
// detect from file.
ParaformerV1 ModelParaformer = "paraformer-v1"
Paraformer8KV1 ModelParaformer = "paraformer-8k-v1"
ParaformerMtlV1 ModelParaformer = "paraformer-mtl-v1"
// real time voice.
ParaformerRealTimeV1 ModelParaformer = "paraformer-realtime-v1"
ParaformerRealTime8KV1 ModelParaformer = "paraformer-realtime-8k-v1"
)
+18
View File
@@ -0,0 +1,18 @@
package qwen
// qwen(text-generation) and qwen-vl(multi-modal) have different data format
// so define generic interfaces for them.
type IQwenContent interface {
*TextContent | *VLContentList | *AudioContentList
IQwenContentMethods
}
// TODO: langchaingo 中有使用这个 interface, 稍后看看是否需要重新设计.
type IQwenContentMethods interface {
ToBytes() []byte
ToString() string
SetText(text string)
AppendText(text string)
SetImage(url string)
SetAudio(url string)
}
+76
View File
@@ -0,0 +1,76 @@
package qwen
type AudioContent struct {
Audio string `json:"audio,omitempty"`
Text string `json:"text,omitempty"`
}
type AudioContentList []AudioContent
func NewAudioContentList() *AudioContentList {
ac := AudioContentList(make([]AudioContent, 0))
return &ac
}
func (acList *AudioContentList) ToBytes() []byte {
if acList == nil || len(*acList) == 0 {
return []byte("")
}
return []byte((*acList)[0].Text)
}
func (acList *AudioContentList) ToString() string {
if acList == nil || len(*acList) == 0 {
return ""
}
return (*acList)[0].Text
}
func (acList *AudioContentList) SetText(s string) {
if acList == nil {
panic("AudioContentList is nil")
}
*acList = append(*acList, AudioContent{Text: s})
}
func (acList *AudioContentList) SetAudio(url string) {
if acList == nil {
panic("AudioContentList is nil or empty")
}
*acList = append(*acList, AudioContent{Audio: url})
}
func (acList *AudioContentList) PopAudioContent() (AudioContent, bool) {
if acList == nil {
panic("AudioContentList is nil or empty")
}
hasAudio := false
for i, v := range *acList {
if v.Audio != "" {
hasAudio = true
preSlice := (*acList)[:i]
if i == len(*acList)-1 {
*acList = preSlice
} else {
postSlice := (*acList)[i+1:]
*acList = append(*acList, preSlice...)
*acList = append(*acList, postSlice...)
}
return v, hasAudio
}
}
return AudioContent{}, hasAudio
}
func (acList *AudioContentList) AppendText(s string) {
if acList == nil || len(*acList) == 0 {
panic("AudioContentList is nil or empty")
}
(*acList)[0].Text += s
}
func (acList *AudioContentList) SetImage(_ string) {
panic("AudioContentList does not support SetImage")
}
+56
View File
@@ -0,0 +1,56 @@
package qwen
import (
"encoding/json"
)
// TextConent is used for text-generation only.
type TextContent struct {
Text string
}
var _ IQwenContentMethods = &TextContent{}
func NewTextContent() *TextContent {
t := TextContent{""}
return &t
}
func (t *TextContent) ToBytes() []byte {
return []byte(t.Text)
}
func (t *TextContent) ToString() string {
return t.Text
}
func (t *TextContent) SetText(text string) {
if t == nil {
panic("TextContent is nil")
}
t.Text = text
}
func (t *TextContent) AppendText(text string) {
if t == nil {
panic("TextContent is nil")
}
t.Text += text
}
func (t *TextContent) SetImage(_ string) {
panic("TextContent does not support SetImage")
}
func (t *TextContent) SetAudio(_ string) {
panic("TextContent does not support SetAudio")
}
// redifine MarshalJSON and UnmarshalJSON.
func (t TextContent) MarshalJSON() ([]byte, error) {
return json.Marshal(t.Text)
}
func (t *TextContent) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &t.Text)
}
+81
View File
@@ -0,0 +1,81 @@
package qwen
// VLContentList is used for multi-modal generation.
type VLContentList []VLContent
var _ IQwenContentMethods = &VLContentList{}
type VLContent struct {
Image string `json:"image,omitempty"`
Text string `json:"text,omitempty"`
}
var _ IQwenContentMethods = &VLContentList{}
func NewVLContentList() *VLContentList {
vl := VLContentList(make([]VLContent, 0))
return &vl
}
func (vlist *VLContentList) ToBytes() []byte {
if vlist == nil || len(*vlist) == 0 {
return []byte("")
}
return []byte((*vlist)[0].Text)
}
func (vlist *VLContentList) ToString() string {
if vlist == nil || len(*vlist) == 0 {
return ""
}
return (*vlist)[0].Text
}
func (vlist *VLContentList) SetText(s string) {
if vlist == nil {
panic("VLContentList is nil")
}
*vlist = append(*vlist, VLContent{Text: s})
}
func (vlist *VLContentList) SetImage(url string) {
if vlist == nil {
panic("VLContentList is nil or empty")
}
*vlist = append(*vlist, VLContent{Image: url})
}
func (vlist *VLContentList) PopImageContent() (VLContent, bool) {
if vlist == nil {
panic("VLContentList is nil or empty")
}
hasImage := false
for i, v := range *vlist {
if v.Image != "" {
hasImage = true
preSlice := (*vlist)[:i]
if i == len(*vlist)-1 {
*vlist = preSlice
} else {
postSlice := (*vlist)[i+1:]
*vlist = append(*vlist, preSlice...)
*vlist = append(*vlist, postSlice...)
}
return v, hasImage
}
}
return VLContent{}, hasImage
}
func (vlist *VLContentList) AppendText(s string) {
if vlist == nil || len(*vlist) == 0 {
panic("VLContentList is nil or empty")
}
(*vlist)[0].Text += s
}
func (vlist *VLContentList) SetAudio(_ string) {
panic("VLContentList does not support SetAudio")
}
+19
View File
@@ -0,0 +1,19 @@
package qwen
import "errors"
type WrapMessageError struct {
Message string
Cause error
}
func (e *WrapMessageError) Error() string {
if e.Cause == nil {
return e.Message
}
return e.Message + ": " + e.Cause.Error()
}
var ErrEmptyResponse = errors.New("empty response")
var ErrModelNotSet = errors.New("model is not set")
+170
View File
@@ -0,0 +1,170 @@
package qwen
import (
"context"
)
type Parameters struct {
ResultFormat string `json:"result_format,omitempty"`
Seed int `json:"seed,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
TopP float64 `json:"top_p,omitempty"`
TopK int `json:"top_k,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
EnableSearch bool `json:"enable_search,omitempty"`
IncrementalOutput bool `json:"incremental_output,omitempty"`
}
func NewParameters() *Parameters {
return &Parameters{}
}
const DefaultTemperature = 1.0
func DefaultParameters() *Parameters {
q := Parameters{}
q.
SetResultFormat("message").
SetTemperature(DefaultTemperature)
return &q
}
func (p *Parameters) SetResultFormat(value string) *Parameters {
p = p.tryInit()
p.ResultFormat = value
return p
}
func (p *Parameters) SetSeed(value int) *Parameters {
p = p.tryInit()
p.Seed = value
return p
}
func (p *Parameters) SetMaxTokens(value int) *Parameters {
p = p.tryInit()
p.MaxTokens = value
return p
}
func (p *Parameters) SetTopP(value float64) *Parameters {
p = p.tryInit()
p.TopP = value
return p
}
func (p *Parameters) SetTopK(value int) *Parameters {
p = p.tryInit()
p.TopK = value
return p
}
func (p *Parameters) SetTemperature(value float64) *Parameters {
p.tryInit()
p.Temperature = value
return p
}
func (p *Parameters) SetEnableSearch(value bool) *Parameters {
p = p.tryInit()
p.EnableSearch = value
return p
}
func (p *Parameters) SetIncrementalOutput(value bool) *Parameters {
p = p.tryInit()
p.IncrementalOutput = value
return p
}
func (p *Parameters) tryInit() *Parameters {
if p == nil {
p = &Parameters{}
}
return p
}
type Message[T IQwenContent] struct {
Role string `json:"role"`
Content T `json:"content"`
}
type Input[T IQwenContent] struct {
Messages []Message[T] `json:"messages"`
}
type StreamingFunc func(ctx context.Context, chunk []byte) error
type Request[T IQwenContent] struct {
Model string `json:"model"`
Input Input[T] `json:"input"`
Parameters *Parameters `json:"parameters,omitempty"`
// streaming callback function.
StreamingFn StreamingFunc `json:"-"`
// qwen-vl model need to upload image to oss for recognition.
HasUploadOss bool `json:"-"`
}
func (q *Request[T]) SetModel(value string) *Request[T] {
q.Model = value
return q
}
func (q *Request[T]) SetInput(value Input[T]) *Request[T] {
q.Input = value
return q
}
func (q *Request[T]) SetParameters(value *Parameters) *Request[T] {
q.Parameters = value
return q
}
func (q *Request[T]) SetStreamingFunc(fn func(ctx context.Context, chunk []byte) error) *Request[T] {
q.StreamingFn = fn
return q
}
type StreamOutput[T IQwenContent] struct {
ID string `json:"id"`
Event string `json:"event"`
HTTPStatus int `json:"http_status"`
Output OutputResponse[T] `json:"output"`
Err error `json:"error"`
}
type Choice[T IQwenContent] struct {
Message Message[T] `json:"message"`
FinishReason string `json:"finish_reason"`
}
// new version response format.
type Output[T IQwenContent] struct {
Choices []Choice[T] `json:"choices"`
}
type Usage struct {
TotalTokens int `json:"total_tokens"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
}
type OutputResponse[T IQwenContent] struct {
Output Output[T] `json:"output"`
Usage Usage `json:"usage"`
RequestID string `json:"request_id"`
// ErrMsg string `json:"error_msg"`
}
func (t *OutputResponse[T]) GetChoices() []Choice[T] {
return t.Output.Choices
}
func (t *OutputResponse[T]) GetUsage() Usage {
return t.Usage
}
func (t *OutputResponse[T]) GetRequestID() string {
return t.RequestID
}
+81
View File
@@ -0,0 +1,81 @@
package qwen
const (
DashScopeBaseURL = "https://dashscope.aliyuncs.com/api"
QwenSubURL = "/v1/services/aigc/text-generation/generation"
QwenVLSubURL = "/v1/services/aigc/multimodal-generation/generation"
QwenAudioSubURL = QwenVLSubURL
)
type ModelQwen = string
const (
// text-generation model.
QwenTurbo ModelQwen = "qwen-turbo"
QwenPlus ModelQwen = "qwen-plus"
QwenMax ModelQwen = "qwen-max"
QwenMax1201 ModelQwen = "qwen-max-1201"
QwenMaxLongContext ModelQwen = "qwen-max-longcontext"
// multi-modal model.
QwenVLPlus ModelQwen = "qwen-vl-plus"
QwenAudioTurbo ModelQwen = "qwen-audio-turbo"
)
// text-generation only.
func URLQwen() string {
return DashScopeBaseURL + QwenSubURL
}
// multimodal.
func URLQwenVL() string {
return DashScopeBaseURL + QwenVLSubURL
}
func URLQwenAudio() string {
return DashScopeBaseURL + QwenAudioSubURL
}
/*
func ChoseModelQwen(model string) ModelQwen {
m := Model{}
switch model {
case "qwen-turbo":
return m.QwenTurbo()
case "qwen-plus":
return m.QwenPlus()
case "qwen-max":
return m.QwenMax()
case "qwen-max-1201":
return m.QwenMax1201()
case "qwen-max-longcontext":
return m.QwenMaxLongContext()
default:
log.Println("target model not found, use default model: qwen-turbo")
return m.QwenTurbo()
}
}
type Model struct{}
func (m *Model) QwenTurbo() ModelQwen {
return QwenTurbo
}
func (m *Model) QwenPlus() ModelQwen {
return QwenPlus
}
func (m *Model) QwenMax() ModelQwen {
return QwenMax
}
func (m *Model) QwenMax1201() ModelQwen {
return QwenMax1201
}
func (m *Model) QwenMaxLongContext() ModelQwen {
return QwenMaxLongContext
}
*/
+196
View File
@@ -0,0 +1,196 @@
package qwen
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"strconv"
"strings"
httpclient "github.com/casibase/dashscope-go-sdk/httpclient"
)
//nolint:lll
func SendMessage[T IQwenContent](ctx context.Context, payload *Request[T], cli httpclient.IHttpClient, url, token string) (*OutputResponse[T], error) {
if payload.Model == "" {
return nil, ErrModelNotSet
}
resp := OutputResponse[T]{}
tokenOpt := httpclient.WithTokenHeaderOption(token)
header := map[string]string{
"Content-Type": "application/json",
}
if payload.HasUploadOss {
header["X-DashScope-OssResourceResolve"] = "enable"
}
headerOpt := httpclient.WithHeader(header)
err := cli.Post(ctx, url, payload, &resp, tokenOpt, headerOpt)
if err != nil {
return nil, err
}
if len(resp.Output.Choices) == 0 {
return nil, ErrEmptyResponse
}
return &resp, nil
}
//nolint:lll
func SendMessageStream[T IQwenContent](ctx context.Context, payload *Request[T], cli httpclient.IHttpClient, url, token string) (*OutputResponse[T], error) {
if payload.Model == "" {
return nil, ErrModelNotSet
}
header := map[string]string{
"Accept": "text/event-stream",
"Content-Type": "application/json",
}
if payload.HasUploadOss {
header["X-DashScope-OssResourceResolve"] = "enable"
}
responseChan := asyncChatStreaming(ctx, payload, header, cli, url, token)
return iterateStreamChannel(ctx, responseChan, payload.StreamingFn)
}
func iterateStreamChannel[T IQwenContent](ctx context.Context, channel <-chan StreamOutput[T], fn StreamingFunc) (*OutputResponse[T], error) {
outputMessage := OutputResponse[T]{}
for rspData := range channel {
if rspData.Err != nil {
return nil, &httpclient.HTTPRequestError{Message: "SSE Error: ", Cause: rspData.Err}
}
if len(rspData.Output.Output.Choices) == 0 {
return nil, ErrEmptyResponse
}
chunk := rspData.Output.Output.Choices[0].Message.Content.ToBytes()
if err := fn(ctx, chunk); err != nil {
return nil, &WrapMessageError{Message: "StreamingFunc Error", Cause: err}
}
outputMessage.RequestID = rspData.Output.RequestID
outputMessage.Usage = rspData.Output.Usage
if outputMessage.Output.Choices == nil {
outputMessage.Output.Choices = rspData.Output.Output.Choices
} else {
choice := outputMessage.Output.Choices[0]
choice.Message.Role = rspData.Output.Output.Choices[0].Message.Role
choice.Message.Content.AppendText(rspData.Output.Output.Choices[0].Message.Content.ToString())
choice.FinishReason = rspData.Output.Output.Choices[0].FinishReason
outputMessage.Output.Choices[0] = choice
}
}
return &outputMessage, nil
}
//nolint:lll
func asyncChatStreaming[T IQwenContent](
ctx context.Context,
payload *Request[T],
header map[string]string,
cli httpclient.IHttpClient,
url, token string,
) <-chan StreamOutput[T] {
chanBuffer := 100
_respChunkChannel := make(chan StreamOutput[T], chanBuffer)
go func() {
_combineStreamingChunk(ctx, payload, header, _respChunkChannel, cli, url, token)
}()
return _respChunkChannel
}
/*
* combine SSE streaming lines to be a structed response data
* id: xxxx
* event: xxxxx
* ......
*/
func _combineStreamingChunk[T IQwenContent](
ctx context.Context,
payload *Request[T],
header map[string]string,
_respChunkChannel chan StreamOutput[T],
cli httpclient.IHttpClient,
url string,
token string,
) {
defer close(_respChunkChannel)
var _rawStreamOutChannel chan string
var err error
headerOpt := httpclient.WithHeader(header)
tokenOpt := httpclient.WithTokenHeaderOption(token)
_rawStreamOutChannel, err = cli.PostSSE(ctx, url, payload, headerOpt, tokenOpt)
if err != nil {
_respChunkChannel <- StreamOutput[T]{Err: err}
return
}
rsp := StreamOutput[T]{}
for v := range _rawStreamOutChannel {
if strings.TrimSpace(v) == "" {
// streaming out combined response
_respChunkChannel <- rsp
rsp = StreamOutput[T]{}
continue
}
err = fillInRespData(v, &rsp)
if err != nil {
rsp.Err = err
_respChunkChannel <- rsp
break
}
}
}
// filled in response data line by line.
func fillInRespData[T IQwenContent](line string, output *StreamOutput[T]) error {
if strings.TrimSpace(line) == "" {
return nil
}
switch {
case strings.HasPrefix(line, "id:"):
output.ID = strings.TrimPrefix(line, "id:")
case strings.HasPrefix(line, "event:"):
output.Event = strings.TrimPrefix(line, "event:")
case strings.HasPrefix(line, ":HTTP_STATUS/"):
code, err := strconv.Atoi(strings.TrimPrefix(line, ":HTTP_STATUS/"))
if err != nil {
output.Err = fmt.Errorf("http_status err: strconv.Atoi %w", err)
}
output.HTTPStatus = code
case strings.HasPrefix(line, "data:"):
dataJSON := strings.TrimPrefix(line, "data:")
if output.Event == "error" {
output.Err = &WrapMessageError{Message: dataJSON}
return nil
}
outputData := OutputResponse[T]{}
err := json.Unmarshal([]byte(dataJSON), &outputData)
if err != nil {
return &WrapMessageError{Message: "unmarshal OutputData Err", Cause: err}
}
output.Output = outputData
default:
data := bytes.TrimSpace([]byte(line))
log.Printf("unknown line: %s", data)
}
return nil
}
+236
View File
@@ -0,0 +1,236 @@
package qwen
import (
"bytes"
"context"
"encoding/json"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"time"
httpclient "github.com/casibase/dashscope-go-sdk/httpclient"
"github.com/gabriel-vasile/mimetype"
)
// upload image to aliyun oss that can be described by LLM
type CertResponse struct {
RequestID string `json:"request_id"`
Data CertOutput `json:"data"`
}
func (c *CertResponse) JSONString() string {
if c == nil {
return ""
}
jsonByte, err := json.Marshal(c)
if err != nil {
return ""
}
return string(jsonByte)
// return c.RequestID
}
type CertOutput struct {
Policy string `json:"policy"`
Signature string `json:"signature"`
UploadDir string `json:"upload_dir"`
UploadHost string `json:"upload_host"`
ExpireInSeconds int `json:"expire_in_seconds"`
MaxFileSizeMB int `json:"max_file_size_mb"`
CapacityLimitMB int `json:"capacity_limit_mb"`
OSSAccessKeyID string `json:"oss_access_key_id"`
XOSSObjectACL string `json:"x_oss_object_acl"`
XOSSForbidOverwrite string `json:"x_oss_forbid_overwrite"`
}
// url: https://dashscope.aliyuncs.com/api/v1/uploads
// header: {'user-agent': 'dashscope/1.14.0; python/3.11.6; platform/macOS-14.2.1-arm64-arm-64bit; processor/arm', 'Authorization': 'Bearer sk-xxxx', 'Accept': 'application/json'}
// params: {'action': 'getPolicy', 'model': 'qwen-vl-plus'}
// timeout: 300
func getUploadCertificate(ctx context.Context, model, apiKey string) (*CertResponse, error) {
url := DashScopeBaseURL + "/v1/uploads"
header := map[string]string{
"Authorization": "Bearer " + apiKey,
"Accept": "application/json",
}
params := map[string]string{
"action": "getPolicy",
"model": model,
}
headerOpt := httpclient.WithHeader(header)
timeoutOpt := httpclient.WithTimeout(3 * time.Second)
cli := httpclient.NewHTTPClient()
respData := &CertResponse{}
err := cli.Get(ctx, url, params, respData, headerOpt, timeoutOpt)
if err != nil {
// panic(err)
return nil, &WrapMessageError{Message: "certificate Error", Cause: err}
}
return respData, nil
}
type UploadRequest struct {
File []byte `json:"file"`
}
// TODO:...
// if has upload resource set headers['X-DashScope-OssResourceResolve'] = 'enable'
/*
elem["image"] = ossURL 把原始的local_path 替换成 ossURL
"content": [
{"image": ""},
{"text": ""},
]
*/
// uploading local image to aliyun oss, a oss url will be returned.
func UploadLocalFile(ctx context.Context, filePath, model, apiKey string) (string, error) {
fileBytes, mimeType, err := loadLocalFileWithMimeType(filePath)
if err != nil {
return "", err
}
fileName := filepath.Base(filePath)
return uploadFIle(ctx, fileBytes, fileName, mimeType, model, apiKey)
}
// download and uploading a online image to aliyun oss, a oss url will be returned.
func UploadFileFromURL(ctx context.Context, fileURL, model, apiKey string) (string, error) {
fileBytes, mimeType, err := downloadFileWithMimeType(fileURL)
if err != nil {
return "", err
}
fileName := filepath.Base(fileURL)
return uploadFIle(ctx, fileBytes, fileName, mimeType, model, apiKey)
}
func uploadFIle(ctx context.Context, fileBytes []byte, fileName, mimeType, model, apiKey string) (string, error) {
certInfo, err := getUploadCertificate(ctx, model, apiKey)
if err != nil {
return "", &WrapMessageError{Message: "upload Cert Error", Cause: err}
}
ossKey := certInfo.Data.UploadDir + "/" + fileName
formData := buildFormData(certInfo.Data, ossKey, mimeType)
header := getUploadHeaders()
uploadReq, extraHeaders, err := buildMultiPartRequest(fileName, formData, fileBytes)
if err != nil {
return "", err
}
for k, v := range extraHeaders {
header[k] = v
}
cli := httpclient.NewHTTPClient()
headerOpt := httpclient.WithHeader(header)
err = cli.Post(ctx, certInfo.Data.UploadHost, uploadReq, nil, headerOpt)
if err != nil {
return "", &httpclient.HTTPRequestError{Message: "upload image Error", Cause: err}
}
ossURL := "oss://" + ossKey
return ossURL, nil
}
func getUploadHeaders() map[string]string {
headers := make(map[string]string)
// headers["User-Agent"] = getUserAgent() // Assuming you have a function getUserAgent()
headers["Accept"] = "application/json"
headers["Date"] = time.Now().Format(time.RFC1123) // Go's equivalent to Python's format_date_time(mktime(datetime.now().timetuple()))
return headers
}
func loadLocalFileWithMimeType(filePath string) ([]byte, string, error) {
imgBytes, err := os.ReadFile(filePath)
if err != nil {
return nil, "", &WrapMessageError{Message: "read file Error", Cause: err}
}
mt, err := mimetype.DetectFile(filePath)
if err != nil {
return nil, "", &WrapMessageError{Message: "detectfile mimetype Error", Cause: err}
}
return imgBytes, mt.String(), nil
}
func downloadFileWithMimeType(url string) ([]byte, string, error) {
resp, err := http.Get(url) //nolint:all
if err != nil {
return nil, "", &WrapMessageError{Message: "http get image Error", Cause: err}
}
defer resp.Body.Close()
imgBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, "", &WrapMessageError{Message: "read image Error", Cause: err}
}
mt, err := mimetype.DetectReader(bytes.NewReader(imgBytes))
if err != nil {
return nil, "", &WrapMessageError{Message: "detect image mimetype Error", Cause: err}
}
return imgBytes, mt.String(), nil
}
// build request body for upload file.
func buildMultiPartRequest(fileName string, formData map[string]string, fileBytes []byte) (*bytes.Buffer, map[string]string, error) {
// buffer to save multipart body
requestBody := new(bytes.Buffer)
writer := multipart.NewWriter(requestBody)
// write form data to buffer
for key, value := range formData {
_ = writer.WriteField(key, value)
}
part, err := writer.CreateFormFile("file", fileName)
if err != nil {
return requestBody, nil, &WrapMessageError{Message: "create form file Error", Cause: err}
}
// write file bytes to buffer
_, err = part.Write(fileBytes)
if err != nil {
return requestBody, nil, &WrapMessageError{Message: "write Error", Cause: err}
}
// Content-Type header content
contentType := writer.FormDataContentType()
err = writer.Close()
if err != nil {
return requestBody, nil, &WrapMessageError{Message: "close writer Error", Cause: err}
}
header := make(map[string]string)
header["Content-Type"] = contentType
return requestBody, header, nil
}
func buildFormData(certOutput CertOutput, ossKey, mimeType string) map[string]string {
formData := make(map[string]string)
formData["OSSAccessKeyId"] = certOutput.OSSAccessKeyID
formData["Signature"] = certOutput.Signature
formData["policy"] = certOutput.Policy
formData["key"] = ossKey
formData["x-oss-object-acl"] = certOutput.XOSSObjectACL
formData["x-oss-forbid-overwrite"] = certOutput.XOSSForbidOverwrite
formData["success_action_status"] = "200"
formData["x-oss-content-type"] = mimeType
return formData
}
+68
View File
@@ -0,0 +1,68 @@
package qwen
import (
"context"
"os"
"testing"
"github.com/stretchr/testify/require"
)
func getAPIKey(t *testing.T) string {
t.Helper()
apikey := os.Getenv("DASHSCOPE_API_KEY")
if apikey == "" {
t.Skip("token is empty")
}
return apikey
}
func TestGetUploadCertificate(t *testing.T) {
t.Parallel()
apiKey := getAPIKey(t)
ctx := context.TODO()
modelCases := []string{
QwenVLPlus,
QwenAudioTurbo,
}
for _, model := range modelCases {
resp, err := getUploadCertificate(ctx, model, apiKey)
require.NoError(t, err)
require.NotNil(t, resp)
}
}
// // this local file is not exist for other user
// func TestUploadingLocalImg(t *testing.T) {
// t.Parallel()
// ctx := context.TODO()
// homePath := os.Getenv("HOME")
// // ossFilePath, err := UploadLocalFile(ctx, homePath+"/Downloads/dog_and_girl.jpeg", QwenVLPlus, os.Getenv("DASHSCOPE_API_KEY"))
// ossFilePath, err := UploadLocalFile(ctx, homePath+"/Desktop/hello_world_female2.wav", QwenAudioTurbo, os.Getenv("DASHSCOPE_API_KEY"))
// t.Log(ossFilePath)
// require.NoError(t, err)
// require.NotEmpty(t, ossFilePath)
// }
func TestUploadingImageFromURL(t *testing.T) {
t.Parallel()
apiKey := getAPIKey(t)
// network problem...
// testImgURL := "https://github.com/eswulei/dashscope-go/blob/main/docs/static/img/parrot-icon.png"
testImgURL := "https://pic.ntimg.cn/20140113/8800276_184351657000_2.jpg"
ctx := context.TODO()
ossFilePath, err := UploadFileFromURL(ctx, testImgURL, "qwen-vl-plus", apiKey)
require.NoError(t, err)
require.NotEmpty(t, ossFilePath)
}
+220
View File
@@ -0,0 +1,220 @@
package dashscopego
import (
"bufio"
"context"
"log"
"strings"
embedding "github.com/casibase/dashscope-go-sdk/embedding"
httpclient "github.com/casibase/dashscope-go-sdk/httpclient"
"github.com/casibase/dashscope-go-sdk/paraformer"
"github.com/casibase/dashscope-go-sdk/qwen"
"github.com/casibase/dashscope-go-sdk/wanx"
)
type TongyiClient struct {
Model string
token string
httpCli httpclient.IHttpClient
}
func NewTongyiClient(model string, token string) *TongyiClient {
httpcli := httpclient.NewHTTPClient()
return newTongyiCLientWithHTTPCli(model, token, httpcli)
}
func newTongyiCLientWithHTTPCli(model string, token string, httpcli httpclient.IHttpClient) *TongyiClient {
return &TongyiClient{
Model: model,
httpCli: httpcli,
token: token,
}
}
// duplicate: CreateCompletion and CreateVLCompletion are the same but with different payload types.
// maybe this can be change in the future.
//
// nolint:lll
func (q *TongyiClient) CreateCompletion(ctx context.Context, payload *qwen.Request[*qwen.TextContent]) (*TextQwenResponse, error) {
payload = paylosdPreCheck(q, payload)
return genericCompletion(ctx, payload, q.httpCli, qwen.URLQwen(), q.token)
}
//nolint:lll
func (q *TongyiClient) CreateVLCompletion(ctx context.Context, payload *qwen.Request[*qwen.VLContentList]) (*VLQwenResponse, error) {
payload = paylosdPreCheck(q, payload)
for _, vMsg := range payload.Input.Messages {
tmpImageContent, hasImg := vMsg.Content.PopImageContent()
if hasImg && vMsg.Role == "user" {
filepath := tmpImageContent.Image
ossURL, hasUploadOss, err := checkIfNeedUploadFile(ctx, filepath, payload.Model, q.token)
if err != nil {
return nil, err
}
if hasUploadOss {
payload.HasUploadOss = true
}
vMsg.Content.SetImage(ossURL)
}
}
return genericCompletion(ctx, payload, q.httpCli, qwen.URLQwenVL(), q.token)
}
//nolint:lll
func (q *TongyiClient) CreateAudioCompletion(ctx context.Context, payload *qwen.Request[*qwen.AudioContentList]) (*AudioQwenResponse, error) {
payload = paylosdPreCheck(q, payload)
for _, acMsg := range payload.Input.Messages {
tmpAudioContent, hasAudio := acMsg.Content.PopAudioContent()
if hasAudio && acMsg.Role == "user" {
filepath := tmpAudioContent.Audio
ossURL, hasUploadOss, err := checkIfNeedUploadFile(ctx, filepath, payload.Model, q.token)
if err != nil {
return nil, err
}
if hasUploadOss {
payload.HasUploadOss = true
}
acMsg.Content.SetAudio(ossURL)
}
}
return genericCompletion(ctx, payload, q.httpCli, qwen.URLQwenVL(), q.token)
}
func checkIfNeedUploadFile(ctx context.Context, filepath string, model, token string) (string, bool, error) {
var err error
var ossURL string
var hasUploadOss bool
switch {
case strings.Contains(filepath, "dashscope.oss"):
// 使用了官方案例中的格式(https://dashscope.oss...).
ossURL = filepath
case strings.HasPrefix(filepath, "oss://"):
// 已经在 oss 中的不必上传.
ossURL = filepath
case strings.HasPrefix(filepath, "file://"):
// 本地文件.
filepath = strings.TrimPrefix(filepath, "file://")
ossURL, err = qwen.UploadLocalFile(ctx, filepath, model, token)
hasUploadOss = true
case strings.HasPrefix(filepath, "https://") || strings.HasPrefix(filepath, "http://"):
// 文件的 URL 链接.
ossURL, err = qwen.UploadFileFromURL(ctx, filepath, model, token)
hasUploadOss = true
}
return ossURL, hasUploadOss, err
}
//nolint:lll
func genericCompletion[T qwen.IQwenContent](ctx context.Context, payload *qwen.Request[T], httpcli httpclient.IHttpClient, url, token string) (*qwen.OutputResponse[T], error) {
if payload.Model == "" {
return nil, ErrModelNotSet
}
// use streaming if streaming func is set
if payload.StreamingFn != nil {
payload.Parameters.SetIncrementalOutput(true)
return qwen.SendMessageStream(ctx, payload, httpcli, url, token)
}
return qwen.SendMessage(ctx, payload, httpcli, url, token)
}
// TODO: intergrate wanx.Request into qwen.IQwenContent(or should rename to ITongyiContent)
//
//nolint:lll
func (q *TongyiClient) CreateImageGeneration(ctx context.Context, payload *wanx.ImageSynthesisRequest) ([]*wanx.ImgBlob, error) {
if payload.Model == "" {
if q.Model == "" {
return nil, ErrModelNotSet
}
payload.Model = q.Model
}
return wanx.CreateImageGeneration(ctx, payload, q.httpCli, q.token)
}
/*
func (q *TongyiClient) CreateVoiceFileToTextGeneration(ctx context.Context, request *paraformer.Request) (any, error) {
if request.Payload.Model == "" {
if q.Model == "" {
return nil, ErrModelNotSet
}
request.Payload.Model = q.Model
}
return
panic("not implemented")
}
*/
func (q *TongyiClient) CreateSpeechToTextGeneration(ctx context.Context, request *paraformer.Request, reader *bufio.Reader) error {
if request.Payload.Model == "" {
if q.Model == "" {
return ErrModelNotSet
}
request.Payload.Model = q.Model
}
wsCli, err := paraformer.ConnRecognitionClient(request, q.token)
if err != nil {
return err
}
// handle response by stream callback
go paraformer.HandleRecognitionResult(ctx, wsCli, request.StreamingFn)
for {
// this buf can not be reused,
// otherwise the data will be overwritten, voice became disorder.
buf := make([]byte, 1024)
n, errRead := reader.Read(buf)
if n == 0 {
break
}
if errRead != nil {
log.Printf("read line error: %v\n", errRead)
err = errRead
return err
}
paraformer.SendRadioData(wsCli, buf)
}
return nil
}
func (q *TongyiClient) CreateEmbedding(ctx context.Context, r *embedding.Request) ([][]float32, error) {
resp, err := embedding.CreateEmbedding(ctx, r, q.httpCli, q.token)
if err != nil {
return nil, err
}
if len(resp.Output.Embeddings) == 0 {
return nil, ErrEmptyResponse
}
embeddings := make([][]float32, 0)
for i := 0; i < len(resp.Output.Embeddings); i++ {
embeddings = append(embeddings, resp.Output.Embeddings[i].Embedding)
}
return embeddings, nil
}
func paylosdPreCheck[T qwen.IQwenContent](q *TongyiClient, payload *qwen.Request[T]) *qwen.Request[T] {
if payload.Model == "" {
payload.Model = q.Model
}
if payload.Parameters == nil {
payload.Parameters = qwen.DefaultParameters()
}
return payload
}
+381
View File
@@ -0,0 +1,381 @@
package dashscopego
import (
"context"
"os"
"strings"
"testing"
httpclient "github.com/casibase/dashscope-go-sdk/httpclient"
qwen "github.com/casibase/dashscope-go-sdk/qwen"
wanx "github.com/casibase/dashscope-go-sdk/wanx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func newTongyiClient(t *testing.T, model string) *TongyiClient {
t.Helper()
token := os.Getenv("DASHSCOPE_API_KEY")
cli := NewTongyiClient(model, token)
if cli.token == "" {
t.Skip("token is empty")
}
return cli
}
func newMockClient(t *testing.T, model string, ctrl *gomock.Controller, f mockFn) *TongyiClient {
t.Helper()
mockHTTPCli := httpclient.NewMockIHttpClient(ctrl)
fackToken := ""
f(mockHTTPCli)
qwenCli := newTongyiCLientWithHTTPCli(model, fackToken, mockHTTPCli)
return qwenCli
}
type mockFn func(mockHTTPCli *httpclient.MockIHttpClient)
func TestBasic(t *testing.T) {
t.Parallel()
ctx := context.TODO()
cli := newTongyiClient(t, "")
text := qwen.TextContent{Text: "Hello"}
input := TextInput{
Messages: []TextMessage{
{Role: "user", Content: &text},
},
}
req := &TextRequest{
Model: "qwen-turbo",
Input: input,
}
resp, err := cli.CreateCompletion(ctx, req)
require.NoError(t, err)
assert.Regexp(t, "hello|hi|how|assist", resp.Output.Choices[0].Message.Content.ToString())
}
func TestStreamingChunk(t *testing.T) {
t.Parallel()
ctx := context.TODO()
cli := newTongyiClient(t, "qwen-turbo")
content := qwen.TextContent{Text: "Hello"}
input := TextInput{
Messages: []TextMessage{
{Role: "user", Content: &content},
},
}
output := ""
streamCallbackFn := func(ctx context.Context, chunk []byte) error {
output += string(chunk)
return nil
}
req := &TextRequest{
// Model: "qwen-turbo",
Input: input,
StreamingFn: streamCallbackFn,
}
resp, err := cli.CreateCompletion(ctx, req)
require.NoError(t, err)
assert.Regexp(t, "hello|hi|how|assist", resp.Output.Choices[0].Message.Content.ToString())
assert.Regexp(t, "hello|hi|how|assist", output)
}
func TestVLBasic(t *testing.T) {
t.Parallel()
ctx := context.TODO()
cli := newTongyiClient(t, "")
sysContent := qwen.VLContentList{
{
Text: "You are a helpful assistant.",
},
}
userContent := qwen.VLContentList{
{
Text: "describe the image",
},
{
Image: "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg",
},
}
input := VLInput{
Messages: []VLMessage{
{Role: "system", Content: &sysContent},
{Role: "user", Content: &userContent},
},
}
req := &VLRequest{
Model: "qwen-vl-plus",
Input: input,
}
req.Parameters = qwen.DefaultParameters()
resp, err := cli.CreateVLCompletion(ctx, req)
require.NoError(t, err)
assert.Regexp(t, "dog|person|individual|woman|girl", resp.Output.Choices[0].Message.Content.ToString())
}
func TestVLStreamChund(t *testing.T) {
t.Parallel()
ctx := context.TODO()
cli := newTongyiClient(t, "")
sysContent := qwen.VLContentList{
{
Text: "You are a helpful assistant.",
},
}
userContent := qwen.VLContentList{
{
Text: "describe the image",
},
{
Image: "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg",
// Image: "https://pic.ntimg.cn/20140113/8800276_184351657000_2.jpg",
},
}
input := VLInput{
Messages: []VLMessage{
{Role: "system", Content: &sysContent},
{Role: "user", Content: &userContent},
},
}
output := ""
streamCallbackFn := func(ctx context.Context, chunk []byte) error {
output += string(chunk)
return nil
}
req := &VLRequest{
Model: "qwen-vl-plus",
Input: input,
StreamingFn: streamCallbackFn,
}
resp, err := cli.CreateVLCompletion(ctx, req)
require.NoError(t, err)
assert.Equal(t, output, resp.Output.Choices[0].Message.Content.ToString())
assert.Regexp(t, "dog|person|individual|woman|girl", strings.ToLower(output))
}
func TestImageGeneration(t *testing.T) {
t.Parallel()
ctx := context.TODO()
cli := newTongyiClient(t, "wanx-v1")
req := &wanx.ImageSynthesisRequest{
Model: "wanx-v1",
Input: wanx.ImageSynthesisInput{
Prompt: "A beautiful sunset",
},
Download: true,
}
imgBlobs, err := cli.CreateImageGeneration(ctx, req)
require.NoError(t, err)
require.NotEmpty(t, imgBlobs)
for _, blob := range imgBlobs {
assert.NotEmpty(t, blob.Data)
assert.NotEmpty(t, blob.ImgURL)
assert.Equal(t, "image/png", blob.ImgType)
}
}
func TestMockStreamingChunk(t *testing.T) {
t.Parallel()
ctx := context.TODO()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := newMockClient(t, "qwen-turbo", ctrl, _mockAsyncFunc)
output := ""
text := qwen.TextContent{Text: "Hello"}
input := TextInput{
Messages: []TextMessage{
{Role: "user", Content: &text},
},
}
req := &TextRequest{
Input: input,
StreamingFn: func(ctx context.Context, chunk []byte) error {
output += string(chunk)
return nil
},
}
// mockURL := ""
resp, err := cli.CreateCompletion(ctx, req)
require.NoError(t, err)
assert.Equal(t, "Hello! How can I assist you today?", resp.Output.Choices[0].Message.Content.ToString())
assert.Equal(t, "Hello! How can I assist you today?", output)
}
func TestMockBasic(t *testing.T) {
t.Parallel()
ctx := context.TODO()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := newMockClient(t, "qwen-turbo", ctrl, _mockSyncFunc)
text := qwen.TextContent{Text: "Hello"}
input := TextInput{
Messages: []TextMessage{
{Role: "user", Content: &text},
},
}
req := &TextRequest{
Input: input,
}
resp, err := cli.CreateCompletion(ctx, req)
require.NoError(t, err)
assert.Equal(t, "Hello! This is a mock message.", resp.Output.Choices[0].Message.Content.ToString())
assert.Equal(t, "mock-ac55-9fd3-8326-8415cbdf5683", resp.RequestID)
assert.Equal(t, 15, resp.Usage.TotalTokens)
}
func _mockAsyncFunc(mockHTTPCli *httpclient.MockIHttpClient) {
MockStreamData := []string{
`id:1`,
`event:result`,
`:HTTP_STATUS/200`,
`data:{
"output": {
"choices": [{
"message": {
"content": "Hello! How",
"role": "assistant"
},
"finish_reason": "null"
}]
},
"usage": {
"total_tokens": 9,
"input_tokens": 6,
"output_tokens": 3
},
"request_id": "95bea986-ac55-9fd3-8326-8415cbdf5683"
}`,
` `,
`id:2`,
`event:result`,
`:HTTP_STATUS/200`,
`data:{
"output": {
"choices": [{
"message": {
"content": " can I assist you today?",
"role": "assistant"
},
"finish_reason": "null"
}]
},
"usage": {
"total_tokens": 15,
"input_tokens": 6,
"output_tokens": 9
},
"request_id": "95bea986-ac55-9fd3-8326-8415cbdf5683"
}`,
` `,
`id:3`,
`event:result`,
`:HTTP_STATUS/200`,
`data:{
"output": {
"choices": [{
"message": {
"content": "",
"role": "assistant"
},
"finish_reason": "stop"
}]
},
"usage": {
"total_tokens": 15,
"input_tokens": 6,
"output_tokens": 9
},
"request_id": "95bea986-ac55-9fd3-8326-8415cbdf5683"
}`,
}
ctx := context.TODO()
_rawStreamOutChannel := make(chan string, 500)
mockHTTPCli.EXPECT().
PostSSE(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Return(_rawStreamOutChannel, nil)
go func() {
for _, line := range MockStreamData {
_rawStreamOutChannel <- line
}
close(_rawStreamOutChannel)
}()
}
func _mockSyncFunc(mockHTTPCli *httpclient.MockIHttpClient) {
ctx := context.TODO()
text := qwen.TextContent{Text: "Hello! This is a mock message."}
mockResp := TextQwenResponse{
Output: qwen.Output[*qwen.TextContent]{
Choices: []qwen.Choice[*qwen.TextContent]{
{
Message: TextMessage{
Content: &text,
Role: "assistant",
},
FinishReason: "stop",
},
},
},
Usage: struct {
TotalTokens int `json:"total_tokens"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
}{
TotalTokens: 15,
InputTokens: 6,
OutputTokens: 9,
},
RequestID: "mock-ac55-9fd3-8326-8415cbdf5683",
}
mockHTTPCli.EXPECT().
Post(ctx, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
SetArg(3, mockResp).
Return(nil)
}
+93
View File
@@ -0,0 +1,93 @@
package wanx
type ImageSynthesisParams struct {
/*
The style of the output image, currently supports the following style values:
"<auto>" default,
"<3d cartoon>" 3D cartoon,
"<anime>" animation,
"<oil painting>" oil painting,
"<watercolor>" watercolor,
"<sketch>" sketch,
"<chinese painting>" Chinese painting,
"<flat illustration>" flat illustration,
*/
Style string `json:"style,omitempty"`
/*
The resolution of the generated image,
currently only supports '1024*1024', '720*1280', '1280*720' three resolutions,
default is 1024*1024 pixels.
*/
Size string `json:"size,omitempty"`
// The number of images generated, currently supports 1~4, default is 1.
N int `json:"n,omitempty"`
// seed.
Seed int `json:"seed,omitempty"`
}
type TaskStatus string
const (
TaskSucceeded TaskStatus = "SUCCEEDED"
TaskFailed TaskStatus = "FAILED"
TaskCanceled TaskStatus = "CANCELED"
TaskPending TaskStatus = "PENDING"
TaskSuspended TaskStatus = "SUSPENDED"
TaskRunning TaskStatus = "RUNNING"
)
type ImageSynthesisInput struct {
Prompt string `json:"prompt"`
NegativePromp string `json:"negative_promp,omitempty"`
}
type ImageSynthesisRequest struct {
Model string `json:"model"`
Input ImageSynthesisInput `json:"input"`
Params ImageSynthesisParams `json:"parameters"`
Download bool `json:"-"`
}
type Output struct {
TaskID string `json:"task_id"`
TaskStatus string `json:"task_status"`
Results []struct {
URL string `json:"url"`
} `json:"results"`
TaskMetrics struct {
Total int `json:"TOTAL"`
Succeeded int `json:"SUCCEEDED"`
Failed int `json:"FAILED"`
} `json:"task_metrics"`
}
type Usage struct {
ImageCount int `json:"image_count"`
}
type ImageResponse struct {
StatusCode int `json:"status_code"`
RequestID string `json:"request_id"`
Code string `json:"code"`
Message string `json:"message"`
Output Output `json:"output"`
Usage Usage `json:"usage"`
}
type ImgBlob struct {
// types include: "image/png".
ImgType string `json:"img_type"`
ImgURL string `json:"img_url"`
// Raw bytes for media formats.
Data []byte `json:"-"`
}
type TaskRequest struct {
TaskID string `json:"task_id"`
}
type TaskResponse struct {
RequestID string `json:"request_id"`
Output Output `json:"output"`
Usage Usage `json:"usage"`
}
+25
View File
@@ -0,0 +1,25 @@
package wanx
import "fmt"
const (
DashScopeBaseURL = "https://dashscope.aliyuncs.com"
ImageSynthesisURI = "/api/v1/services/aigc/text2image/image-synthesis"
TaskURI = "/api/v1/tasks/%s"
)
type ModelWanx = string
const (
WanxV1 ModelWanx = "wanx-v1"
WanxStyleRepaintV1 ModelWanx = "wanx-style-repaint-v1"
WanxBgGenV2 ModelWanx = "wanx-background-generation-v2"
)
func ImageSynthesisURL() string {
return DashScopeBaseURL + ImageSynthesisURI
}
func TaskURL(taskID string) string {
return DashScopeBaseURL + fmt.Sprintf(TaskURI, taskID)
}
+130
View File
@@ -0,0 +1,130 @@
package wanx
import (
"context"
"errors"
"log"
"time"
httpclient "github.com/casibase/dashscope-go-sdk/httpclient"
)
var (
ErrEmptyResponse = errors.New("empty response")
ErrEmptyTaskID = errors.New("task id is empty")
ErrTaskUnsuccess = errors.New("task is not success")
ErrModelNotSet = errors.New("model is not set")
)
//nolint:lll
func CreateImageGeneration(ctx context.Context, payload *ImageSynthesisRequest, httpcli httpclient.IHttpClient, token string) ([]*ImgBlob, error) {
header := map[string]string{
"Content-Type": "application/json",
}
tokenOpt := httpclient.WithTokenHeaderOption(token)
headerOpt := httpclient.WithHeader(header)
resp, err := SyncCall(ctx, payload, httpcli, tokenOpt, headerOpt)
if err != nil {
return nil, err
}
blobList := make([]*ImgBlob, 0, len(resp.Results))
for _, img := range resp.Results {
tmpImgBlob := &ImgBlob{
ImgType: "image/png",
ImgURL: img.URL,
}
if payload.Download {
imgByte, err := httpcli.GetImage(ctx, img.URL, tokenOpt)
if err != nil {
return nil, err
}
tmpImgBlob.Data = imgByte
}
blobList = append(blobList, tmpImgBlob)
}
return blobList, nil
}
// tongyi-wanx-api only support AsyncCall, so we need to warp it to be Sync.
//
//nolint:lll
func SyncCall(ctx context.Context, req *ImageSynthesisRequest, httpcli httpclient.IHttpClient, options ...httpclient.HTTPOption) (*Output, error) {
rsp, err := AsyncCall(ctx, req, httpcli, options...)
if err != nil {
return nil, err
}
currentTaskStatus := TaskStatus(rsp.Output.TaskStatus)
taskID := rsp.Output.TaskID
if taskID == "" {
return nil, ErrEmptyTaskID
}
taskReq := TaskRequest{TaskID: taskID}
taskResp := &TaskResponse{}
for currentTaskStatus == TaskPending ||
currentTaskStatus == TaskRunning ||
currentTaskStatus == TaskSuspended {
delayDurationToCheckStatus := 800
log.Println("TaskStatus: ", currentTaskStatus)
time.Sleep(time.Duration(delayDurationToCheckStatus) * time.Millisecond)
// log.Println("TaskStatus: ", currentTaskStatus)
taskResp, err = CheckTaskStatus(ctx, &taskReq, httpcli, options...)
if err != nil {
return nil, err
}
currentTaskStatus = TaskStatus(taskResp.Output.TaskStatus)
}
if currentTaskStatus == TaskFailed ||
currentTaskStatus == TaskCanceled {
return nil, ErrTaskUnsuccess
}
if len(taskResp.Output.Results) == 0 {
return nil, ErrEmptyResponse
}
return &taskResp.Output, nil
}
// calling tongyi-wanx-api to get image-generation async task id.
//
//nolint:lll
func AsyncCall(ctx context.Context, req *ImageSynthesisRequest, httpcli httpclient.IHttpClient, options ...httpclient.HTTPOption) (*ImageResponse, error) {
header := map[string]string{"X-DashScope-Async": "enable"}
headerOpt := httpclient.WithHeader(header)
options = append(options, headerOpt)
if req.Model == "" {
return nil, ErrModelNotSet
}
resp := ImageResponse{}
err := httpcli.Post(ctx, ImageSynthesisURL(), req, &resp, options...)
if err != nil {
return nil, err
}
return &resp, nil
}
//nolint:lll
func CheckTaskStatus(ctx context.Context, req *TaskRequest, httpcli httpclient.IHttpClient, options ...httpclient.HTTPOption) (*TaskResponse, error) {
resp := TaskResponse{}
err := httpcli.Get(ctx, TaskURL(req.TaskID), nil, &resp, options...)
if err != nil {
return nil, err
}
return &resp, nil
}