Files
smartmate/backend/newAgent/stream/openai.go
Losita e1a06be768 Version: 0.8.5.dev.260330
后端:
1.把node/plan的具体逻辑做完了,没仔细看,进入下一步之前需要仔细review

前端:
无改动

全仓库:
无改动
2026-03-30 22:08:30 +08:00

320 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package newagentstream
import (
"encoding/json"
"github.com/cloudwego/eino/schema"
)
// OpenAIChunkResponse 是 OpenAI 兼容的流式 chunk DTO。
//
// 设计说明:
// 1. 外层继续保持 OpenAI 兼容壳,避免前端和调试工具一次性大改;
// 2. 新增顶层 Extra 字段,用来承载“工具调用 / 确认请求 / 中断恢复”等结构化事件;
// 3. 这样旧前端仍可继续读取 delta.content / delta.reasoning_content新前端则可渐进消费 extra。
type OpenAIChunkResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []OpenAIChunkChoice `json:"choices,omitempty"`
Extra *OpenAIChunkExtra `json:"extra,omitempty"`
}
// OpenAIChunkChoice 对应 OpenAI choices[0]。
type OpenAIChunkChoice struct {
Index int `json:"index"`
Delta OpenAIChunkDelta `json:"delta"`
FinishReason *string `json:"finish_reason"`
}
// OpenAIChunkDelta 是真正承载 role/content/reasoning 的位置。
type OpenAIChunkDelta struct {
Role string `json:"role,omitempty"`
Content string `json:"content,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
}
// StreamExtraKind 表示当前 chunk 在业务语义上属于哪类事件。
type StreamExtraKind string
const (
StreamExtraKindReasoningText StreamExtraKind = "reasoning_text"
StreamExtraKindAssistantText StreamExtraKind = "assistant_text"
StreamExtraKindStatus StreamExtraKind = "status"
StreamExtraKindToolCall StreamExtraKind = "tool_call"
StreamExtraKindToolResult StreamExtraKind = "tool_result"
StreamExtraKindConfirm StreamExtraKind = "confirm_request"
StreamExtraKindInterrupt StreamExtraKind = "interrupt"
StreamExtraKindFinish StreamExtraKind = "finish"
)
// StreamDisplayMode 表示前端更适合如何展示该结构化事件。
type StreamDisplayMode string
const (
StreamDisplayModeAppend StreamDisplayMode = "append"
StreamDisplayModeReplace StreamDisplayMode = "replace"
StreamDisplayModeCard StreamDisplayMode = "card"
)
// OpenAIChunkExtra 是挂在 OpenAI 兼容壳上的结构化扩展字段。
//
// 职责边界:
// 1. Kind / Stage / BlockID 提供前端排版和分组所需的最小元信息;
// 2. Status / Tool / Confirm / Interrupt 只存展示层真正需要的摘要,不直接耦合后端完整状态对象;
// 3. Meta 留给后续做灰度扩展,避免每加一种小字段都要立刻改 DTO 结构。
type OpenAIChunkExtra struct {
Kind StreamExtraKind `json:"kind,omitempty"`
BlockID string `json:"block_id,omitempty"`
Stage string `json:"stage,omitempty"`
DisplayMode StreamDisplayMode `json:"display_mode,omitempty"`
Status *StreamStatusExtra `json:"status,omitempty"`
Tool *StreamToolExtra `json:"tool,omitempty"`
Confirm *StreamConfirmExtra `json:"confirm,omitempty"`
Interrupt *StreamInterruptExtra `json:"interrupt,omitempty"`
Meta map[string]any `json:"meta,omitempty"`
}
// StreamStatusExtra 表示普通阶段状态或提示性事件。
type StreamStatusExtra struct {
Code string `json:"code,omitempty"`
Summary string `json:"summary,omitempty"`
}
// StreamToolExtra 表示一次工具调用相关事件。
type StreamToolExtra struct {
Name string `json:"name,omitempty"`
Status string `json:"status,omitempty"`
Summary string `json:"summary,omitempty"`
ArgumentsPreview string `json:"arguments_preview,omitempty"`
}
// StreamConfirmExtra 表示一次待确认事件的展示摘要。
type StreamConfirmExtra struct {
InteractionID string `json:"interaction_id,omitempty"`
Title string `json:"title,omitempty"`
Summary string `json:"summary,omitempty"`
}
// StreamInterruptExtra 表示一次中断事件的展示摘要。
type StreamInterruptExtra struct {
InteractionID string `json:"interaction_id,omitempty"`
Type string `json:"type,omitempty"`
Summary string `json:"summary,omitempty"`
}
// ToOpenAIStream 把 Eino message 转成 OpenAI 兼容 chunk。
func ToOpenAIStream(chunk *schema.Message, requestID, modelName string, created int64, includeRole bool) (string, error) {
return ToOpenAIStreamWithExtra(chunk, requestID, modelName, created, includeRole, nil)
}
// ToOpenAIStreamWithExtra 把 Eino message 转成带 extra 的 OpenAI 兼容 chunk。
//
// 职责边界:
// 1. 负责把 chunk.Content / chunk.ReasoningContent 映射到协议字段;
// 2. 负责挂载可选 extra供前端识别工具调用、确认请求等结构化事件
// 3. 不负责发送,也不负责决定“这个 chunk 该不该推”。
func ToOpenAIStreamWithExtra(chunk *schema.Message, requestID, modelName string, created int64, includeRole bool, extra *OpenAIChunkExtra) (string, error) {
delta := OpenAIChunkDelta{}
if includeRole {
delta.Role = "assistant"
}
if chunk != nil {
delta.Content = chunk.Content
delta.ReasoningContent = chunk.ReasoningContent
}
return buildOpenAIChunkPayload(requestID, modelName, created, delta, nil, extra)
}
// ToOpenAIReasoningChunk 直接构造一个 reasoning chunk。
func ToOpenAIReasoningChunk(requestID, modelName string, created int64, reasoning string, includeRole bool) (string, error) {
return ToOpenAIReasoningChunkWithExtra(requestID, modelName, created, reasoning, includeRole, nil)
}
// ToOpenAIReasoningChunkWithExtra 直接构造一个带 extra 的 reasoning chunk。
func ToOpenAIReasoningChunkWithExtra(requestID, modelName string, created int64, reasoning string, includeRole bool, extra *OpenAIChunkExtra) (string, error) {
delta := OpenAIChunkDelta{ReasoningContent: reasoning}
if includeRole {
delta.Role = "assistant"
}
return buildOpenAIChunkPayload(requestID, modelName, created, delta, nil, extra)
}
// ToOpenAIAssistantChunk 直接构造一个正文 chunk。
func ToOpenAIAssistantChunk(requestID, modelName string, created int64, content string, includeRole bool) (string, error) {
return ToOpenAIAssistantChunkWithExtra(requestID, modelName, created, content, includeRole, nil)
}
// ToOpenAIAssistantChunkWithExtra 直接构造一个带 extra 的正文 chunk。
func ToOpenAIAssistantChunkWithExtra(requestID, modelName string, created int64, content string, includeRole bool, extra *OpenAIChunkExtra) (string, error) {
delta := OpenAIChunkDelta{Content: content}
if includeRole {
delta.Role = "assistant"
}
return buildOpenAIChunkPayload(requestID, modelName, created, delta, nil, extra)
}
// ToOpenAIFinishStream 生成流式结束 chunkfinish_reason=stop
func ToOpenAIFinishStream(requestID, modelName string, created int64) (string, error) {
return ToOpenAIFinishStreamWithExtra(requestID, modelName, created, nil)
}
// ToOpenAIFinishStreamWithExtra 生成带 extra 的流式结束 chunk。
func ToOpenAIFinishStreamWithExtra(requestID, modelName string, created int64, extra *OpenAIChunkExtra) (string, error) {
stop := "stop"
return buildOpenAIChunkPayload(requestID, modelName, created, OpenAIChunkDelta{}, &stop, extra)
}
// NewReasoningTextExtra 创建“思考文字”事件的 extra。
func NewReasoningTextExtra(blockID, stage string) *OpenAIChunkExtra {
return &OpenAIChunkExtra{
Kind: StreamExtraKindReasoningText,
BlockID: blockID,
Stage: stage,
DisplayMode: StreamDisplayModeAppend,
}
}
// NewAssistantTextExtra 创建“正文文字”事件的 extra。
func NewAssistantTextExtra(blockID, stage string) *OpenAIChunkExtra {
return &OpenAIChunkExtra{
Kind: StreamExtraKindAssistantText,
BlockID: blockID,
Stage: stage,
DisplayMode: StreamDisplayModeAppend,
}
}
// NewStatusExtra 创建普通状态事件的 extra。
func NewStatusExtra(blockID, stage, code, summary string) *OpenAIChunkExtra {
return &OpenAIChunkExtra{
Kind: StreamExtraKindStatus,
BlockID: blockID,
Stage: stage,
DisplayMode: StreamDisplayModeCard,
Status: &StreamStatusExtra{
Code: code,
Summary: summary,
},
}
}
// NewToolCallExtra 创建“工具调用开始/中间态”事件的 extra。
func NewToolCallExtra(blockID, stage, toolName, status, summary, argumentsPreview string) *OpenAIChunkExtra {
return &OpenAIChunkExtra{
Kind: StreamExtraKindToolCall,
BlockID: blockID,
Stage: stage,
DisplayMode: StreamDisplayModeCard,
Tool: &StreamToolExtra{
Name: toolName,
Status: status,
Summary: summary,
ArgumentsPreview: argumentsPreview,
},
}
}
// NewToolResultExtra 创建“工具结果”事件的 extra。
func NewToolResultExtra(blockID, stage, toolName, status, summary, argumentsPreview string) *OpenAIChunkExtra {
return &OpenAIChunkExtra{
Kind: StreamExtraKindToolResult,
BlockID: blockID,
Stage: stage,
DisplayMode: StreamDisplayModeCard,
Tool: &StreamToolExtra{
Name: toolName,
Status: status,
Summary: summary,
ArgumentsPreview: argumentsPreview,
},
}
}
// NewConfirmRequestExtra 创建“待确认”事件的 extra。
func NewConfirmRequestExtra(blockID, stage, interactionID, title, summary string) *OpenAIChunkExtra {
return &OpenAIChunkExtra{
Kind: StreamExtraKindConfirm,
BlockID: blockID,
Stage: stage,
DisplayMode: StreamDisplayModeCard,
Confirm: &StreamConfirmExtra{
InteractionID: interactionID,
Title: title,
Summary: summary,
},
}
}
// NewInterruptExtra 创建“中断”事件的 extra。
func NewInterruptExtra(blockID, stage, interactionID, interactionType, summary string) *OpenAIChunkExtra {
return &OpenAIChunkExtra{
Kind: StreamExtraKindInterrupt,
BlockID: blockID,
Stage: stage,
DisplayMode: StreamDisplayModeCard,
Interrupt: &StreamInterruptExtra{
InteractionID: interactionID,
Type: interactionType,
Summary: summary,
},
}
}
// NewFinishExtra 创建“收尾完成”事件的 extra。
func NewFinishExtra(blockID, stage string) *OpenAIChunkExtra {
return &OpenAIChunkExtra{
Kind: StreamExtraKindFinish,
BlockID: blockID,
Stage: stage,
DisplayMode: StreamDisplayModeReplace,
}
}
func buildOpenAIChunkPayload(requestID, modelName string, created int64, delta OpenAIChunkDelta, finishReason *string, extra *OpenAIChunkExtra) (string, error) {
// 1. 若既没有 role也没有正文/思考,也没有 finish_reason且也没有 extra则视为“空块”直接跳过。
// 2. 这样后续 emitter 即使拆成“结构化事件 + 文本事件”双轨,也能复用统一的空块兜底。
if delta.Role == "" && delta.Content == "" && delta.ReasoningContent == "" && finishReason == nil && !hasStreamExtra(extra) {
return "", nil
}
choices := make([]OpenAIChunkChoice, 0, 1)
if delta.Role != "" || delta.Content != "" || delta.ReasoningContent != "" || finishReason != nil {
choices = append(choices, OpenAIChunkChoice{
Index: 0,
Delta: delta,
FinishReason: finishReason,
})
}
dto := OpenAIChunkResponse{
ID: requestID,
Object: "chat.completion.chunk",
Created: created,
Model: modelName,
Choices: choices,
Extra: extra,
}
data, err := json.Marshal(dto)
if err != nil {
return "", err
}
return string(data), nil
}
func hasStreamExtra(extra *OpenAIChunkExtra) bool {
if extra == nil {
return false
}
return extra.Kind != "" ||
extra.BlockID != "" ||
extra.Stage != "" ||
extra.DisplayMode != "" ||
extra.Status != nil ||
extra.Tool != nil ||
extra.Confirm != nil ||
extra.Interrupt != nil ||
len(extra.Meta) > 0
}