2026-06-19 21:58:17 +08:00
|
|
|
|
package eino
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
openaiImpl "github.com/cloudwego/eino-ext/components/model/openai"
|
|
|
|
|
|
"github.com/cloudwego/eino/compose"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/hhs/camtalk/internal/ai/stt"
|
|
|
|
|
|
"github.com/hhs/camtalk/internal/ai/tts"
|
|
|
|
|
|
"github.com/hhs/camtalk/internal/config"
|
|
|
|
|
|
"github.com/hhs/camtalk/internal/logger"
|
|
|
|
|
|
"github.com/hhs/camtalk/internal/models"
|
|
|
|
|
|
"github.com/hhs/camtalk/internal/session"
|
2026-06-21 15:38:28 +08:00
|
|
|
|
"github.com/hhs/camtalk/internal/store"
|
2026-06-19 21:58:17 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
nodeSTT = "stt"
|
|
|
|
|
|
nodeHistory = "history"
|
2026-06-19 23:24:01 +08:00
|
|
|
|
nodeLLM = "llm"
|
|
|
|
|
|
nodeMessageToString = "msg2str"
|
|
|
|
|
|
nodeSplitter = "splitter"
|
2026-06-19 21:58:17 +08:00
|
|
|
|
nodeTTS = "tts"
|
|
|
|
|
|
nodeDone = "done"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// PipelineGraph 封装编译后的 Eino Graph。
|
|
|
|
|
|
type PipelineGraph struct {
|
|
|
|
|
|
Runnable compose.Runnable[PipelineInput, PipelineOutput]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewPipelineGraph 构建 CamTalk AI 编排 Graph。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 拓扑:START → STT → History → ChatModel → Splitter → TTS → Done → END
|
|
|
|
|
|
//
|
|
|
|
|
|
// Graph 使用 Stream 模式调用,ChatModel 实现真正的 token 级流式输出。
|
|
|
|
|
|
// LLM token 通过 Callback 的 OnEndWithStreamOutput 实时推送到客户端。
|
|
|
|
|
|
func NewPipelineGraph(
|
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
|
cfg *config.Config,
|
|
|
|
|
|
sttService stt.Service,
|
|
|
|
|
|
ttsService tts.Service,
|
|
|
|
|
|
sessionMgr session.Manager,
|
2026-06-21 15:38:28 +08:00
|
|
|
|
scenarioRepo store.UserScenarioRepository,
|
2026-06-19 21:58:17 +08:00
|
|
|
|
) (*PipelineGraph, error) {
|
|
|
|
|
|
log := logger.Log
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 创建 eino-ext ChatModel(对接 DashScope OpenAI 兼容接口)
|
|
|
|
|
|
chatModel, err := openaiImpl.NewChatModel(ctx, &openaiImpl.ChatModelConfig{
|
|
|
|
|
|
APIKey: cfg.AI.LLM.APIKey,
|
|
|
|
|
|
Model: cfg.AI.LLM.Model,
|
|
|
|
|
|
BaseURL: cfg.AI.LLM.Endpoint,
|
|
|
|
|
|
Timeout: time.Duration(cfg.AI.LLM.Timeout) * time.Second,
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
log.Infow("Eino ChatModel 初始化成功",
|
|
|
|
|
|
"model", cfg.AI.LLM.Model,
|
|
|
|
|
|
"endpoint", cfg.AI.LLM.Endpoint)
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 构建 Graph(值类型,非指针)
|
|
|
|
|
|
g := compose.NewGraph[PipelineInput, PipelineOutput](
|
|
|
|
|
|
compose.WithGenLocalState(genLocalState),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 添加节点
|
|
|
|
|
|
maxHistory := cfg.Session.MaxHistory
|
|
|
|
|
|
|
|
|
|
|
|
_ = g.AddLambdaNode(nodeSTT, NewSTTLambda(sttService))
|
2026-06-21 15:38:28 +08:00
|
|
|
|
_ = g.AddLambdaNode(nodeHistory, NewHistoryLambda(sessionMgr.GetHistory, scenarioRepo, maxHistory))
|
2026-06-19 21:58:17 +08:00
|
|
|
|
_ = g.AddChatModelNode(nodeLLM, chatModel)
|
2026-06-19 23:24:01 +08:00
|
|
|
|
_ = g.AddLambdaNode(nodeMessageToString, NewMessageToStringLambda())
|
2026-06-19 21:58:17 +08:00
|
|
|
|
_ = g.AddLambdaNode(nodeSplitter, NewSplitterLambda())
|
|
|
|
|
|
_ = g.AddLambdaNode(nodeTTS, NewTTSLambda(
|
|
|
|
|
|
ttsService,
|
|
|
|
|
|
cfg.AI.TTS.Voice,
|
|
|
|
|
|
cfg.AI.TTS.Speed,
|
|
|
|
|
|
cfg.AI.TTS.OutputFormat,
|
|
|
|
|
|
cfg.AI.TTS.SampleRate,
|
|
|
|
|
|
))
|
|
|
|
|
|
_ = g.AddLambdaNode(nodeDone, NewDoneLambda(cfg.AI.LLM.Model))
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 连接边
|
|
|
|
|
|
_ = g.AddEdge(compose.START, nodeSTT)
|
|
|
|
|
|
_ = g.AddEdge(nodeSTT, nodeHistory)
|
|
|
|
|
|
_ = g.AddEdge(nodeHistory, nodeLLM)
|
2026-06-19 23:24:01 +08:00
|
|
|
|
_ = g.AddEdge(nodeLLM, nodeMessageToString)
|
|
|
|
|
|
_ = g.AddEdge(nodeMessageToString, nodeSplitter)
|
2026-06-19 21:58:17 +08:00
|
|
|
|
_ = g.AddEdge(nodeSplitter, nodeTTS)
|
|
|
|
|
|
_ = g.AddEdge(nodeTTS, nodeDone)
|
|
|
|
|
|
_ = g.AddEdge(nodeDone, compose.END)
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 编译(回调在运行时通过 Stream option 传入)
|
|
|
|
|
|
runnable, err := g.Compile(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log.Infow("Eino Graph 编译成功", "nodes", 6)
|
|
|
|
|
|
return &PipelineGraph{Runnable: runnable}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// buildPipelineInput 从 WebSocket 请求和会话配置构建 Graph 输入。
|
|
|
|
|
|
func buildPipelineInput(req models.WsQuery, sessionID string, sess *models.Session, audioData, imageData []byte) PipelineInput {
|
|
|
|
|
|
return PipelineInput{
|
|
|
|
|
|
AudioData: audioData,
|
|
|
|
|
|
ImageData: imageData,
|
|
|
|
|
|
Text: req.Text,
|
|
|
|
|
|
SessionID: sessionID,
|
|
|
|
|
|
RequestID: req.RequestID,
|
|
|
|
|
|
Language: sess.Config.Language,
|
|
|
|
|
|
Scenario: sess.Config.Scenario,
|
|
|
|
|
|
TTSEnabled: sess.Config.TTSEnabled,
|
2026-06-21 15:38:28 +08:00
|
|
|
|
UserID: sess.UserID,
|
2026-06-19 21:58:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|