mirror of
https://github.com/hanzoai/dashscope-go-sdk.git
synced 2026-09-30 21:39:25 +00:00
feat: remove log print (#1)
This commit is contained in:
@@ -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.
|
||||
@@ -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 .
|
||||
@@ -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 := ¶former.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
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
package dashscopego
|
||||
|
||||
const (
|
||||
DashscopeTokenEnvName = "DASHSCOPE_API_KEY" //nolint:gosec
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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://")
|
||||
)
|
||||
@@ -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 := ¶former.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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user