Version: 0.8.5.dev.260330
后端: 1.把node/plan的具体逻辑做完了,没仔细看,进入下一步之前需要仔细review 前端: 无改动 全仓库: 无改动
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||||
|
newagentnode "github.com/LoveLosita/smartflow/backend/newAgent/node"
|
||||||
"github.com/cloudwego/eino/compose"
|
"github.com/cloudwego/eino/compose"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,23 +20,25 @@ const (
|
|||||||
NodeDeliver = "deliver"
|
NodeDeliver = "deliver"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RunAgentGraph(ctx context.Context, state *newagentmodel.AgentRuntimeState) (*newagentmodel.AgentRuntimeState, error) {
|
func RunAgentGraph(ctx context.Context, input newagentmodel.AgentGraphRunInput) (*newagentmodel.AgentGraphState, error) {
|
||||||
|
state := newagentmodel.NewAgentGraphState(input)
|
||||||
if state == nil {
|
if state == nil {
|
||||||
return nil, errors.New("agent graph: state is nil")
|
return nil, errors.New("agent graph: graph state is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
flowState := state.EnsureCommonState()
|
flowState := state.EnsureFlowState()
|
||||||
if flowState == nil {
|
if flowState == nil {
|
||||||
return nil, errors.New("agent graph: common state is nil")
|
return nil, errors.New("agent graph: flow state is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
g := compose.NewGraph[*newagentmodel.AgentRuntimeState, *newagentmodel.AgentRuntimeState]()
|
nodes := newagentnode.NewAgentNodes()
|
||||||
|
g := compose.NewGraph[*newagentmodel.AgentGraphState, *newagentmodel.AgentGraphState]()
|
||||||
|
|
||||||
// --- 注册节点 ---
|
// --- 注册节点 ---
|
||||||
if err := g.AddLambdaNode(NodeChat, compose.InvokableLambda(chatNode)); err != nil {
|
if err := g.AddLambdaNode(NodeChat, compose.InvokableLambda(chatNode)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := g.AddLambdaNode(NodePlan, compose.InvokableLambda(planNode)); err != nil {
|
if err := g.AddLambdaNode(NodePlan, compose.InvokableLambda(nodes.Plan)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := g.AddLambdaNode(NodeConfirm, compose.InvokableLambda(confirmNode)); err != nil {
|
if err := g.AddLambdaNode(NodeConfirm, compose.InvokableLambda(confirmNode)); err != nil {
|
||||||
@@ -127,41 +130,32 @@ func RunAgentGraph(ctx context.Context, state *newagentmodel.AgentRuntimeState)
|
|||||||
return runnable.Invoke(ctx, state)
|
return runnable.Invoke(ctx, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 占位节点,后续由 node 层替换 ---
|
// --- 占位节点,后续逐步由 node 层替换 ---
|
||||||
|
|
||||||
func chatNode(_ context.Context, st *newagentmodel.AgentRuntimeState) (*newagentmodel.AgentRuntimeState, error) {
|
func chatNode(_ context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||||
if st == nil {
|
if st == nil {
|
||||||
return nil, errors.New("chat node: state is nil")
|
return nil, errors.New("chat node: state is nil")
|
||||||
}
|
}
|
||||||
st.EnsureCommonState()
|
st.EnsureFlowState()
|
||||||
|
st.EnsureConversationContext()
|
||||||
|
st.EnsureChunkEmitter()
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// 1. 识别当前请求是普通聊天、首次任务进入,还是从 pending interaction 恢复。
|
// 1. 识别当前请求是普通聊天、首次任务进入,还是从 pending interaction 恢复。
|
||||||
// 2. 若只是普通聊天,则生成回复并把 Phase 设为 PhaseChatting,后续直接 END。
|
// 2. 若只是普通聊天,则生成回复并把 Phase 设为 PhaseChatting,后续直接 END。
|
||||||
// 3. 若识别到任务意图,则把 Phase 切到 planning / waiting_confirm / executing 对应阶段。
|
// 3. 若识别到任务意图,则把 Phase 切到 planning / waiting_confirm / executing 对应阶段。
|
||||||
// 4. 若本轮是恢复请求,则这里只负责吞掉用户最新输入并准备恢复,不再重复输出闲聊回复。
|
// 4. 若本轮是恢复请求,则这里只负责吞掉用户最新输入并准备恢复,不再重复输出闲聊回复。
|
||||||
|
// 5. 后续 chatNode 可直接读取 st.Request.UserInput、st.ConversationContext 与 st.Deps.ResolveChatClient()。
|
||||||
return st, nil
|
return st, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func planNode(_ context.Context, st *newagentmodel.AgentRuntimeState) (*newagentmodel.AgentRuntimeState, error) {
|
func confirmNode(_ context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||||
if st == nil {
|
|
||||||
return nil, errors.New("plan node: state is nil")
|
|
||||||
}
|
|
||||||
st.EnsureCommonState()
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// 1. 每轮把“完整 plan + 当前步骤 + 置顶上下文”注入给 LLM,让模型只补一步规划。
|
|
||||||
// 2. 若缺少关键信息,则调用 st.OpenAskUserInteraction(...) 打开 ask_user 中断。
|
|
||||||
// 3. 若规划已经完整,则调用 st.FinishPlan(steps),把流程切到 waiting_confirm。
|
|
||||||
// 4. 若规划未完成,则保持 PhasePlanning,分支回到 plan 继续循环。
|
|
||||||
return st, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func confirmNode(_ context.Context, st *newagentmodel.AgentRuntimeState) (*newagentmodel.AgentRuntimeState, error) {
|
|
||||||
if st == nil {
|
if st == nil {
|
||||||
return nil, errors.New("confirm node: state is nil")
|
return nil, errors.New("confirm node: state is nil")
|
||||||
}
|
}
|
||||||
st.EnsureCommonState()
|
st.EnsureFlowState()
|
||||||
|
st.EnsureConversationContext()
|
||||||
|
st.EnsureChunkEmitter()
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// 1. 这里不再做“confirm 节点内自循环等待”,而是统一走中断恢复模式。
|
// 1. 这里不再做“confirm 节点内自循环等待”,而是统一走中断恢复模式。
|
||||||
@@ -171,14 +165,18 @@ func confirmNode(_ context.Context, st *newagentmodel.AgentRuntimeState) (*newag
|
|||||||
return st, nil
|
return st, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeNode(_ context.Context, st *newagentmodel.AgentRuntimeState) (*newagentmodel.AgentRuntimeState, error) {
|
func executeNode(_ context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||||
if st == nil {
|
if st == nil {
|
||||||
return nil, errors.New("execute node: state is nil")
|
return nil, errors.New("execute node: state is nil")
|
||||||
}
|
}
|
||||||
flowState := st.EnsureCommonState()
|
flowState := st.EnsureFlowState()
|
||||||
|
st.EnsureConversationContext()
|
||||||
|
st.EnsureChunkEmitter()
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// 1. 让 LLM 在“当前步骤”约束下做一轮 ReAct:思考 → 调工具/观察 → reflection。
|
// 1. 让 LLM 在“当前步骤”约束下做一轮 ReAct:思考 → 调工具/观察 → reflection。
|
||||||
|
// 1.1 执行阶段所需上下文应直接从 st.ConversationContext 读取。
|
||||||
|
// 1.2 执行阶段模型依赖应通过 st.Deps.ResolveExecuteClient() 获取。
|
||||||
// 2. 若执行中发现缺少关键用户信息,则调用 st.OpenAskUserInteraction(...) 并走 interrupt。
|
// 2. 若执行中发现缺少关键用户信息,则调用 st.OpenAskUserInteraction(...) 并走 interrupt。
|
||||||
// 3. 若命中写工具确认闸门:
|
// 3. 若命中写工具确认闸门:
|
||||||
// 3.1 若走同连接确认,则把 Phase 置为 waiting_confirm 并跳到 confirm;
|
// 3.1 若走同连接确认,则把 Phase 置为 waiting_confirm 并跳到 confirm;
|
||||||
@@ -188,11 +186,13 @@ func executeNode(_ context.Context, st *newagentmodel.AgentRuntimeState) (*newag
|
|||||||
return st, nil
|
return st, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func interruptNode(_ context.Context, st *newagentmodel.AgentRuntimeState) (*newagentmodel.AgentRuntimeState, error) {
|
func interruptNode(_ context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||||
if st == nil {
|
if st == nil {
|
||||||
return nil, errors.New("interrupt node: state is nil")
|
return nil, errors.New("interrupt node: state is nil")
|
||||||
}
|
}
|
||||||
st.EnsureCommonState()
|
st.EnsureFlowState()
|
||||||
|
st.EnsureConversationContext()
|
||||||
|
st.EnsureChunkEmitter()
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// 1. 若 PendingInteraction.Type=ask_user,则像普通聊天一样流式吐出问题文本。
|
// 1. 若 PendingInteraction.Type=ask_user,则像普通聊天一样流式吐出问题文本。
|
||||||
@@ -202,11 +202,13 @@ func interruptNode(_ context.Context, st *newagentmodel.AgentRuntimeState) (*new
|
|||||||
return st, nil
|
return st, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deliverNode(_ context.Context, st *newagentmodel.AgentRuntimeState) (*newagentmodel.AgentRuntimeState, error) {
|
func deliverNode(_ context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||||
if st == nil {
|
if st == nil {
|
||||||
return nil, errors.New("deliver node: state is nil")
|
return nil, errors.New("deliver node: state is nil")
|
||||||
}
|
}
|
||||||
flowState := st.EnsureCommonState()
|
flowState := st.EnsureFlowState()
|
||||||
|
st.EnsureConversationContext()
|
||||||
|
st.EnsureChunkEmitter()
|
||||||
|
|
||||||
// TODO: 将执行结果推给用户,并在所有外部落库完成后再标记 done。
|
// TODO: 将执行结果推给用户,并在所有外部落库完成后再标记 done。
|
||||||
flowState.Done()
|
flowState.Done()
|
||||||
@@ -215,12 +217,12 @@ func deliverNode(_ context.Context, st *newagentmodel.AgentRuntimeState) (*newag
|
|||||||
|
|
||||||
// --- 分支函数 ---
|
// --- 分支函数 ---
|
||||||
|
|
||||||
func branchAfterChat(_ context.Context, st *newagentmodel.AgentRuntimeState) (string, error) {
|
func branchAfterChat(_ context.Context, st *newagentmodel.AgentGraphState) (string, error) {
|
||||||
if nextNode, interrupted := branchIfInterrupted(st); interrupted {
|
if nextNode, interrupted := branchIfInterrupted(st); interrupted {
|
||||||
return nextNode, nil
|
return nextNode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
flowState := st.EnsureCommonState()
|
flowState := st.EnsureFlowState()
|
||||||
switch flowState.Phase {
|
switch flowState.Phase {
|
||||||
case newagentmodel.PhasePlanning:
|
case newagentmodel.PhasePlanning:
|
||||||
return NodePlan, nil
|
return NodePlan, nil
|
||||||
@@ -236,24 +238,24 @@ func branchAfterChat(_ context.Context, st *newagentmodel.AgentRuntimeState) (st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func branchAfterPlan(_ context.Context, st *newagentmodel.AgentRuntimeState) (string, error) {
|
func branchAfterPlan(_ context.Context, st *newagentmodel.AgentGraphState) (string, error) {
|
||||||
if nextNode, interrupted := branchIfInterrupted(st); interrupted {
|
if nextNode, interrupted := branchIfInterrupted(st); interrupted {
|
||||||
return nextNode, nil
|
return nextNode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
flowState := st.EnsureCommonState()
|
flowState := st.EnsureFlowState()
|
||||||
if flowState.Phase == newagentmodel.PhaseWaitingConfirm {
|
if flowState.Phase == newagentmodel.PhaseWaitingConfirm {
|
||||||
return NodeConfirm, nil
|
return NodeConfirm, nil
|
||||||
}
|
}
|
||||||
return NodePlan, nil
|
return NodePlan, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func branchAfterConfirm(_ context.Context, st *newagentmodel.AgentRuntimeState) (string, error) {
|
func branchAfterConfirm(_ context.Context, st *newagentmodel.AgentGraphState) (string, error) {
|
||||||
if nextNode, interrupted := branchIfInterrupted(st); interrupted {
|
if nextNode, interrupted := branchIfInterrupted(st); interrupted {
|
||||||
return nextNode, nil
|
return nextNode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
flowState := st.EnsureCommonState()
|
flowState := st.EnsureFlowState()
|
||||||
switch flowState.Phase {
|
switch flowState.Phase {
|
||||||
case newagentmodel.PhaseExecuting:
|
case newagentmodel.PhaseExecuting:
|
||||||
return NodeExecute, nil
|
return NodeExecute, nil
|
||||||
@@ -266,12 +268,12 @@ func branchAfterConfirm(_ context.Context, st *newagentmodel.AgentRuntimeState)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func branchAfterExecute(_ context.Context, st *newagentmodel.AgentRuntimeState) (string, error) {
|
func branchAfterExecute(_ context.Context, st *newagentmodel.AgentGraphState) (string, error) {
|
||||||
if nextNode, interrupted := branchIfInterrupted(st); interrupted {
|
if nextNode, interrupted := branchIfInterrupted(st); interrupted {
|
||||||
return nextNode, nil
|
return nextNode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
flowState := st.EnsureCommonState()
|
flowState := st.EnsureFlowState()
|
||||||
if flowState.Phase == newagentmodel.PhaseWaitingConfirm {
|
if flowState.Phase == newagentmodel.PhaseWaitingConfirm {
|
||||||
return NodeConfirm, nil
|
return NodeConfirm, nil
|
||||||
}
|
}
|
||||||
@@ -281,11 +283,12 @@ func branchAfterExecute(_ context.Context, st *newagentmodel.AgentRuntimeState)
|
|||||||
return NodeExecute, nil
|
return NodeExecute, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func branchIfInterrupted(st *newagentmodel.AgentRuntimeState) (string, bool) {
|
func branchIfInterrupted(st *newagentmodel.AgentGraphState) (string, bool) {
|
||||||
if st == nil {
|
if st == nil {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
if st.HasPendingInteraction() {
|
runtimeState := st.EnsureRuntimeState()
|
||||||
|
if runtimeState != nil && runtimeState.HasPendingInteraction() {
|
||||||
return NodeInterrupt, true
|
return NodeInterrupt, true
|
||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
// Phase 表示 agent 循环图当前所处的阶段。
|
// Phase 表示 agent 主循环当前所处的大阶段。
|
||||||
type Phase string
|
type Phase string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -12,8 +12,14 @@ const (
|
|||||||
|
|
||||||
const DefaultMaxRounds = 30
|
const DefaultMaxRounds = 30
|
||||||
|
|
||||||
|
// CommonState 承载可持久化的主流程状态。
|
||||||
|
//
|
||||||
|
// 职责边界:
|
||||||
|
// 1. 负责记录“当前处于哪个阶段、当前计划是什么、执行到了第几步、已经消耗了多少轮”;
|
||||||
|
// 2. 负责提供最小必要的安全访问方法,避免 graph/node/prompt 层到处手写切片越界判断;
|
||||||
|
// 3. 不负责承载对话历史、tool schema、pinned context 这类模型输入材料,它们仍然属于 ConversationContext。
|
||||||
type CommonState struct {
|
type CommonState struct {
|
||||||
// 身份
|
// 身份信息
|
||||||
TraceID string
|
TraceID string
|
||||||
UserID int
|
UserID int
|
||||||
ConversationID string
|
ConversationID string
|
||||||
@@ -21,8 +27,10 @@ type CommonState struct {
|
|||||||
// 流程阶段
|
// 流程阶段
|
||||||
Phase Phase
|
Phase Phase
|
||||||
|
|
||||||
// Plan
|
// 计划状态
|
||||||
PlanSteps []string
|
// 1. 这里直接使用结构化的 PlanStep,避免 planning -> execute 之间丢失 done_when。
|
||||||
|
// 2. CurrentStep 表示“当前 plan 步骤下标”,不是 execute 内部 ReAct 的思考轮次。
|
||||||
|
PlanSteps []PlanStep
|
||||||
CurrentStep int
|
CurrentStep int
|
||||||
|
|
||||||
// 安全边界
|
// 安全边界
|
||||||
@@ -40,53 +48,58 @@ func NewCommonState(traceID string, userID int, conversationID string) *CommonSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NextRound 消耗一轮并返回是否还有余量。
|
// NextRound 消耗一轮预算,并返回当前是否仍在允许范围内。
|
||||||
func (s *CommonState) NextRound() bool {
|
func (s *CommonState) NextRound() bool {
|
||||||
s.RoundUsed++
|
s.RoundUsed++
|
||||||
return s.RoundUsed <= s.MaxRounds
|
return s.RoundUsed <= s.MaxRounds
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exhausted 判断是否已耗尽轮次。
|
// Exhausted 判断是否已经耗尽轮次预算。
|
||||||
func (s *CommonState) Exhausted() bool {
|
func (s *CommonState) Exhausted() bool {
|
||||||
return s.RoundUsed >= s.MaxRounds
|
return s.RoundUsed >= s.MaxRounds
|
||||||
}
|
}
|
||||||
|
|
||||||
// FinishPlan 标记 plan 完成,进入等待确认阶段。
|
// FinishPlan 在 planning 完成后固化完整计划,并推进到待确认阶段。
|
||||||
func (s *CommonState) FinishPlan(steps []string) {
|
//
|
||||||
|
// 步骤说明:
|
||||||
|
// 1. 直接保存完整的 []PlanStep,避免 execute 阶段再去依赖 pinned context 回捞完成判定;
|
||||||
|
// 2. 统一把 CurrentStep 重置到第 0 步,保证后续 confirm/execute 都从计划开头进入;
|
||||||
|
// 3. 这里只负责状态切换,不负责刷新 ConversationContext 中的置顶 plan 文本。
|
||||||
|
func (s *CommonState) FinishPlan(steps []PlanStep) {
|
||||||
s.PlanSteps = steps
|
s.PlanSteps = steps
|
||||||
s.CurrentStep = 0
|
s.CurrentStep = 0
|
||||||
s.Phase = PhaseWaitingConfirm
|
s.Phase = PhaseWaitingConfirm
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfirmPlan 用户确认后进入执行阶段。
|
// ConfirmPlan 表示用户已确认计划,流程进入执行阶段。
|
||||||
func (s *CommonState) ConfirmPlan() {
|
func (s *CommonState) ConfirmPlan() {
|
||||||
s.Phase = PhaseExecuting
|
s.Phase = PhaseExecuting
|
||||||
}
|
}
|
||||||
|
|
||||||
// RejectPlan 用户拒绝,回到规划阶段。
|
// RejectPlan 表示用户拒绝当前计划,清空计划并回退到 planning。
|
||||||
func (s *CommonState) RejectPlan() {
|
func (s *CommonState) RejectPlan() {
|
||||||
s.PlanSteps = nil
|
s.PlanSteps = nil
|
||||||
s.CurrentStep = 0
|
s.CurrentStep = 0
|
||||||
s.Phase = PhasePlanning
|
s.Phase = PhasePlanning
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdvanceStep 推进到下一个 plan 步骤,返回是否还有剩余步骤。
|
// AdvanceStep 推进到下一个计划步骤,并返回是否仍有剩余步骤。
|
||||||
func (s *CommonState) AdvanceStep() bool {
|
func (s *CommonState) AdvanceStep() bool {
|
||||||
s.CurrentStep++
|
s.CurrentStep++
|
||||||
return s.CurrentStep < len(s.PlanSteps)
|
return s.CurrentStep < len(s.PlanSteps)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Done 标记整个流程结束。
|
// Done 标记整个任务流程已经结束。
|
||||||
func (s *CommonState) Done() {
|
func (s *CommonState) Done() {
|
||||||
s.Phase = PhaseDone
|
s.Phase = PhaseDone
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasPlan 判断当前 state 是否已经持有一份可执行的 plan。
|
// HasPlan 判断当前 state 是否已经持有一份完整计划。
|
||||||
//
|
//
|
||||||
// 职责边界:
|
// 职责边界:
|
||||||
// 1. 负责把“外部直接判断 len(PlanSteps) > 0”的零散逻辑收口到 state 内部;
|
// 1. 负责收口“是否存在 plan”这一层判断,避免外层到处写 len(PlanSteps) > 0;
|
||||||
// 2. 只回答“是否存在 plan”,不判断当前索引是否有效;
|
// 2. 不判断 CurrentStep 当前是否有效,当前步骤是否合法由 HasCurrentPlanStep 回答;
|
||||||
// 3. 当 state 为空时返回 false,调用方可据此决定是否回退到重新规划。
|
// 3. state 为空时统一返回 false,调用方可据此决定是否回退到 planning。
|
||||||
func (s *CommonState) HasPlan() bool {
|
func (s *CommonState) HasPlan() bool {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return false
|
return false
|
||||||
@@ -94,40 +107,35 @@ func (s *CommonState) HasPlan() bool {
|
|||||||
return len(s.PlanSteps) > 0
|
return len(s.PlanSteps) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// CurrentPlanStep 返回当前正在执行的 plan 步骤文本。
|
// CurrentPlanStep 返回当前正在执行的结构化计划步骤。
|
||||||
//
|
//
|
||||||
// 职责边界:
|
// 职责边界:
|
||||||
// 1. 负责根据 CurrentStep 安全读取 PlanSteps,避免调用方重复写切片越界判断;
|
// 1. 负责根据 CurrentStep 安全读取 PlanSteps,避免 graph/node/prompt 层重复写越界判断;
|
||||||
// 2. 当 state 为空、plan 为空、或当前索引越界时,统一返回 ("", false);
|
// 2. 若 state 为空、plan 为空、或当前索引越界,则统一返回 (PlanStep{}, false);
|
||||||
// 3. 不负责推进步骤,也不负责修正 CurrentStep 的取值。
|
// 3. 不负责推进步骤,也不负责修正 CurrentStep 的取值。
|
||||||
func (s *CommonState) CurrentPlanStep() (string, bool) {
|
func (s *CommonState) CurrentPlanStep() (PlanStep, bool) {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return "", false
|
return PlanStep{}, false
|
||||||
}
|
}
|
||||||
if s.CurrentStep < 0 || s.CurrentStep >= len(s.PlanSteps) {
|
if s.CurrentStep < 0 || s.CurrentStep >= len(s.PlanSteps) {
|
||||||
return "", false
|
return PlanStep{}, false
|
||||||
}
|
}
|
||||||
return s.PlanSteps[s.CurrentStep], true
|
return s.PlanSteps[s.CurrentStep], true
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasCurrentPlanStep 判断“当前步骤”是否存在且可安全读取。
|
// HasCurrentPlanStep 判断“当前步骤”是否存在且可安全读取。
|
||||||
//
|
|
||||||
// 职责边界:
|
|
||||||
// 1. 负责给 graph / node 层提供一个更直白的布尔判断入口;
|
|
||||||
// 2. 内部复用 CurrentPlanStep,避免两处维护相同的索引边界逻辑;
|
|
||||||
// 3. 不返回步骤内容,只回答“当前是否还有可注入的步骤”。
|
|
||||||
func (s *CommonState) HasCurrentPlanStep() bool {
|
func (s *CommonState) HasCurrentPlanStep() bool {
|
||||||
_, ok := s.CurrentPlanStep()
|
_, ok := s.CurrentPlanStep()
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlanProgress 返回当前 plan 的执行进度。
|
// PlanProgress 返回当前计划的执行进度。
|
||||||
//
|
//
|
||||||
// 输出语义:
|
// 输出语义:
|
||||||
// 1. current 使用对人类更友好的 1-based 序号,适合直接写入 prompt 或日志;
|
// 1. current 使用更适合给用户看的 1-based 序号;
|
||||||
// 2. total 表示当前 plan 总步数;
|
// 2. total 表示当前计划的总步数;
|
||||||
// 3. 若尚未生成 plan,则返回 (0, 0);
|
// 3. 若当前还没有计划,则返回 (0, 0);
|
||||||
// 4. 若 CurrentStep 已越过末尾,则 current 会被收敛到 total,避免上层出现 total+1 这类噪音值。
|
// 4. 若 CurrentStep 已越界到末尾之后,则把 current 收敛到 total,避免出现 total+1 这种噪音值。
|
||||||
func (s *CommonState) PlanProgress() (current int, total int) {
|
func (s *CommonState) PlanProgress() (current int, total int) {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return 0, 0
|
return 0, 0
|
||||||
|
|||||||
193
backend/newAgent/model/graph_run_state.go
Normal file
193
backend/newAgent/model/graph_run_state.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
newagentllm "github.com/LoveLosita/smartflow/backend/newAgent/llm"
|
||||||
|
newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AgentGraphRequest 描述一次 agent graph 运行的请求级输入。
|
||||||
|
//
|
||||||
|
// 职责边界:
|
||||||
|
// 1. 这里只放“当前这次请求”天然携带的轻量数据,例如用户本轮输入;
|
||||||
|
// 2. 不负责承载可持久化流程状态,流程状态仍归 AgentRuntimeState;
|
||||||
|
// 3. 不负责承载 LLM / emitter / store 等依赖,这些统一放进 AgentGraphDeps。
|
||||||
|
type AgentGraphRequest struct {
|
||||||
|
UserInput string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize 统一清洗请求级输入中的字符串字段。
|
||||||
|
func (r *AgentGraphRequest) Normalize() {
|
||||||
|
if r == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.UserInput = strings.TrimSpace(r.UserInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgentGraphDeps 描述 graph/node 层运行时真正依赖的可插拔能力。
|
||||||
|
//
|
||||||
|
// 设计目的:
|
||||||
|
// 1. 让 graph 不再只拿到“裸状态”,而是能拿到上下文、模型和输出能力;
|
||||||
|
// 2. Chat/Plan/Execute/Deliver 允许分别挂不同 client,但也允许先复用同一个 client;
|
||||||
|
// 3. ChunkEmitter 统一承接阶段提示、正文、工具事件、确认请求等 SSE 输出。
|
||||||
|
type AgentGraphDeps struct {
|
||||||
|
ChatClient *newagentllm.Client
|
||||||
|
PlanClient *newagentllm.Client
|
||||||
|
ExecuteClient *newagentllm.Client
|
||||||
|
DeliverClient *newagentllm.Client
|
||||||
|
ChunkEmitter *newagentstream.ChunkEmitter
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureChunkEmitter 保证 graph 运行时始终有一个可用的 chunk 发射器。
|
||||||
|
//
|
||||||
|
// 步骤说明:
|
||||||
|
// 1. 依赖为空时回退到 Noop emitter,避免骨架期因为没接前端而到处判空;
|
||||||
|
// 2. 这里只兜底“能安全调用”,不负责填充真实 request_id / model_name;
|
||||||
|
// 3. 后续 service 层一旦接上真实 emitter,会自然覆盖这里的空实现。
|
||||||
|
func (d *AgentGraphDeps) EnsureChunkEmitter() *newagentstream.ChunkEmitter {
|
||||||
|
if d == nil {
|
||||||
|
return newagentstream.NewChunkEmitter(newagentstream.NoopPayloadEmitter(), "", "", 0)
|
||||||
|
}
|
||||||
|
if d.ChunkEmitter == nil {
|
||||||
|
d.ChunkEmitter = newagentstream.NewChunkEmitter(newagentstream.NoopPayloadEmitter(), "", "", 0)
|
||||||
|
}
|
||||||
|
return d.ChunkEmitter
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveChatClient 返回 chat 阶段可用的模型客户端。
|
||||||
|
func (d *AgentGraphDeps) ResolveChatClient() *newagentllm.Client {
|
||||||
|
if d == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return d.ChatClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolvePlanClient 返回 planning 阶段可用的模型客户端。
|
||||||
|
//
|
||||||
|
// 兜底策略:
|
||||||
|
// 1. 优先使用显式注入的 PlanClient;
|
||||||
|
// 2. 若未单独注入,则回退到 ChatClient;
|
||||||
|
// 3. 这样在骨架期可先用一套 client 跑通,再按需拆分 strategist / worker。
|
||||||
|
func (d *AgentGraphDeps) ResolvePlanClient() *newagentllm.Client {
|
||||||
|
if d == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if d.PlanClient != nil {
|
||||||
|
return d.PlanClient
|
||||||
|
}
|
||||||
|
return d.ChatClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveExecuteClient 返回 execute 阶段可用的模型客户端。
|
||||||
|
func (d *AgentGraphDeps) ResolveExecuteClient() *newagentllm.Client {
|
||||||
|
if d == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if d.ExecuteClient != nil {
|
||||||
|
return d.ExecuteClient
|
||||||
|
}
|
||||||
|
if d.PlanClient != nil {
|
||||||
|
return d.PlanClient
|
||||||
|
}
|
||||||
|
return d.ChatClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveDeliverClient 返回 deliver 阶段可用的模型客户端。
|
||||||
|
func (d *AgentGraphDeps) ResolveDeliverClient() *newagentllm.Client {
|
||||||
|
if d == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if d.DeliverClient != nil {
|
||||||
|
return d.DeliverClient
|
||||||
|
}
|
||||||
|
if d.ExecuteClient != nil {
|
||||||
|
return d.ExecuteClient
|
||||||
|
}
|
||||||
|
if d.PlanClient != nil {
|
||||||
|
return d.PlanClient
|
||||||
|
}
|
||||||
|
return d.ChatClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgentGraphRunInput 是执行 newAgent 通用 graph 所需的完整入口参数。
|
||||||
|
//
|
||||||
|
// 字段说明:
|
||||||
|
// 1. RuntimeState:可持久化流程状态与 pending interaction;
|
||||||
|
// 2. ConversationContext:本轮喂给模型的上下文材料;
|
||||||
|
// 3. Request:当前这次请求的轻量输入;
|
||||||
|
// 4. Deps:graph/node 层真正依赖的可插拔能力。
|
||||||
|
type AgentGraphRunInput struct {
|
||||||
|
RuntimeState *AgentRuntimeState
|
||||||
|
ConversationContext *ConversationContext
|
||||||
|
Request AgentGraphRequest
|
||||||
|
Deps AgentGraphDeps
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgentGraphState 是 graph 内部真正流转的运行态容器。
|
||||||
|
//
|
||||||
|
// 职责边界:
|
||||||
|
// 1. 负责把“流程状态 + 对话上下文 + 请求输入 + 运行依赖”收口到同一个对象;
|
||||||
|
// 2. 负责给 graph 分支和 node 提供最小必要的兜底访问方法;
|
||||||
|
// 3. 不负责持久化,不负责真正业务执行。
|
||||||
|
type AgentGraphState struct {
|
||||||
|
RuntimeState *AgentRuntimeState
|
||||||
|
ConversationContext *ConversationContext
|
||||||
|
Request AgentGraphRequest
|
||||||
|
Deps AgentGraphDeps
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAgentGraphState 把入口参数整理成 graph 内部状态。
|
||||||
|
func NewAgentGraphState(input AgentGraphRunInput) *AgentGraphState {
|
||||||
|
st := &AgentGraphState{
|
||||||
|
RuntimeState: input.RuntimeState,
|
||||||
|
ConversationContext: input.ConversationContext,
|
||||||
|
Request: input.Request,
|
||||||
|
Deps: input.Deps,
|
||||||
|
}
|
||||||
|
st.Request.Normalize()
|
||||||
|
st.EnsureRuntimeState()
|
||||||
|
st.EnsureConversationContext()
|
||||||
|
st.Deps.EnsureChunkEmitter()
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureRuntimeState 保证 graph 内部始终持有一份可用的运行态。
|
||||||
|
func (s *AgentGraphState) EnsureRuntimeState() *AgentRuntimeState {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if s.RuntimeState == nil {
|
||||||
|
s.RuntimeState = NewAgentRuntimeState(nil)
|
||||||
|
}
|
||||||
|
s.RuntimeState.EnsureCommonState()
|
||||||
|
return s.RuntimeState
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureFlowState 返回可持久化的主流程状态。
|
||||||
|
func (s *AgentGraphState) EnsureFlowState() *CommonState {
|
||||||
|
runtimeState := s.EnsureRuntimeState()
|
||||||
|
if runtimeState == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return runtimeState.EnsureCommonState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureConversationContext 保证 graph 内部始终持有一份可用的会话上下文。
|
||||||
|
func (s *AgentGraphState) EnsureConversationContext() *ConversationContext {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if s.ConversationContext == nil {
|
||||||
|
s.ConversationContext = NewConversationContext("")
|
||||||
|
}
|
||||||
|
return s.ConversationContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureChunkEmitter 返回 graph 可安全调用的 chunk 发射器。
|
||||||
|
func (s *AgentGraphState) EnsureChunkEmitter() *newagentstream.ChunkEmitter {
|
||||||
|
if s == nil {
|
||||||
|
return newagentstream.NewChunkEmitter(newagentstream.NoopPayloadEmitter(), "", "", 0)
|
||||||
|
}
|
||||||
|
return s.Deps.EnsureChunkEmitter()
|
||||||
|
}
|
||||||
121
backend/newAgent/model/plan_contract.go
Normal file
121
backend/newAgent/model/plan_contract.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PlanAction 表示规划阶段单轮决策的动作类型。
|
||||||
|
//
|
||||||
|
// 设计原则:
|
||||||
|
// 1. 规划阶段只关心“继续规划 / 追问用户 / 规划完成”这三类动作;
|
||||||
|
// 2. 这里先不把工具调用塞进 contract,避免过早把 plan loop 复杂化;
|
||||||
|
// 3. 规划层产出的是“自然语言计划”,不是执行层的工具动作。
|
||||||
|
type PlanAction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PlanActionContinue 表示当前信息已足够,继续规划下一轮。
|
||||||
|
PlanActionContinue PlanAction = "continue"
|
||||||
|
|
||||||
|
// PlanActionAskUser 表示当前规划缺少关键信息,需要中断并追问用户。
|
||||||
|
PlanActionAskUser PlanAction = "ask_user"
|
||||||
|
|
||||||
|
// PlanActionDone 表示规划已经完成,可以进入 confirm 或下一阶段。
|
||||||
|
PlanActionDone PlanAction = "plan_done"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PlanDecision 是 plan prompt 单轮产出的统一决策结构。
|
||||||
|
//
|
||||||
|
// 职责边界:
|
||||||
|
// 1. Speak 是本轮先对用户说的话;若 action=ask_user,通常这里会承载要追问的问题;
|
||||||
|
// 2. Action 是规划阶段的下一步动作类型;
|
||||||
|
// 3. Reason 是给后端和日志看的简短解释;
|
||||||
|
// 4. PlanSteps 只在 plan_done 时要求返回,表示本轮最终确认下来的完整自然语言计划。
|
||||||
|
type PlanDecision struct {
|
||||||
|
Speak string `json:"speak,omitempty"`
|
||||||
|
Action PlanAction `json:"action"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
PlanSteps []PlanStep `json:"plan_steps,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize 统一清洗规划决策中的字符串字段。
|
||||||
|
func (d *PlanDecision) Normalize() {
|
||||||
|
if d == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.Speak = strings.TrimSpace(d.Speak)
|
||||||
|
d.Action = PlanAction(strings.TrimSpace(string(d.Action)))
|
||||||
|
d.Reason = strings.TrimSpace(d.Reason)
|
||||||
|
for i := range d.PlanSteps {
|
||||||
|
d.PlanSteps[i].Normalize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate 校验规划决策的最小合法性。
|
||||||
|
//
|
||||||
|
// 校验原则:
|
||||||
|
// 1. 这里只校验“协议是否自洽”,不校验规划内容是否聪明、是否足够好;
|
||||||
|
// 2. 只有 plan_done 允许返回完整 plan_steps;
|
||||||
|
// 3. 真正的规划质量判断仍留给后续 node 层和用户确认环节。
|
||||||
|
func (d *PlanDecision) Validate() error {
|
||||||
|
if d == nil {
|
||||||
|
return fmt.Errorf("plan decision 不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Normalize()
|
||||||
|
if d.Action == "" {
|
||||||
|
return fmt.Errorf("plan decision.action 不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch d.Action {
|
||||||
|
case PlanActionContinue, PlanActionAskUser:
|
||||||
|
if len(d.PlanSteps) > 0 {
|
||||||
|
return fmt.Errorf("%s 动作不应携带 plan_steps", d.Action)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case PlanActionDone:
|
||||||
|
if len(d.PlanSteps) == 0 {
|
||||||
|
return fmt.Errorf("plan_done 动作必须携带完整 plan_steps")
|
||||||
|
}
|
||||||
|
for i := range d.PlanSteps {
|
||||||
|
if err := d.PlanSteps[i].Validate(); err != nil {
|
||||||
|
return fmt.Errorf("plan_steps[%d] 非法: %w", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("未知 plan action: %s", d.Action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlanStep 表示规划阶段产出的一条自然语言步骤。
|
||||||
|
//
|
||||||
|
// 设计说明:
|
||||||
|
// 1. Content 是步骤正文,后续可直接落到 CommonState.PlanSteps;
|
||||||
|
// 2. DoneWhen 是可选的完成判定描述,用来给 execute 阶段提供最小退出条件;
|
||||||
|
// 3. 这里仍然保持“自然语言优先”,不把 plan step 过度结构化。
|
||||||
|
type PlanStep struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
DoneWhen string `json:"done_when,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize 统一清洗 plan step 中的字符串字段。
|
||||||
|
func (s *PlanStep) Normalize() {
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Content = strings.TrimSpace(s.Content)
|
||||||
|
s.DoneWhen = strings.TrimSpace(s.DoneWhen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate 校验单条 plan step 的最小合法性。
|
||||||
|
func (s *PlanStep) Validate() error {
|
||||||
|
if s == nil {
|
||||||
|
return fmt.Errorf("plan step 不能为空")
|
||||||
|
}
|
||||||
|
s.Normalize()
|
||||||
|
if s.Content == "" {
|
||||||
|
return fmt.Errorf("plan step.content 不能为空")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
48
backend/newAgent/node/agent_nodes.go
Normal file
48
backend/newAgent/node/agent_nodes.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package newagentnode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AgentNodes 是 newAgent 通用图的节点容器。
|
||||||
|
//
|
||||||
|
// 职责边界:
|
||||||
|
// 1. 负责把 node 层真正实现的方法统一暴露给 graph 注册;
|
||||||
|
// 2. 负责收口“graph 只编排、node 真执行”的结构约束;
|
||||||
|
// 3. 当前先迁移 Plan,其他节点后续按同样模式逐步下沉。
|
||||||
|
type AgentNodes struct{}
|
||||||
|
|
||||||
|
// NewAgentNodes 创建通用节点容器。
|
||||||
|
func NewAgentNodes() *AgentNodes {
|
||||||
|
return &AgentNodes{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan 是规划阶段的正式节点方法。
|
||||||
|
//
|
||||||
|
// 职责边界:
|
||||||
|
// 1. 这里只做 graph -> node 的参数转接;
|
||||||
|
// 2. 真正的单轮规划逻辑仍由 RunPlanNode 负责;
|
||||||
|
// 3. 这样 graph 层后续只需挂 n.Plan,而不再自己维护占位 planNode。
|
||||||
|
func (n *AgentNodes) Plan(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||||
|
if st == nil {
|
||||||
|
return nil, errors.New("plan node: state is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := RunPlanNode(
|
||||||
|
ctx,
|
||||||
|
PlanNodeInput{
|
||||||
|
RuntimeState: st.EnsureRuntimeState(),
|
||||||
|
ConversationContext: st.EnsureConversationContext(),
|
||||||
|
UserInput: st.Request.UserInput,
|
||||||
|
Client: st.Deps.ResolvePlanClient(),
|
||||||
|
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||||
|
ResumeNode: "plan",
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return st, nil
|
||||||
|
}
|
||||||
205
backend/newAgent/node/plan.go
Normal file
205
backend/newAgent/node/plan.go
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
package newagentnode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
newagentllm "github.com/LoveLosita/smartflow/backend/newAgent/llm"
|
||||||
|
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||||
|
newagentprompt "github.com/LoveLosita/smartflow/backend/newAgent/prompt"
|
||||||
|
newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream"
|
||||||
|
"github.com/cloudwego/eino/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
planStageName = "plan"
|
||||||
|
planStatusBlockID = "plan.status"
|
||||||
|
planSpeakBlockID = "plan.speak"
|
||||||
|
planPinnedKey = "current_plan"
|
||||||
|
planCurrentStepKey = "current_step"
|
||||||
|
planCurrentStepTitle = "当前步骤"
|
||||||
|
planFullPlanTitle = "当前完整计划"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PlanNodeInput 描述单轮规划节点执行所需的最小依赖。
|
||||||
|
type PlanNodeInput struct {
|
||||||
|
RuntimeState *newagentmodel.AgentRuntimeState
|
||||||
|
ConversationContext *newagentmodel.ConversationContext
|
||||||
|
UserInput string
|
||||||
|
Client *newagentllm.Client
|
||||||
|
ChunkEmitter *newagentstream.ChunkEmitter
|
||||||
|
ResumeNode string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunPlanNode 执行一轮规划节点逻辑。
|
||||||
|
//
|
||||||
|
// 步骤说明:
|
||||||
|
// 1. 先校验最小依赖,并推送一条“正在规划”的状态,避免用户空等;
|
||||||
|
// 2. 再用 prompt/plan.go 组装 messages,请模型严格输出 PlanDecision JSON;
|
||||||
|
// 3. 若模型先对用户说了话,则先把 speak 伪流式推给前端,并写回 history;
|
||||||
|
// 4. 最后按 action 推进流程:
|
||||||
|
// 4.1 continue:继续停留在 planning;
|
||||||
|
// 4.2 ask_user:打开 pending interaction,后续交给 interrupt 收口;
|
||||||
|
// 4.3 plan_done:固化完整计划,刷新 pinned context,并进入 waiting_confirm。
|
||||||
|
func RunPlanNode(ctx context.Context, input PlanNodeInput) error {
|
||||||
|
runtimeState, conversationContext, emitter, err := preparePlanNodeInput(input)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flowState := runtimeState.EnsureCommonState()
|
||||||
|
|
||||||
|
// 1. 先发一条阶段状态,让前端知道当前已经进入规划环节。
|
||||||
|
if err := emitter.EmitStatus(
|
||||||
|
planStatusBlockID,
|
||||||
|
planStageName,
|
||||||
|
"planning",
|
||||||
|
"正在梳理目标并补全执行计划。",
|
||||||
|
false,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("规划阶段状态推送失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 构造本轮规划输入,并要求模型输出结构化 PlanDecision。
|
||||||
|
messages := newagentprompt.BuildPlanMessages(flowState, conversationContext, input.UserInput)
|
||||||
|
decision, rawResult, err := newagentllm.GenerateJSON[newagentmodel.PlanDecision](
|
||||||
|
ctx,
|
||||||
|
input.Client,
|
||||||
|
messages,
|
||||||
|
newagentllm.GenerateOptions{
|
||||||
|
Temperature: 0.2,
|
||||||
|
MaxTokens: 1600,
|
||||||
|
Thinking: newagentllm.ThinkingModeEnabled,
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"stage": planStageName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if rawResult != nil && strings.TrimSpace(rawResult.Text) != "" {
|
||||||
|
return fmt.Errorf("规划输出解析失败,原始输出=%s,错误=%w", strings.TrimSpace(rawResult.Text), err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("规划阶段模型调用失败: %w", err)
|
||||||
|
}
|
||||||
|
if err := decision.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("规划决策不合法: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 若模型先对用户说了话,则先以伪流式推送,再写回 history,保证上下文连续。
|
||||||
|
if strings.TrimSpace(decision.Speak) != "" {
|
||||||
|
if err := emitter.EmitPseudoAssistantText(
|
||||||
|
ctx,
|
||||||
|
planSpeakBlockID,
|
||||||
|
planStageName,
|
||||||
|
decision.Speak,
|
||||||
|
newagentstream.DefaultPseudoStreamOptions(),
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("规划文案推送失败: %w", err)
|
||||||
|
}
|
||||||
|
conversationContext.AppendHistory(schema.AssistantMessage(decision.Speak, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 按规划动作推进流程状态。
|
||||||
|
switch decision.Action {
|
||||||
|
case newagentmodel.PlanActionContinue:
|
||||||
|
flowState.Phase = newagentmodel.PhasePlanning
|
||||||
|
return nil
|
||||||
|
case newagentmodel.PlanActionAskUser:
|
||||||
|
question := resolvePlanAskUserText(decision)
|
||||||
|
runtimeState.OpenAskUserInteraction(uuid.NewString(), question, strings.TrimSpace(input.ResumeNode))
|
||||||
|
return nil
|
||||||
|
case newagentmodel.PlanActionDone:
|
||||||
|
// 4.1 直接把结构化 PlanStep 固化到 CommonState,避免 state 层丢失 done_when。
|
||||||
|
// 4.2 再把完整自然语言计划写入 pinned context,保证后续 execute 优先看到。
|
||||||
|
// 4.3 最后进入 waiting_confirm,等待用户确认整体计划。
|
||||||
|
flowState.FinishPlan(decision.PlanSteps)
|
||||||
|
writePlanPinnedBlocks(conversationContext, decision.PlanSteps)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("未支持的规划动作: %s", decision.Action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func preparePlanNodeInput(input PlanNodeInput) (*newagentmodel.AgentRuntimeState, *newagentmodel.ConversationContext, *newagentstream.ChunkEmitter, error) {
|
||||||
|
if input.RuntimeState == nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("plan node: runtime state 不能为空")
|
||||||
|
}
|
||||||
|
if input.Client == nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("plan node: plan client 未注入")
|
||||||
|
}
|
||||||
|
|
||||||
|
input.RuntimeState.EnsureCommonState()
|
||||||
|
if input.ConversationContext == nil {
|
||||||
|
input.ConversationContext = newagentmodel.NewConversationContext("")
|
||||||
|
}
|
||||||
|
if input.ChunkEmitter == nil {
|
||||||
|
input.ChunkEmitter = newagentstream.NewChunkEmitter(newagentstream.NoopPayloadEmitter(), "", "", time.Now().Unix())
|
||||||
|
}
|
||||||
|
return input.RuntimeState, input.ConversationContext, input.ChunkEmitter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolvePlanAskUserText(decision *newagentmodel.PlanDecision) string {
|
||||||
|
if decision == nil {
|
||||||
|
return "我还缺一点关键信息,想先向你确认一下。"
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(decision.Speak) != "" {
|
||||||
|
return strings.TrimSpace(decision.Speak)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(decision.Reason) != "" {
|
||||||
|
return strings.TrimSpace(decision.Reason)
|
||||||
|
}
|
||||||
|
return "我还缺一点关键信息,想先向你确认一下。"
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePlanPinnedBlocks(ctx *newagentmodel.ConversationContext, steps []newagentmodel.PlanStep) {
|
||||||
|
if ctx == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPlanText := buildPinnedPlanText(steps)
|
||||||
|
if strings.TrimSpace(fullPlanText) != "" {
|
||||||
|
ctx.UpsertPinnedBlock(newagentmodel.ContextBlock{
|
||||||
|
Key: planPinnedKey,
|
||||||
|
Title: planFullPlanTitle,
|
||||||
|
Content: fullPlanText,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(steps) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
firstStep := strings.TrimSpace(steps[0].Content)
|
||||||
|
if strings.TrimSpace(steps[0].DoneWhen) != "" {
|
||||||
|
firstStep = fmt.Sprintf("%s\n完成判定:%s", firstStep, strings.TrimSpace(steps[0].DoneWhen))
|
||||||
|
}
|
||||||
|
ctx.UpsertPinnedBlock(newagentmodel.ContextBlock{
|
||||||
|
Key: planCurrentStepKey,
|
||||||
|
Title: planCurrentStepTitle,
|
||||||
|
Content: firstStep,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPinnedPlanText(steps []newagentmodel.PlanStep) string {
|
||||||
|
if len(steps) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := make([]string, 0, len(steps))
|
||||||
|
for i, step := range steps {
|
||||||
|
content := strings.TrimSpace(step.Content)
|
||||||
|
if content == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
line := fmt.Sprintf("%d. %s", i+1, content)
|
||||||
|
if strings.TrimSpace(step.DoneWhen) != "" {
|
||||||
|
line += fmt.Sprintf("\n完成判定:%s", strings.TrimSpace(step.DoneWhen))
|
||||||
|
}
|
||||||
|
lines = append(lines, line)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(strings.Join(lines, "\n\n"))
|
||||||
|
}
|
||||||
@@ -8,25 +8,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ExecuteNextPlanSignal 表示“当前 plan 步骤已经完成,可以进入下一个步骤”。
|
// ExecuteNextPlanSignal 表示“当前 plan step 已完成,可以推进到下一个步骤”。
|
||||||
//
|
|
||||||
// TODO(newagent/node): 后续 executeNode 识别到该信号后,调用 state.AdvanceStep() 或决定进入交付阶段。
|
|
||||||
ExecuteNextPlanSignal = "[NEXT_PLAN]"
|
ExecuteNextPlanSignal = "[NEXT_PLAN]"
|
||||||
|
|
||||||
// ExecuteDoneSignal 表示“整个任务已经完成,可以结束执行链路”。
|
// ExecuteDoneSignal 表示“整个任务已经完成,可以进入最终交付”。
|
||||||
//
|
|
||||||
// TODO(newagent/node): 后续 executeNode 识别到该信号后,调用 state.Done() 并进入 deliver。
|
|
||||||
ExecuteDoneSignal = "[DONE]"
|
ExecuteDoneSignal = "[DONE]"
|
||||||
|
|
||||||
// ExecuteAskUserSignal 表示“执行阶段缺关键信息,需要向用户追问”。
|
// ExecuteAskUserSignal 表示“执行当前步骤缺少关键信息,需要向用户追问”。
|
||||||
//
|
|
||||||
// TODO(newagent/node): 后续若你决定支持 ask_user,这里可作为统一控制信号继续扩展。
|
|
||||||
ExecuteAskUserSignal = "[ASK_USER]"
|
ExecuteAskUserSignal = "[ASK_USER]"
|
||||||
)
|
)
|
||||||
|
|
||||||
const executeSystemPrompt = `
|
const executeSystemPrompt = `
|
||||||
你是 SmartFlow NewAgent 的执行器。
|
你是 SmartFlow NewAgent 的执行器。
|
||||||
|
|
||||||
你的职责是在“当前 plan 步骤”的约束下,进行思考、执行、观察,再决定下一步动作。
|
你的职责是在“当前 plan 步骤”的约束下,进行思考、执行、观察,再决定下一步动作。
|
||||||
|
|
||||||
请遵守以下规则:
|
请遵守以下规则:
|
||||||
@@ -52,13 +45,6 @@ func BuildExecuteSystemPrompt() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BuildExecuteMessages 组装执行阶段的 messages。
|
// BuildExecuteMessages 组装执行阶段的 messages。
|
||||||
//
|
|
||||||
// 职责边界:
|
|
||||||
// 1. 负责收敛执行阶段需要的 system / pinned / history / runtime prompt;
|
|
||||||
// 2. 负责把“完整 plan + 当前步骤 + 控制信号”显式告知模型;
|
|
||||||
// 3. 不负责解析模型输出,也不负责真正调用工具。
|
|
||||||
//
|
|
||||||
// TODO(newagent/node): 后续 executeNode 应直接复用这个方法,而不是在节点内手拼执行提示词。
|
|
||||||
func BuildExecuteMessages(state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext) []*schema.Message {
|
func BuildExecuteMessages(state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext) []*schema.Message {
|
||||||
return buildStageMessages(
|
return buildStageMessages(
|
||||||
BuildExecuteSystemPrompt(),
|
BuildExecuteSystemPrompt(),
|
||||||
@@ -94,8 +80,13 @@ func BuildExecuteUserPrompt(state *newagentmodel.CommonState) string {
|
|||||||
sb.WriteString(ExecuteAskUserSignal)
|
sb.WriteString(ExecuteAskUserSignal)
|
||||||
sb.WriteString("。\n")
|
sb.WriteString("。\n")
|
||||||
sb.WriteString("\n当前步骤正文:\n")
|
sb.WriteString("\n当前步骤正文:\n")
|
||||||
sb.WriteString(currentStep)
|
sb.WriteString(strings.TrimSpace(currentStep.Content))
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
|
if strings.TrimSpace(currentStep.DoneWhen) != "" {
|
||||||
|
sb.WriteString("\n当前步骤完成判定:\n")
|
||||||
|
sb.WriteString(strings.TrimSpace(currentStep.DoneWhen))
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString("当前 plan 已存在,但当前步骤索引无效;请不要擅自执行其他步骤。\n")
|
sb.WriteString("当前 plan 已存在,但当前步骤索引无效;请不要擅自执行其他步骤。\n")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,24 +8,17 @@ import (
|
|||||||
"github.com/cloudwego/eino/schema"
|
"github.com/cloudwego/eino/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// PlanDoneSignal 表示“规划阶段结束,可以进入 confirm 或下一阶段”。
|
|
||||||
//
|
|
||||||
// TODO(newagent/node): 后续由 planNode 读取模型输出时识别这个信号,并据此调用 state.FinishPlan(...)。
|
|
||||||
PlanDoneSignal = "[PLAN_DONE]"
|
|
||||||
)
|
|
||||||
|
|
||||||
const planSystemPrompt = `
|
const planSystemPrompt = `
|
||||||
你是 SmartFlow NewAgent 的规划器。
|
你是 SmartFlow NewAgent 的规划器。
|
||||||
|
你的职责不是直接执行任务,而是先把用户意图拆成一组清晰、稳定、可逐步执行的自然语言计划,并严格按后端约定的 JSON 协议输出。
|
||||||
你的职责不是直接执行任务,而是先把用户意图拆成一组清晰、稳定、可逐步执行的自然语言计划。
|
|
||||||
|
|
||||||
请遵守以下规则:
|
请遵守以下规则:
|
||||||
1. 只负责规划,不要假装已经调用了工具,也不要伪造执行结果。
|
1. 只负责规划,不要假装已经调用了工具,也不要伪造执行结果。
|
||||||
2. 每一轮只推进一步规划;如果信息不足,可以明确指出缺口。
|
2. 每一轮只推进一步规划;如果信息不足,应明确转成 ask_user,而不是继续硬猜。
|
||||||
3. 若当前计划仍不完整,就继续围绕当前任务补全计划,不要跳去执行细节。
|
3. 若当前计划仍不完整,就继续围绕当前任务补全计划,不要跳去执行细节。
|
||||||
4. 若你认为计划已经完整可执行,请在输出中显式带上 ` + "`" + `[` + `PLAN_DONE` + `]` + "`" + ` 信号。
|
4. 若你认为计划已经完整可执行,请返回 action=plan_done,并附带完整 plan_steps。
|
||||||
5. 计划必须使用自然语言,便于后端将完整 plan 重新注入到后续上下文顶部。
|
5. plan_steps 必须使用自然语言,便于后端将完整 plan 重新注入到后续上下文顶部。
|
||||||
|
6. 只输出 JSON,不要输出 markdown,不要输出额外解释,不要在 JSON 外再补文字。
|
||||||
|
|
||||||
你会看到:
|
你会看到:
|
||||||
- 当前阶段与轮次信息
|
- 当前阶段与轮次信息
|
||||||
@@ -47,10 +40,8 @@ func BuildPlanSystemPrompt() string {
|
|||||||
//
|
//
|
||||||
// 职责边界:
|
// 职责边界:
|
||||||
// 1. 负责把 state + context 收敛成规划阶段模型输入;
|
// 1. 负责把 state + context 收敛成规划阶段模型输入;
|
||||||
// 2. 负责把“置顶上下文”和“工具摘要”放到 history 前面,降低模型跑偏概率;
|
// 2. 负责把置顶上下文和工具摘要放在 history 前面,降低模型跑偏概率;
|
||||||
// 3. 不负责解析模型输出,不负责判断是否真的完成规划。
|
// 3. 不负责解析模型输出,也不负责判断规划质量。
|
||||||
//
|
|
||||||
// TODO(newagent/node): 后续 planNode 直接复用这个入口,不要在节点里散落拼 message 的逻辑。
|
|
||||||
func BuildPlanMessages(state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext, userInput string) []*schema.Message {
|
func BuildPlanMessages(state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext, userInput string) []*schema.Message {
|
||||||
return buildStageMessages(
|
return buildStageMessages(
|
||||||
BuildPlanSystemPrompt(),
|
BuildPlanSystemPrompt(),
|
||||||
@@ -60,21 +51,15 @@ func BuildPlanMessages(state *newagentmodel.CommonState, ctx *newagentmodel.Conv
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BuildPlanUserPrompt 构造规划阶段的用户提示词。
|
// BuildPlanUserPrompt 构造规划阶段的用户提示词。
|
||||||
//
|
|
||||||
// 设计目标:
|
|
||||||
// 1. 把当前阶段、轮次、既有 plan、当前步骤等控制信息显式写给模型;
|
|
||||||
// 2. 保持自然语言风格,方便你后续继续改成自己想要的控制协议;
|
|
||||||
// 3. 用户原始输入单独放在末尾,避免被系统拼装信息淹没。
|
|
||||||
func BuildPlanUserPrompt(state *newagentmodel.CommonState, userInput string) string {
|
func BuildPlanUserPrompt(state *newagentmodel.CommonState, userInput string) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
sb.WriteString("请继续当前任务的规划阶段。\n")
|
sb.WriteString("请继续当前任务的规划阶段。\n")
|
||||||
sb.WriteString(renderStateSummary(state))
|
sb.WriteString(renderStateSummary(state))
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
sb.WriteString("本轮目标:围绕当前任务继续规划,直到形成一份稳定、可执行的自然语言 plan。\n")
|
sb.WriteString("本轮目标:围绕当前任务继续规划,直到形成一份稳定、可执行的自然语言 plan,或在信息不足时明确追问用户。\n\n")
|
||||||
sb.WriteString("如果计划已经完整,请显式输出 ")
|
sb.WriteString(BuildPlanDecisionContractText())
|
||||||
sb.WriteString(PlanDoneSignal)
|
sb.WriteString("\n")
|
||||||
sb.WriteString("。\n")
|
|
||||||
|
|
||||||
trimmedInput := strings.TrimSpace(userInput)
|
trimmedInput := strings.TrimSpace(userInput)
|
||||||
if trimmedInput != "" {
|
if trimmedInput != "" {
|
||||||
@@ -86,10 +71,61 @@ func BuildPlanUserPrompt(state *newagentmodel.CommonState, userInput string) str
|
|||||||
return strings.TrimSpace(sb.String())
|
return strings.TrimSpace(sb.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildPlanDecisionContractText 返回规划阶段的输出协议说明。
|
||||||
|
func BuildPlanDecisionContractText() string {
|
||||||
|
return strings.TrimSpace(fmt.Sprintf(`
|
||||||
|
输出协议(严格 JSON):
|
||||||
|
- speak:给用户看的话;若 action=%s,这里通常就是要追问用户的问题
|
||||||
|
- action:只能是 %s / %s / %s
|
||||||
|
- reason:给后端和日志看的简短说明
|
||||||
|
- plan_steps:仅当 action=%s 时允许返回;返回时必须是完整计划,不是增量
|
||||||
|
- plan_steps[].content:步骤正文,必填
|
||||||
|
- plan_steps[].done_when:可选,建议写“什么情况下算这一步做完”
|
||||||
|
|
||||||
|
合法示例:
|
||||||
|
{
|
||||||
|
"speak": "我先把计划再收束一下。",
|
||||||
|
"action": "%s",
|
||||||
|
"reason": "当前信息已足够继续规划"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"speak": "你更希望我优先安排今天,还是按整周来规划?",
|
||||||
|
"action": "%s",
|
||||||
|
"reason": "当前时间范围仍不明确"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"speak": "计划已经整理好了,我先给你确认一下。",
|
||||||
|
"action": "%s",
|
||||||
|
"reason": "当前计划已具备执行条件",
|
||||||
|
"plan_steps": [
|
||||||
|
{
|
||||||
|
"content": "先确认本周可用时间范围",
|
||||||
|
"done_when": "拿到明确的可用时间段列表"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "基于可用时间生成执行安排",
|
||||||
|
"done_when": "得到一份用户可确认的安排方案"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
newagentmodel.PlanActionAskUser,
|
||||||
|
newagentmodel.PlanActionContinue,
|
||||||
|
newagentmodel.PlanActionAskUser,
|
||||||
|
newagentmodel.PlanActionDone,
|
||||||
|
newagentmodel.PlanActionDone,
|
||||||
|
newagentmodel.PlanActionContinue,
|
||||||
|
newagentmodel.PlanActionAskUser,
|
||||||
|
newagentmodel.PlanActionDone,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
// buildStageMessages 组装某个阶段通用的 messages。
|
// buildStageMessages 组装某个阶段通用的 messages。
|
||||||
//
|
//
|
||||||
// 步骤说明:
|
// 步骤说明:
|
||||||
// 1. 先合并 context 自带 system prompt 与阶段 prompt,保证通用约束和阶段约束都能生效;
|
// 1. 先合并 context 自带 system prompt 与阶段 prompt,保证通用约束和阶段约束都生效;
|
||||||
// 2. 再把置顶上下文块和工具摘要补成 system message,尽量顶在 history 前面;
|
// 2. 再把置顶上下文块和工具摘要补成 system message,尽量顶在 history 前面;
|
||||||
// 3. 最后追加历史消息与本轮 user prompt,保持“新约束在前、历史在后”的稳定顺序。
|
// 3. 最后追加历史消息与本轮 user prompt,保持“新约束在前、历史在后”的稳定顺序。
|
||||||
func buildStageMessages(stageSystemPrompt string, ctx *newagentmodel.ConversationContext, runtimeUserPrompt string) []*schema.Message {
|
func buildStageMessages(stageSystemPrompt string, ctx *newagentmodel.ConversationContext, runtimeUserPrompt string) []*schema.Message {
|
||||||
@@ -123,10 +159,10 @@ func buildStageMessages(stageSystemPrompt string, ctx *newagentmodel.Conversatio
|
|||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderStateSummary 将当前流程状态渲染成简洁文本。
|
// renderStateSummary 把当前流程状态渲染成简洁文本。
|
||||||
func renderStateSummary(state *newagentmodel.CommonState) string {
|
func renderStateSummary(state *newagentmodel.CommonState) string {
|
||||||
if state == nil {
|
if state == nil {
|
||||||
return "当前状态:state 缺失,请先进行兜底处理。"
|
return "当前状态:state 缺失,请先做兜底处理。"
|
||||||
}
|
}
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
@@ -142,22 +178,30 @@ func renderStateSummary(state *newagentmodel.CommonState) string {
|
|||||||
|
|
||||||
sb.WriteString("当前完整 plan:\n")
|
sb.WriteString("当前完整 plan:\n")
|
||||||
for i, step := range state.PlanSteps {
|
for i, step := range state.PlanSteps {
|
||||||
sb.WriteString(fmt.Sprintf("%d. %s\n", i+1, strings.TrimSpace(step)))
|
sb.WriteString(fmt.Sprintf("%d. %s\n", i+1, strings.TrimSpace(step.Content)))
|
||||||
|
if strings.TrimSpace(step.DoneWhen) != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf(" 完成判定:%s\n", strings.TrimSpace(step.DoneWhen)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if step, ok := state.CurrentPlanStep(); ok {
|
if step, ok := state.CurrentPlanStep(); ok {
|
||||||
sb.WriteString(fmt.Sprintf("当前步骤进度:%d/%d\n", current, total))
|
sb.WriteString(fmt.Sprintf("当前步骤进度:%d/%d\n", current, total))
|
||||||
sb.WriteString("当前步骤内容:\n")
|
sb.WriteString("当前步骤内容:\n")
|
||||||
sb.WriteString(step)
|
sb.WriteString(strings.TrimSpace(step.Content))
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
|
if strings.TrimSpace(step.DoneWhen) != "" {
|
||||||
|
sb.WriteString("当前步骤完成判定:\n")
|
||||||
|
sb.WriteString(strings.TrimSpace(step.DoneWhen))
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString("当前步骤进度:暂无有效当前步骤。\n")
|
sb.WriteString("当前步骤进度:暂时无有效当前步骤。\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderPinnedBlocks 将 ConversationContext 中的置顶块渲染成一段独立的 system 内容。
|
// renderPinnedBlocks 把 ConversationContext 中的置顶块渲染成独立的 system 文本。
|
||||||
func renderPinnedBlocks(ctx *newagentmodel.ConversationContext) string {
|
func renderPinnedBlocks(ctx *newagentmodel.ConversationContext) string {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -186,7 +230,7 @@ func renderPinnedBlocks(ctx *newagentmodel.ConversationContext) string {
|
|||||||
return strings.TrimSpace(sb.String())
|
return strings.TrimSpace(sb.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderToolSchemas 将工具摘要渲染成独立文本块。
|
// renderToolSchemas 把工具摘要渲染成独立文本块。
|
||||||
func renderToolSchemas(ctx *newagentmodel.ConversationContext) string {
|
func renderToolSchemas(ctx *newagentmodel.ConversationContext) string {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package newagentstream
|
package newagentstream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PayloadEmitter 是真正向外层 SSE 管道写 chunk 的最小接口。
|
// PayloadEmitter 是真正向外层 SSE 管道写 chunk 的最小接口。
|
||||||
@@ -13,9 +15,52 @@ import (
|
|||||||
// 3. 只要签名是 `func(string) error`,都能接进来。
|
// 3. 只要签名是 `func(string) error`,都能接进来。
|
||||||
type PayloadEmitter func(payload string) error
|
type PayloadEmitter func(payload string) error
|
||||||
|
|
||||||
// StageEmitter 是 graph/node 对“当前阶段”进行推送的最小接口。
|
// StageEmitter 是 graph/node 对“当前阶段”进行推送的兼容接口。
|
||||||
|
//
|
||||||
|
// 设计说明:
|
||||||
|
// 1. 旧调用侧仍然只关心 stage/detail 两段文本,因此这里先保留;
|
||||||
|
// 2. 新的结构化事件能力会通过 ChunkEmitter 补齐,而不是继续扩展这个函数签名;
|
||||||
|
// 3. 这样能兼顾当前兼容性和后续协议升级空间。
|
||||||
type StageEmitter func(stage, detail string)
|
type StageEmitter func(stage, detail string)
|
||||||
|
|
||||||
|
// PseudoStreamOptions 描述“整段文字伪流式输出”的切块与节奏配置。
|
||||||
|
//
|
||||||
|
// 字段语义:
|
||||||
|
// 1. MinChunkRunes:达到该最小长度后,若命中标点/换行等边界,可提前切块;
|
||||||
|
// 2. MaxChunkRunes:单块最大 rune 数,超过后强制切块,避免一次性发太长;
|
||||||
|
// 3. ChunkInterval:块与块之间的等待时间;为 0 时表示只做切块,不做人为延迟。
|
||||||
|
type PseudoStreamOptions struct {
|
||||||
|
MinChunkRunes int
|
||||||
|
MaxChunkRunes int
|
||||||
|
ChunkInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultPseudoStreamMinChunkRunes = 8
|
||||||
|
defaultPseudoStreamMaxChunkRunes = 24
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultPseudoStreamOptions 返回一份适合中文短句展示的默认伪流式配置。
|
||||||
|
func DefaultPseudoStreamOptions() PseudoStreamOptions {
|
||||||
|
return PseudoStreamOptions{
|
||||||
|
MinChunkRunes: defaultPseudoStreamMinChunkRunes,
|
||||||
|
MaxChunkRunes: defaultPseudoStreamMaxChunkRunes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChunkEmitter 是 newAgent 统一的 SSE chunk 发射器。
|
||||||
|
//
|
||||||
|
// 职责边界:
|
||||||
|
// 1. 负责把“正文 / 思考 / 工具事件 / 确认请求 / 中断提示”统一转换成 OpenAI 兼容 payload;
|
||||||
|
// 2. 负责在必要时把结构化事件附带成 extra,同时给当前前端提供可读的降级文本;
|
||||||
|
// 3. 不负责决定什么时候发什么,也不负责持久化状态。
|
||||||
|
type ChunkEmitter struct {
|
||||||
|
emit PayloadEmitter
|
||||||
|
RequestID string
|
||||||
|
ModelName string
|
||||||
|
Created int64
|
||||||
|
}
|
||||||
|
|
||||||
// NoopPayloadEmitter 返回一个空实现,便于骨架期安全占位。
|
// NoopPayloadEmitter 返回一个空实现,便于骨架期安全占位。
|
||||||
func NoopPayloadEmitter() PayloadEmitter {
|
func NoopPayloadEmitter() PayloadEmitter {
|
||||||
return func(string) error { return nil }
|
return func(string) error { return nil }
|
||||||
@@ -34,69 +79,320 @@ func WrapStageEmitter(fn func(stage, detail string)) StageEmitter {
|
|||||||
return fn
|
return fn
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmitStageAsReasoning 把“阶段提示”伪装成 reasoning chunk 推给前端。
|
// NewChunkEmitter 创建统一 chunk 发射器。
|
||||||
//
|
//
|
||||||
// 设计背景:
|
// 兜底策略:
|
||||||
// 1. 你当前 Apifox 只认思考块和正文块,因此阶段提示需要先借 reasoning_content 走通;
|
// 1. emit 为空时回退到 Noop,避免骨架期到处判空;
|
||||||
// 2. 这样后续真正前端上线时,只需要在这一层换协议,而不必回到各 skill 重改 graph;
|
// 2. modelName 为空时回填 worker,保持 OpenAI 兼容字段稳定;
|
||||||
// 3. 这里不拼花哨格式,只给出稳定、可读、可 grep 的文本。
|
// 3. created <= 0 时用当前时间兜底,避免上层还没决定时间戳就无法复用。
|
||||||
func EmitStageAsReasoning(emit PayloadEmitter, requestID, modelName string, created int64, stage, detail string, includeRole bool) error {
|
func NewChunkEmitter(emit PayloadEmitter, requestID, modelName string, created int64) *ChunkEmitter {
|
||||||
if emit == nil {
|
if emit == nil {
|
||||||
|
emit = NoopPayloadEmitter()
|
||||||
|
}
|
||||||
|
|
||||||
|
modelName = strings.TrimSpace(modelName)
|
||||||
|
if modelName == "" {
|
||||||
|
modelName = "worker"
|
||||||
|
}
|
||||||
|
if created <= 0 {
|
||||||
|
created = time.Now().Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ChunkEmitter{
|
||||||
|
emit: emit,
|
||||||
|
RequestID: strings.TrimSpace(requestID),
|
||||||
|
ModelName: modelName,
|
||||||
|
Created: created,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmitReasoningText 输出一段 reasoning 文字,并附带 reasoning_text extra。
|
||||||
|
func (e *ChunkEmitter) EmitReasoningText(blockID, stage, text string, includeRole bool) error {
|
||||||
|
if e == nil || e.emit == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
text := BuildStageReasoningText(stage, detail)
|
text = strings.TrimSpace(text)
|
||||||
payload, err := ToOpenAIReasoningChunk(requestID, modelName, created, text, includeRole)
|
if text == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := ToOpenAIReasoningChunkWithExtra(
|
||||||
|
e.RequestID,
|
||||||
|
e.ModelName,
|
||||||
|
e.Created,
|
||||||
|
text,
|
||||||
|
includeRole,
|
||||||
|
NewReasoningTextExtra(blockID, stage),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if payload == "" {
|
if payload == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return emit(payload)
|
return e.emit(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmitAssistantText 输出一段 assistant 正文,并附带 assistant_text extra。
|
||||||
|
func (e *ChunkEmitter) EmitAssistantText(blockID, stage, text string, includeRole bool) error {
|
||||||
|
if e == nil || e.emit == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
if text == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := ToOpenAIAssistantChunkWithExtra(
|
||||||
|
e.RequestID,
|
||||||
|
e.ModelName,
|
||||||
|
e.Created,
|
||||||
|
text,
|
||||||
|
includeRole,
|
||||||
|
NewAssistantTextExtra(blockID, stage),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if payload == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.emit(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmitPseudoReasoningText 把整段 reasoning 文本按伪流式方式逐块推出。
|
||||||
|
func (e *ChunkEmitter) EmitPseudoReasoningText(ctx context.Context, blockID, stage, text string, options PseudoStreamOptions) error {
|
||||||
|
return e.emitPseudoText(
|
||||||
|
ctx,
|
||||||
|
text,
|
||||||
|
options,
|
||||||
|
func(chunk string, includeRole bool) error {
|
||||||
|
return e.EmitReasoningText(blockID, stage, chunk, includeRole)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmitPseudoAssistantText 把整段 assistant 文本按伪流式方式逐块推出。
|
||||||
|
func (e *ChunkEmitter) EmitPseudoAssistantText(ctx context.Context, blockID, stage, text string, options PseudoStreamOptions) error {
|
||||||
|
return e.emitPseudoText(
|
||||||
|
ctx,
|
||||||
|
text,
|
||||||
|
options,
|
||||||
|
func(chunk string, includeRole bool) error {
|
||||||
|
return e.EmitAssistantText(blockID, stage, chunk, includeRole)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmitStatus 输出一条阶段状态事件。
|
||||||
|
//
|
||||||
|
// 当前兼容策略:
|
||||||
|
// 1. extra 用 status 表达结构化语义;
|
||||||
|
// 2. reasoning_content 里同时放一份可读降级文本,保证旧前端也能看到。
|
||||||
|
func (e *ChunkEmitter) EmitStatus(blockID, stage, code, summary string, includeRole bool) error {
|
||||||
|
if e == nil || e.emit == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
text := BuildStageReasoningText(stage, summary)
|
||||||
|
payload, err := ToOpenAIReasoningChunkWithExtra(
|
||||||
|
e.RequestID,
|
||||||
|
e.ModelName,
|
||||||
|
e.Created,
|
||||||
|
text,
|
||||||
|
includeRole,
|
||||||
|
NewStatusExtra(blockID, stage, code, summary),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if payload == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.emit(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmitToolCallStart 输出一次工具调用开始事件。
|
||||||
|
func (e *ChunkEmitter) EmitToolCallStart(blockID, stage, toolName, summary, argumentsPreview string, includeRole bool) error {
|
||||||
|
if e == nil || e.emit == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
text := BuildToolCallReasoningText(toolName, summary, argumentsPreview)
|
||||||
|
payload, err := ToOpenAIReasoningChunkWithExtra(
|
||||||
|
e.RequestID,
|
||||||
|
e.ModelName,
|
||||||
|
e.Created,
|
||||||
|
text,
|
||||||
|
includeRole,
|
||||||
|
NewToolCallExtra(blockID, stage, toolName, "start", summary, argumentsPreview),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if payload == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.emit(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmitToolCallResult 输出一次工具调用结果事件。
|
||||||
|
func (e *ChunkEmitter) EmitToolCallResult(blockID, stage, toolName, summary, argumentsPreview string, includeRole bool) error {
|
||||||
|
if e == nil || e.emit == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
text := BuildToolResultReasoningText(toolName, summary)
|
||||||
|
payload, err := ToOpenAIReasoningChunkWithExtra(
|
||||||
|
e.RequestID,
|
||||||
|
e.ModelName,
|
||||||
|
e.Created,
|
||||||
|
text,
|
||||||
|
includeRole,
|
||||||
|
NewToolResultExtra(blockID, stage, toolName, "done", summary, argumentsPreview),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if payload == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.emit(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmitConfirmRequest 输出一次待确认事件。
|
||||||
|
//
|
||||||
|
// 当前展示策略:
|
||||||
|
// 1. 对旧前端,confirm 文案通过 assistant content 直接可见;
|
||||||
|
// 2. 对新前端,extra.confirm 可直接驱动确认卡片或按钮;
|
||||||
|
// 3. 默认使用伪流式,避免确认文案整块砸下来太生硬。
|
||||||
|
func (e *ChunkEmitter) EmitConfirmRequest(ctx context.Context, blockID, stage, interactionID, title, summary string, options PseudoStreamOptions) error {
|
||||||
|
if e == nil || e.emit == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
text := BuildConfirmAssistantText(title, summary)
|
||||||
|
extra := NewConfirmRequestExtra(blockID, stage, interactionID, title, summary)
|
||||||
|
return e.emitPseudoText(
|
||||||
|
ctx,
|
||||||
|
text,
|
||||||
|
options,
|
||||||
|
func(chunk string, includeRole bool) error {
|
||||||
|
payload, err := ToOpenAIAssistantChunkWithExtra(
|
||||||
|
e.RequestID,
|
||||||
|
e.ModelName,
|
||||||
|
e.Created,
|
||||||
|
chunk,
|
||||||
|
includeRole,
|
||||||
|
extra,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if payload == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.emit(payload)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmitInterruptMessage 输出一次中断提示。
|
||||||
|
//
|
||||||
|
// 适用场景:
|
||||||
|
// 1. ask_user 追问;
|
||||||
|
// 2. 告知用户当前会话已进入等待状态;
|
||||||
|
// 3. 后续 connection_lost 恢复若需要对用户补一句解释,也可复用这一入口。
|
||||||
|
func (e *ChunkEmitter) EmitInterruptMessage(ctx context.Context, blockID, stage, interactionID, interactionType, summary string, options PseudoStreamOptions) error {
|
||||||
|
if e == nil || e.emit == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
text := BuildInterruptAssistantText(interactionType, summary)
|
||||||
|
extra := NewInterruptExtra(blockID, stage, interactionID, interactionType, summary)
|
||||||
|
return e.emitPseudoText(
|
||||||
|
ctx,
|
||||||
|
text,
|
||||||
|
options,
|
||||||
|
func(chunk string, includeRole bool) error {
|
||||||
|
payload, err := ToOpenAIAssistantChunkWithExtra(
|
||||||
|
e.RequestID,
|
||||||
|
e.ModelName,
|
||||||
|
e.Created,
|
||||||
|
chunk,
|
||||||
|
includeRole,
|
||||||
|
extra,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if payload == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.emit(payload)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmitFinish 统一输出 stop 结束块,并带上 finish extra。
|
||||||
|
func (e *ChunkEmitter) EmitFinish(blockID, stage string) error {
|
||||||
|
if e == nil || e.emit == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := ToOpenAIFinishStreamWithExtra(
|
||||||
|
e.RequestID,
|
||||||
|
e.ModelName,
|
||||||
|
e.Created,
|
||||||
|
NewFinishExtra(blockID, stage),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if payload == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.emit(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmitDone 统一输出 OpenAI 兼容流式结束标记。
|
||||||
|
func (e *ChunkEmitter) EmitDone() error {
|
||||||
|
if e == nil || e.emit == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.emit("[DONE]")
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmitStageAsReasoning 把“阶段提示”伪装成 reasoning chunk 推给前端。
|
||||||
|
//
|
||||||
|
// 兼容说明:
|
||||||
|
// 1. 保留旧函数签名,方便当前旧链路直接复用;
|
||||||
|
// 2. 实际实现已升级为统一的 ChunkEmitter + status extra;
|
||||||
|
// 3. 这样后续新链路可以直接跳过这个兼容函数,转用结构化方法。
|
||||||
|
func EmitStageAsReasoning(emit PayloadEmitter, requestID, modelName string, created int64, stage, detail string, includeRole bool) error {
|
||||||
|
return NewChunkEmitter(emit, requestID, modelName, created).EmitStatus(stage, stage, stage, detail, includeRole)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmitAssistantReply 把一段完整正文作为 assistant chunk 推出。
|
// EmitAssistantReply 把一段完整正文作为 assistant chunk 推出。
|
||||||
//
|
//
|
||||||
// 注意:
|
// 注意:
|
||||||
// 1. 这里是“整段发”,不是把文本强行拆碎;
|
// 1. 这里保持“整段发”,不主动切块;
|
||||||
// 2. 这样后续如果某条链路不需要真流式,也可以复用统一出口;
|
// 2. 若后续某条链路需要更自然的阅读节奏,应直接调用 EmitPseudoAssistantText;
|
||||||
// 3. 真正按 token/chunk 细粒度流式输出,应由 llm.Stream + 上层循环处理。
|
// 3. 为兼容老调用侧,这里 blockID 和 stage 都留空。
|
||||||
func EmitAssistantReply(emit PayloadEmitter, requestID, modelName string, created int64, content string, includeRole bool) error {
|
func EmitAssistantReply(emit PayloadEmitter, requestID, modelName string, created int64, content string, includeRole bool) error {
|
||||||
if emit == nil {
|
return NewChunkEmitter(emit, requestID, modelName, created).EmitAssistantText("", "", content, includeRole)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
payload, err := ToOpenAIAssistantChunk(requestID, modelName, created, content, includeRole)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if payload == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return emit(payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmitFinish 统一输出 stop 结束块。
|
// EmitFinish 统一输出 stop 结束块。
|
||||||
func EmitFinish(emit PayloadEmitter, requestID, modelName string, created int64) error {
|
func EmitFinish(emit PayloadEmitter, requestID, modelName string, created int64) error {
|
||||||
if emit == nil {
|
return NewChunkEmitter(emit, requestID, modelName, created).EmitFinish("", "")
|
||||||
return nil
|
|
||||||
}
|
|
||||||
payload, err := ToOpenAIFinishStream(requestID, modelName, created)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if payload == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return emit(payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmitDone 统一输出 OpenAI 兼容流式结束标记。
|
// EmitDone 统一输出 OpenAI 兼容流式结束标记。
|
||||||
func EmitDone(emit PayloadEmitter) error {
|
func EmitDone(emit PayloadEmitter) error {
|
||||||
if emit == nil {
|
return NewChunkEmitter(emit, "", "", 0).EmitDone()
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return emit("[DONE]")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildStageReasoningText 生成统一阶段提示文本。
|
// BuildStageReasoningText 生成统一阶段提示文本。
|
||||||
@@ -113,3 +409,187 @@ func BuildStageReasoningText(stage, detail string) string {
|
|||||||
return detail
|
return detail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildToolCallReasoningText 生成“工具调用开始”时的可读提示文本。
|
||||||
|
func BuildToolCallReasoningText(toolName, summary, argumentsPreview string) string {
|
||||||
|
toolName = strings.TrimSpace(toolName)
|
||||||
|
summary = strings.TrimSpace(summary)
|
||||||
|
argumentsPreview = strings.TrimSpace(argumentsPreview)
|
||||||
|
|
||||||
|
lines := make([]string, 0, 3)
|
||||||
|
if toolName != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf("正在调用工具:%s", toolName))
|
||||||
|
}
|
||||||
|
if summary != "" {
|
||||||
|
lines = append(lines, summary)
|
||||||
|
}
|
||||||
|
if argumentsPreview != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf("参数摘要:%s", argumentsPreview))
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(strings.Join(lines, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildToolResultReasoningText 生成“工具调用结果”时的可读提示文本。
|
||||||
|
func BuildToolResultReasoningText(toolName, summary string) string {
|
||||||
|
toolName = strings.TrimSpace(toolName)
|
||||||
|
summary = strings.TrimSpace(summary)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case toolName != "" && summary != "":
|
||||||
|
return fmt.Sprintf("工具结果:%s\n%s", toolName, summary)
|
||||||
|
case toolName != "":
|
||||||
|
return fmt.Sprintf("工具结果:%s", toolName)
|
||||||
|
default:
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildConfirmAssistantText 生成给用户看的确认文案。
|
||||||
|
func BuildConfirmAssistantText(title, summary string) string {
|
||||||
|
title = strings.TrimSpace(title)
|
||||||
|
summary = strings.TrimSpace(summary)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case title != "" && summary != "":
|
||||||
|
return fmt.Sprintf("%s\n%s", title, summary)
|
||||||
|
case title != "":
|
||||||
|
return title
|
||||||
|
default:
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildInterruptAssistantText 生成给用户看的中断文案。
|
||||||
|
func BuildInterruptAssistantText(interactionType, summary string) string {
|
||||||
|
interactionType = strings.TrimSpace(interactionType)
|
||||||
|
summary = strings.TrimSpace(summary)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case interactionType != "" && summary != "":
|
||||||
|
return fmt.Sprintf("当前进入 %s 阶段。\n%s", interactionType, summary)
|
||||||
|
case summary != "":
|
||||||
|
return summary
|
||||||
|
default:
|
||||||
|
return interactionType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ChunkEmitter) emitPseudoText(ctx context.Context, text string, options PseudoStreamOptions, emitChunk func(chunk string, includeRole bool) error) error {
|
||||||
|
if emitChunk == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
if text == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks := SplitPseudoStreamText(text, options)
|
||||||
|
for i, chunk := range chunks {
|
||||||
|
if err := emitChunk(chunk, i == 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if i < len(chunks)-1 {
|
||||||
|
if err := waitPseudoStreamInterval(ctx, options.ChunkInterval); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitPseudoStreamText 按“标点优先、长度兜底”的策略切分整段文本。
|
||||||
|
//
|
||||||
|
// 步骤说明:
|
||||||
|
// 1. 优先在句号、问号、感叹号、分号、换行等自然边界切块,保证阅读顺畅;
|
||||||
|
// 2. 若长时间遇不到合适边界,则在 MaxChunkRunes 处强制切块,避免整段卡太久;
|
||||||
|
// 3. 对中文文本优先按 rune 长度处理,避免多字节字符被截断。
|
||||||
|
func SplitPseudoStreamText(text string, options PseudoStreamOptions) []string {
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
if text == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
options = normalizePseudoStreamOptions(options)
|
||||||
|
runes := []rune(text)
|
||||||
|
if len(runes) <= options.MaxChunkRunes {
|
||||||
|
return []string{text}
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks := make([]string, 0, len(runes)/options.MinChunkRunes+1)
|
||||||
|
start := 0
|
||||||
|
size := 0
|
||||||
|
for i, r := range runes {
|
||||||
|
size++
|
||||||
|
|
||||||
|
shouldFlush := false
|
||||||
|
if size >= options.MaxChunkRunes {
|
||||||
|
shouldFlush = true
|
||||||
|
}
|
||||||
|
if size >= options.MinChunkRunes && isPseudoStreamBoundary(r) {
|
||||||
|
shouldFlush = true
|
||||||
|
}
|
||||||
|
if !shouldFlush {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk := strings.TrimSpace(string(runes[start : i+1]))
|
||||||
|
if chunk != "" {
|
||||||
|
chunks = append(chunks, chunk)
|
||||||
|
}
|
||||||
|
start = i + 1
|
||||||
|
size = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if start < len(runes) {
|
||||||
|
chunk := strings.TrimSpace(string(runes[start:]))
|
||||||
|
if chunk != "" {
|
||||||
|
chunks = append(chunks, chunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(chunks) == 0 {
|
||||||
|
return []string{text}
|
||||||
|
}
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePseudoStreamOptions(options PseudoStreamOptions) PseudoStreamOptions {
|
||||||
|
if options.MinChunkRunes <= 0 {
|
||||||
|
options.MinChunkRunes = defaultPseudoStreamMinChunkRunes
|
||||||
|
}
|
||||||
|
if options.MaxChunkRunes <= 0 {
|
||||||
|
options.MaxChunkRunes = defaultPseudoStreamMaxChunkRunes
|
||||||
|
}
|
||||||
|
if options.MaxChunkRunes < options.MinChunkRunes {
|
||||||
|
options.MaxChunkRunes = options.MinChunkRunes
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPseudoStreamBoundary(r rune) bool {
|
||||||
|
switch r {
|
||||||
|
case '。', '!', '?', ';', ':', ',', '.', '!', '?', ';', ':', ',', '\n':
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitPseudoStreamInterval(ctx context.Context, interval time.Duration) error {
|
||||||
|
if interval <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
timer := time.NewTimer(interval)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-timer.C:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,16 +8,17 @@ import (
|
|||||||
|
|
||||||
// OpenAIChunkResponse 是 OpenAI 兼容的流式 chunk DTO。
|
// OpenAIChunkResponse 是 OpenAI 兼容的流式 chunk DTO。
|
||||||
//
|
//
|
||||||
// 之所以单独放到 Agent/stream:
|
// 设计说明:
|
||||||
// 1. 未来无论 quicknote、taskquery 还是 schedule,只要需要 SSE 都会复用这套协议壳;
|
// 1. 外层继续保持 OpenAI 兼容壳,避免前端和调试工具一次性大改;
|
||||||
// 2. 这样 node/graph 层只关注“我要推什么内容”,不再自己拼 JSON;
|
// 2. 新增顶层 Extra 字段,用来承载“工具调用 / 确认请求 / 中断恢复”等结构化事件;
|
||||||
// 3. 后续如果前端协议升级,也能在这里集中改。
|
// 3. 这样旧前端仍可继续读取 delta.content / delta.reasoning_content,新前端则可渐进消费 extra。
|
||||||
type OpenAIChunkResponse struct {
|
type OpenAIChunkResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Object string `json:"object"`
|
Object string `json:"object"`
|
||||||
Created int64 `json:"created"`
|
Created int64 `json:"created"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Choices []OpenAIChunkChoice `json:"choices"`
|
Choices []OpenAIChunkChoice `json:"choices,omitempty"`
|
||||||
|
Extra *OpenAIChunkExtra `json:"extra,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenAIChunkChoice 对应 OpenAI choices[0]。
|
// OpenAIChunkChoice 对应 OpenAI choices[0]。
|
||||||
@@ -34,13 +35,87 @@ type OpenAIChunkDelta struct {
|
|||||||
ReasoningContent string `json:"reasoning_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。
|
// 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 映射到协议字段;
|
// 1. 负责把 chunk.Content / chunk.ReasoningContent 映射到协议字段;
|
||||||
// 2. 负责按 includeRole 决定是否在首块带上 assistant 角色;
|
// 2. 负责挂载可选 extra,供前端识别工具调用、确认请求等结构化事件;
|
||||||
// 3. 不负责发送,也不负责决定“这个 chunk 该不该推”。
|
// 3. 不负责发送,也不负责决定“这个 chunk 该不该推”。
|
||||||
func ToOpenAIStream(chunk *schema.Message, requestID, modelName string, created int64, includeRole bool) (string, error) {
|
func ToOpenAIStreamWithExtra(chunk *schema.Message, requestID, modelName string, created int64, includeRole bool, extra *OpenAIChunkExtra) (string, error) {
|
||||||
delta := OpenAIChunkDelta{}
|
delta := OpenAIChunkDelta{}
|
||||||
if includeRole {
|
if includeRole {
|
||||||
delta.Role = "assistant"
|
delta.Role = "assistant"
|
||||||
@@ -49,50 +124,177 @@ func ToOpenAIStream(chunk *schema.Message, requestID, modelName string, created
|
|||||||
delta.Content = chunk.Content
|
delta.Content = chunk.Content
|
||||||
delta.ReasoningContent = chunk.ReasoningContent
|
delta.ReasoningContent = chunk.ReasoningContent
|
||||||
}
|
}
|
||||||
return buildOpenAIChunkPayload(requestID, modelName, created, delta, nil)
|
return buildOpenAIChunkPayload(requestID, modelName, created, delta, nil, extra)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToOpenAIReasoningChunk 直接构造一个 reasoning chunk。
|
// ToOpenAIReasoningChunk 直接构造一个 reasoning chunk。
|
||||||
func ToOpenAIReasoningChunk(requestID, modelName string, created int64, reasoning string, includeRole bool) (string, error) {
|
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}
|
delta := OpenAIChunkDelta{ReasoningContent: reasoning}
|
||||||
if includeRole {
|
if includeRole {
|
||||||
delta.Role = "assistant"
|
delta.Role = "assistant"
|
||||||
}
|
}
|
||||||
return buildOpenAIChunkPayload(requestID, modelName, created, delta, nil)
|
return buildOpenAIChunkPayload(requestID, modelName, created, delta, nil, extra)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToOpenAIAssistantChunk 直接构造一个正文 chunk。
|
// ToOpenAIAssistantChunk 直接构造一个正文 chunk。
|
||||||
func ToOpenAIAssistantChunk(requestID, modelName string, created int64, content string, includeRole bool) (string, error) {
|
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}
|
delta := OpenAIChunkDelta{Content: content}
|
||||||
if includeRole {
|
if includeRole {
|
||||||
delta.Role = "assistant"
|
delta.Role = "assistant"
|
||||||
}
|
}
|
||||||
return buildOpenAIChunkPayload(requestID, modelName, created, delta, nil)
|
return buildOpenAIChunkPayload(requestID, modelName, created, delta, nil, extra)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToOpenAIFinishStream 生成流式结束 chunk(finish_reason=stop)。
|
// ToOpenAIFinishStream 生成流式结束 chunk(finish_reason=stop)。
|
||||||
func ToOpenAIFinishStream(requestID, modelName string, created int64) (string, error) {
|
func ToOpenAIFinishStream(requestID, modelName string, created int64) (string, error) {
|
||||||
stop := "stop"
|
return ToOpenAIFinishStreamWithExtra(requestID, modelName, created, nil)
|
||||||
return buildOpenAIChunkPayload(requestID, modelName, created, OpenAIChunkDelta{}, &stop)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildOpenAIChunkPayload(requestID, modelName string, created int64, delta OpenAIChunkDelta, finishReason *string) (string, error) {
|
// ToOpenAIFinishStreamWithExtra 生成带 extra 的流式结束 chunk。
|
||||||
// 1. 若既没有 role,也没有正文/思考,也没有 finish_reason,则视为“空块”,直接跳过。
|
func ToOpenAIFinishStreamWithExtra(requestID, modelName string, created int64, extra *OpenAIChunkExtra) (string, error) {
|
||||||
// 2. 这样可以避免上层每次都自己写一遍空块判断。
|
stop := "stop"
|
||||||
if delta.Role == "" && delta.Content == "" && delta.ReasoningContent == "" && finishReason == nil {
|
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
|
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{
|
dto := OpenAIChunkResponse{
|
||||||
ID: requestID,
|
ID: requestID,
|
||||||
Object: "chat.completion.chunk",
|
Object: "chat.completion.chunk",
|
||||||
Created: created,
|
Created: created,
|
||||||
Model: modelName,
|
Model: modelName,
|
||||||
Choices: []OpenAIChunkChoice{{
|
Choices: choices,
|
||||||
Index: 0,
|
Extra: extra,
|
||||||
Delta: delta,
|
|
||||||
FinishReason: finishReason,
|
|
||||||
}},
|
|
||||||
}
|
}
|
||||||
data, err := json.Marshal(dto)
|
data, err := json.Marshal(dto)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -100,3 +302,18 @@ func buildOpenAIChunkPayload(requestID, modelName string, created int64, delta O
|
|||||||
}
|
}
|
||||||
return string(data), nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user