feat: 引入 Eino 框架并实现 AI 编排层基础设施与节点
- 引入 cloudwego/eino v0.9.9 和 eino-ext/components/model/openai v0.1.13 - 新增 internal/eino/ 包: - types.go: PipelineInput/Output、STTOutput、TokenUsage 类型定义 - state.go: PipelineState 跨节点状态收集(线程安全) - callback.go: ChatModel OnEndWithStreamOutput 回调,逐 token 推送 llm_chunk - nodes_stt.go: STT Lambda,支持文本/语音输入模式 - nodes_history.go: 历史组装 Lambda,含多模态图片支持 - nodes_splitter.go: 句子分割 Transform Lambda - nodes_tts.go: TTS Lambda,逐句合成推送音频 - nodes_done.go: Done Lambda,发送 llm_done 并追加历史 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
133
backend/internal/eino/callback.go
Normal file
133
backend/internal/eino/callback.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package eino
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/cloudwego/eino/callbacks"
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
callbacksHelper "github.com/cloudwego/eino/utils/callbacks"
|
||||
|
||||
"github.com/hhs/camtalk/internal/logger"
|
||||
"github.com/hhs/camtalk/internal/models"
|
||||
"github.com/hhs/camtalk/internal/orchestrator"
|
||||
)
|
||||
|
||||
// context key 类型,避免与其他包冲突。
|
||||
type ctxKeySender struct{}
|
||||
type ctxKeyRequestID struct{}
|
||||
type ctxKeyState struct{}
|
||||
|
||||
// WithSender 将 Sender 注入 context。
|
||||
func WithSender(ctx context.Context, sender orchestrator.Sender) context.Context {
|
||||
return context.WithValue(ctx, ctxKeySender{}, sender)
|
||||
}
|
||||
|
||||
// WithRequestID 将 requestID 注入 context。
|
||||
func WithRequestID(ctx context.Context, requestID string) context.Context {
|
||||
return context.WithValue(ctx, ctxKeyRequestID{}, requestID)
|
||||
}
|
||||
|
||||
// WithPipelineState 将 PipelineState 注入 context。
|
||||
func WithPipelineState(ctx context.Context, state *PipelineState) context.Context {
|
||||
return context.WithValue(ctx, ctxKeyState{}, state)
|
||||
}
|
||||
|
||||
// senderFromCtx 从 context 获取 Sender。
|
||||
func senderFromCtx(ctx context.Context) orchestrator.Sender {
|
||||
s, _ := ctx.Value(ctxKeySender{}).(orchestrator.Sender)
|
||||
return s
|
||||
}
|
||||
|
||||
// requestIDFromCtx 从 context 获取 requestID。
|
||||
func requestIDFromCtx(ctx context.Context) string {
|
||||
s, _ := ctx.Value(ctxKeyRequestID{}).(string)
|
||||
return s
|
||||
}
|
||||
|
||||
// stateFromCtx 从 context 获取 PipelineState。
|
||||
func stateFromCtx(ctx context.Context) *PipelineState {
|
||||
s, _ := ctx.Value(ctxKeyState{}).(*PipelineState)
|
||||
return s
|
||||
}
|
||||
|
||||
// BuildCallbackHandler 构建 Eino Callback Handler。
|
||||
//
|
||||
// 核心职责:ChatModel 节点通过 OnEndWithStreamOutput 逐 token 推送 llm_chunk 到客户端,
|
||||
// 同时累积完整文本到 PipelineState。
|
||||
//
|
||||
// 其他节点的消息推送(stt_result、tts_audio、llm_done)由各 Lambda 内部直接调用 Sender。
|
||||
func BuildCallbackHandler() callbacks.Handler {
|
||||
return callbacksHelper.NewHandlerHelper().
|
||||
ChatModel(&callbacksHelper.ModelCallbackHandler{
|
||||
OnEndWithStreamOutput: func(ctx context.Context, info *callbacks.RunInfo, output *schema.StreamReader[*model.CallbackOutput]) context.Context {
|
||||
log := logger.Log
|
||||
sender := senderFromCtx(ctx)
|
||||
requestID := requestIDFromCtx(ctx)
|
||||
state := stateFromCtx(ctx)
|
||||
|
||||
if sender == nil || requestID == "" {
|
||||
log.Warnw("ModelCallback: missing sender or request_id in context",
|
||||
"node", info.Name)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// 异步消费流,避免阻塞框架的下游处理。
|
||||
// 框架对流做了内部拷贝,此 goroutine 读取独立副本。
|
||||
go func() {
|
||||
defer output.Close()
|
||||
|
||||
for {
|
||||
chunk, err := output.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
log.Errorw("ModelCallback: stream recv error",
|
||||
"node", info.Name, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if chunk == nil || chunk.Message == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
delta := chunk.Message.Content
|
||||
if delta == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 推送 llm_chunk 到客户端
|
||||
if err := sender.SendLLMChunk(models.WsLLMChunk{
|
||||
Type: "llm_chunk",
|
||||
RequestID: requestID,
|
||||
Delta: delta,
|
||||
Role: "assistant",
|
||||
}); err != nil {
|
||||
log.Errorw("ModelCallback: send llm_chunk failed", "error", err)
|
||||
}
|
||||
|
||||
// 累积完整文本到 State
|
||||
if state != nil {
|
||||
state.AppendText(delta)
|
||||
}
|
||||
|
||||
// 记录 token 用量(流的最后一帧携带)
|
||||
if chunk.TokenUsage != nil && state != nil {
|
||||
state.mu.Lock()
|
||||
state.TokenUsage = &TokenUsage{
|
||||
Prompt: chunk.TokenUsage.PromptTokens,
|
||||
Completion: chunk.TokenUsage.CompletionTokens,
|
||||
Total: chunk.TokenUsage.TotalTokens,
|
||||
}
|
||||
state.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ctx
|
||||
},
|
||||
}).
|
||||
Handler()
|
||||
}
|
||||
Reference in New Issue
Block a user