Files

221 lines
6.1 KiB
Go

package dashscopego
import (
"bufio"
"context"
"log"
"strings"
embedding "github.com/hanzoai/dashscope-go-sdk/embedding"
httpclient "github.com/hanzoai/dashscope-go-sdk/httpclient"
"github.com/hanzoai/dashscope-go-sdk/paraformer"
"github.com/hanzoai/dashscope-go-sdk/qwen"
"github.com/hanzoai/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
}