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/models" "github.com/hhs/camtalk/internal/orchestrator" "github.com/hhs/camtalk/internal/trace" ) // context key 类型,避免与其他包冲突。 type ctxKeySender 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(使用 trace 包)。 func WithRequestID(ctx context.Context, requestID string) context.Context { return trace.WithRequestID(ctx, 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(使用 trace 包)。 func requestIDFromCtx(ctx context.Context) string { return trace.GetRequestID(ctx) } // 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 := trace.FromContext(ctx) 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() }