Files
CamTalk/backend/internal/eino/graph.go

120 lines
3.5 KiB
Go
Raw Normal View History

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"
"github.com/hhs/camtalk/internal/store"
)
const (
nodeSTT = "stt"
nodeHistory = "history"
nodeLLM = "llm"
nodeMessageToString = "msg2str"
nodeSplitter = "splitter"
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,
scenarioRepo store.UserScenarioRepository,
) (*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))
_ = g.AddLambdaNode(nodeHistory, NewHistoryLambda(sessionMgr.GetHistory, scenarioRepo, maxHistory))
_ = g.AddChatModelNode(nodeLLM, chatModel)
_ = g.AddLambdaNode(nodeMessageToString, NewMessageToStringLambda())
_ = 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)
_ = g.AddEdge(nodeLLM, nodeMessageToString)
_ = g.AddEdge(nodeMessageToString, nodeSplitter)
_ = 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", 7)
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,
UserID: sess.UserID,
}
}