- 引入 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>
134 lines
3.8 KiB
Go
134 lines
3.8 KiB
Go
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()
|
||
}
|