Version: 0.8.8.dev.260403
后端: 1.新建Deliver节点:LLM生成任务总结,失败降级到机械格式化,伪流式输出 2.新建Confirm节点:确认卡片推送与状态持久化 3.新建Interrupt节点:追问/确认/默认中断三种处理路径 4.实现状态持久化体系:model层定义AgentStateStore接口+AgentStateSnapshot快照,dao/cache.go新增Redis CRUD,agent_nodes层每节点自动存快照、Deliver完成后清理 5.所有model struct补充JSON tags,支持Redis序列化/反序列化 前端:无 仓库:无
This commit is contained in:
@@ -11,8 +11,8 @@ import (
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责把 node 层真正实现的方法统一暴露给 graph 注册;
|
||||
// 2. 负责收口“graph 只编排、node 真执行”的结构约束;
|
||||
// 3. 当前先迁移 Plan,其他节点后续按同样模式逐步下沉。
|
||||
// 2. 负责收口"graph 只编排、node 真执行"的结构约束;
|
||||
// 3. 负责在每个节点执行成功后统一做状态持久化(Save/Delete)。
|
||||
type AgentNodes struct{}
|
||||
|
||||
// NewAgentNodes 创建通用节点容器。
|
||||
@@ -25,7 +25,7 @@ func NewAgentNodes() *AgentNodes {
|
||||
// 职责边界:
|
||||
// 1. 这里只做 graph -> node 的参数转接;
|
||||
// 2. 真正的入口逻辑仍由 RunChatNode 负责;
|
||||
// 3. 这样 graph 层后续只需挂 n.Chat,而不再自己维护占位 chatNode。
|
||||
// 3. Chat 的 Save 交给 Service 层处理,这里不做持久化。
|
||||
func (n *AgentNodes) Chat(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||
if st == nil {
|
||||
return nil, errors.New("chat node: state is nil")
|
||||
@@ -47,12 +47,39 @@ func (n *AgentNodes) Chat(ctx context.Context, st *newagentmodel.AgentGraphState
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// Confirm 是确认阶段的正式节点方法。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 这里只做 graph -> node 的参数转接;
|
||||
// 2. 真正的确认逻辑仍由 RunConfirmNode 负责;
|
||||
// 3. 不需要 LLM Client — 确认内容由已有状态机械格式化。
|
||||
// 4. Confirm 执行成功后保存状态,因为它创建了 PendingInteraction。
|
||||
func (n *AgentNodes) Confirm(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||
if st == nil {
|
||||
return nil, errors.New("confirm node: state is nil")
|
||||
}
|
||||
|
||||
if err := RunConfirmNode(
|
||||
ctx,
|
||||
ConfirmNodeInput{
|
||||
RuntimeState: st.EnsureRuntimeState(),
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||
},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
saveAgentState(ctx, st)
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// Plan 是规划阶段的正式节点方法。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 这里只做 graph -> node 的参数转接;
|
||||
// 2. 真正的单轮规划逻辑仍由 RunPlanNode 负责;
|
||||
// 3. 这样 graph 层后续只需挂 n.Plan,而不再自己维护占位 planNode。
|
||||
// 3. Plan 执行成功后保存状态,支持意外断线恢复。
|
||||
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")
|
||||
@@ -71,6 +98,33 @@ func (n *AgentNodes) Plan(ctx context.Context, st *newagentmodel.AgentGraphState
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
saveAgentState(ctx, st)
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// Interrupt 是中断阶段的正式节点方法。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 这里只做 graph -> node 的参数转接;
|
||||
// 2. 真正的中断逻辑仍由 RunInterruptNode 负责;
|
||||
// 3. 不需要 LLM Client — 所有文本已在 PendingInteraction 里。
|
||||
// 4. 不需要 Save — 上游节点(Plan/Execute/Confirm)已经存过了。
|
||||
func (n *AgentNodes) Interrupt(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||
if st == nil {
|
||||
return nil, errors.New("interrupt node: state is nil")
|
||||
}
|
||||
|
||||
if err := RunInterruptNode(
|
||||
ctx,
|
||||
InterruptNodeInput{
|
||||
RuntimeState: st.EnsureRuntimeState(),
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||
},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
@@ -78,13 +132,13 @@ func (n *AgentNodes) Plan(ctx context.Context, st *newagentmodel.AgentGraphState
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 这里只做 graph -> node 的参数转接;
|
||||
// 2. 真正的单轮执行逻辑仍由 RunExecuteNode 负责;
|
||||
// 3. 这样 graph 层后续只需挂 n.Execute,而不再自己维护占位 executeNode。
|
||||
// 2. 真正的单轮执行逻辑仍由 RunExecuteNode 负责。
|
||||
//
|
||||
// 设计原则:
|
||||
// 1. LLM 主导:LLM 自己判断 done_when 是否满足,自己决定何时推进/完成;
|
||||
// 2. 后端兜底:只做资源控制、安全兜底、证据记录;
|
||||
// 3. 不做硬校验:后端不质疑 LLM 的 advance/complete 决策。
|
||||
// 4. Execute 每轮执行成功后保存状态,支持意外断线恢复。
|
||||
func (n *AgentNodes) Execute(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||
if st == nil {
|
||||
return nil, errors.New("execute node: state is nil")
|
||||
@@ -103,5 +157,102 @@ func (n *AgentNodes) Execute(ctx context.Context, st *newagentmodel.AgentGraphSt
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
saveAgentState(ctx, st)
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// Deliver 是交付阶段的正式节点方法。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 这里只做 graph -> node 的参数转接;
|
||||
// 2. 真正的交付逻辑仍由 RunDeliverNode 负责;
|
||||
// 3. 调 LLM 生成任务总结,失败时降级到机械格式化。
|
||||
// 4. 任务完成后删除 Redis 快照,清理持久化状态。
|
||||
func (n *AgentNodes) Deliver(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
|
||||
if st == nil {
|
||||
return nil, errors.New("deliver node: state is nil")
|
||||
}
|
||||
|
||||
if err := RunDeliverNode(
|
||||
ctx,
|
||||
DeliverNodeInput{
|
||||
RuntimeState: st.EnsureRuntimeState(),
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
Client: st.Deps.ResolveDeliverClient(),
|
||||
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||
},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deleteAgentState(ctx, st)
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// --- 持久化辅助 ---
|
||||
|
||||
// saveAgentState 在节点执行成功后,将当前运行态快照保存到 Redis。
|
||||
//
|
||||
// 设计原则:
|
||||
// 1. Save 失败只记日志,不中断 Graph 流程;
|
||||
// 2. StateStore 为空时静默跳过(骨架期 / 测试环境);
|
||||
// 3. conversationID 为空时也静默跳过,避免写入无效 key。
|
||||
//
|
||||
// TODO: 接入项目统一的日志框架后,把 _ = err 改成结构化日志。
|
||||
func saveAgentState(ctx context.Context, st *newagentmodel.AgentGraphState) {
|
||||
if st == nil {
|
||||
return
|
||||
}
|
||||
store := st.Deps.StateStore
|
||||
if store == nil {
|
||||
return
|
||||
}
|
||||
|
||||
runtimeState := st.EnsureRuntimeState()
|
||||
if runtimeState == nil {
|
||||
return
|
||||
}
|
||||
|
||||
flowState := runtimeState.EnsureCommonState()
|
||||
if flowState == nil || flowState.ConversationID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
snapshot := &newagentmodel.AgentStateSnapshot{
|
||||
RuntimeState: runtimeState,
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
}
|
||||
|
||||
_ = store.Save(ctx, flowState.ConversationID, snapshot)
|
||||
}
|
||||
|
||||
// deleteAgentState 在任务完成后,删除 Redis 中的运行态快照。
|
||||
//
|
||||
// 设计原则:
|
||||
// 1. Delete 失败只记日志,不中断 Graph 流程;
|
||||
// 2. 删除是幂等的,key 不存在也视为成功;
|
||||
// 3. StateStore 为空时静默跳过。
|
||||
//
|
||||
// TODO: 接入项目统一的日志框架后,把 _ = err 改成结构化日志。
|
||||
func deleteAgentState(ctx context.Context, st *newagentmodel.AgentGraphState) {
|
||||
if st == nil {
|
||||
return
|
||||
}
|
||||
store := st.Deps.StateStore
|
||||
if store == nil {
|
||||
return
|
||||
}
|
||||
|
||||
runtimeState := st.EnsureRuntimeState()
|
||||
if runtimeState == nil {
|
||||
return
|
||||
}
|
||||
|
||||
flowState := runtimeState.EnsureCommonState()
|
||||
if flowState == nil || flowState.ConversationID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
_ = store.Delete(ctx, flowState.ConversationID)
|
||||
}
|
||||
|
||||
208
backend/newAgent/node/confirm.go
Normal file
208
backend/newAgent/node/confirm.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package newagentnode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream"
|
||||
)
|
||||
|
||||
const (
|
||||
confirmStageName = "confirm"
|
||||
confirmStatusBlockID = "confirm.status"
|
||||
)
|
||||
|
||||
// ConfirmNodeInput 描述确认节点单轮运行所需的最小依赖。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 不需要 LLM Client — 确认内容由已有状态机械格式化,不调模型;
|
||||
// 2. RuntimeState 提供计划步骤和待确认工具快照;
|
||||
// 3. ChunkEmitter 负责推送确认事件到前端。
|
||||
type ConfirmNodeInput struct {
|
||||
RuntimeState *newagentmodel.AgentRuntimeState
|
||||
ConversationContext *newagentmodel.ConversationContext
|
||||
ChunkEmitter *newagentstream.ChunkEmitter
|
||||
}
|
||||
|
||||
// RunConfirmNode 执行一轮确认节点逻辑。
|
||||
//
|
||||
// 核心职责:
|
||||
// 1. 判断确认来源:有 PendingConfirmTool → 工具确认;有 PlanSteps → 计划确认;
|
||||
// 2. 机械格式化确认内容(不需要 LLM 调用);
|
||||
// 3. 推送确认事件 EmitConfirmRequest → 前端渲染确认卡片;
|
||||
// 4. 调用 OpenConfirmInteraction 固化中断快照,Phase 自动变为 interrupted。
|
||||
//
|
||||
// 设计原则:
|
||||
// 1. 不等待用户响应 — 等待是 interruptNode 的职责;
|
||||
// 2. 不执行任何工具 — 只固化"意图",执行留给恢复后的 Execute;
|
||||
// 3. Confirm 是图里唯一负责"生成确认事件 + 固化快照"的地方,上游节点只设 Phase。
|
||||
func RunConfirmNode(ctx context.Context, input ConfirmNodeInput) error {
|
||||
runtimeState, _, emitter, err := prepareConfirmNodeInput(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
flowState := runtimeState.EnsureCommonState()
|
||||
|
||||
// 优先处理工具确认(Execute 发起的写操作确认)。
|
||||
if runtimeState.PendingConfirmTool != nil {
|
||||
return handleToolConfirm(ctx, runtimeState, flowState, emitter)
|
||||
}
|
||||
|
||||
// 其次处理计划确认(Plan 完成后的整体验收)。
|
||||
if flowState.HasPlan() {
|
||||
return handlePlanConfirm(ctx, runtimeState, flowState, emitter)
|
||||
}
|
||||
|
||||
// 既没有工具也没有计划 → 异常状态,不应到达此处。
|
||||
return fmt.Errorf("confirm node: 没有可确认的内容(无计划、无待确认工具)")
|
||||
}
|
||||
|
||||
// handlePlanConfirm 处理计划确认。
|
||||
//
|
||||
// 流程:
|
||||
// 1. 从 flowState.PlanSteps 格式化可读摘要;
|
||||
// 2. 推送确认事件到前端;
|
||||
// 3. 调用 OpenConfirmInteraction 固化快照(无 PendingTool)。
|
||||
func handlePlanConfirm(
|
||||
ctx context.Context,
|
||||
runtimeState *newagentmodel.AgentRuntimeState,
|
||||
flowState *newagentmodel.CommonState,
|
||||
emitter *newagentstream.ChunkEmitter,
|
||||
) error {
|
||||
summary := buildPlanSummary(flowState.PlanSteps)
|
||||
interactionID := generateConfirmInteractionID(flowState)
|
||||
|
||||
if err := emitter.EmitConfirmRequest(
|
||||
ctx, confirmStatusBlockID, confirmStageName,
|
||||
interactionID,
|
||||
"计划确认",
|
||||
summary,
|
||||
newagentstream.DefaultPseudoStreamOptions(),
|
||||
); err != nil {
|
||||
return fmt.Errorf("计划确认事件推送失败: %w", err)
|
||||
}
|
||||
|
||||
runtimeState.OpenConfirmInteraction(
|
||||
interactionID,
|
||||
summary,
|
||||
"plan",
|
||||
nil,
|
||||
)
|
||||
|
||||
_ = emitter.EmitStatus(
|
||||
confirmStatusBlockID, confirmStageName,
|
||||
"plan_confirm", "计划已生成,等待用户确认。", false,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleToolConfirm 处理工具确认。
|
||||
//
|
||||
// 流程:
|
||||
// 1. 从 PendingConfirmTool 构建确认摘要;
|
||||
// 2. 推送确认事件到前端;
|
||||
// 3. 调用 OpenConfirmInteraction 固化快照(含 PendingTool);
|
||||
// 4. 清空 PendingConfirmTool 临时邮箱。
|
||||
func handleToolConfirm(
|
||||
ctx context.Context,
|
||||
runtimeState *newagentmodel.AgentRuntimeState,
|
||||
flowState *newagentmodel.CommonState,
|
||||
emitter *newagentstream.ChunkEmitter,
|
||||
) error {
|
||||
pendingTool := runtimeState.PendingConfirmTool
|
||||
summary := buildToolConfirmSummary(pendingTool)
|
||||
interactionID := generateConfirmInteractionID(flowState)
|
||||
|
||||
if err := emitter.EmitConfirmRequest(
|
||||
ctx, confirmStatusBlockID, confirmStageName,
|
||||
interactionID,
|
||||
"操作确认",
|
||||
summary,
|
||||
newagentstream.DefaultPseudoStreamOptions(),
|
||||
); err != nil {
|
||||
return fmt.Errorf("工具确认事件推送失败: %w", err)
|
||||
}
|
||||
|
||||
runtimeState.OpenConfirmInteraction(
|
||||
interactionID,
|
||||
summary,
|
||||
"execute",
|
||||
pendingTool,
|
||||
)
|
||||
|
||||
// 确认快照已固化到 PendingInteraction,清空临时邮箱。
|
||||
runtimeState.PendingConfirmTool = nil
|
||||
|
||||
_ = emitter.EmitStatus(
|
||||
confirmStatusBlockID, confirmStageName,
|
||||
"tool_confirm", "操作等待确认。", false,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildPlanSummary 把 PlanSteps 格式化成人类可读的确认摘要。
|
||||
func buildPlanSummary(steps []newagentmodel.PlanStep) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("共 %d 步:\n", len(steps)))
|
||||
for i, step := range steps {
|
||||
sb.WriteString(fmt.Sprintf("%d. %s", i+1, step.Content))
|
||||
if step.DoneWhen != "" {
|
||||
sb.WriteString(fmt.Sprintf("(完成条件:%s)", step.DoneWhen))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
return strings.TrimSpace(sb.String())
|
||||
}
|
||||
|
||||
// buildToolConfirmSummary 从工具快照构建确认摘要。
|
||||
func buildToolConfirmSummary(tool *newagentmodel.PendingToolCallSnapshot) string {
|
||||
if tool == nil {
|
||||
return "待确认操作"
|
||||
}
|
||||
if tool.Summary != "" {
|
||||
return tool.Summary
|
||||
}
|
||||
detail := fmt.Sprintf("即将执行工具:%s", tool.ToolName)
|
||||
if tool.ArgsJSON != "" {
|
||||
var args map[string]any
|
||||
if json.Unmarshal([]byte(tool.ArgsJSON), &args) == nil && len(args) > 0 {
|
||||
detail += fmt.Sprintf(",参数:%s", tool.ArgsJSON)
|
||||
}
|
||||
}
|
||||
return detail
|
||||
}
|
||||
|
||||
// generateConfirmInteractionID 生成确认交互的唯一标识。
|
||||
func generateConfirmInteractionID(flowState *newagentmodel.CommonState) string {
|
||||
prefix := flowState.TraceID
|
||||
if prefix == "" {
|
||||
prefix = "confirm"
|
||||
}
|
||||
return fmt.Sprintf("%s-%d", prefix, time.Now().UnixMilli())
|
||||
}
|
||||
|
||||
// prepareConfirmNodeInput 校验并准备确认节点的运行态依赖。
|
||||
func prepareConfirmNodeInput(input ConfirmNodeInput) (
|
||||
*newagentmodel.AgentRuntimeState,
|
||||
*newagentmodel.ConversationContext,
|
||||
*newagentstream.ChunkEmitter,
|
||||
error,
|
||||
) {
|
||||
if input.RuntimeState == nil {
|
||||
return nil, nil, nil, fmt.Errorf("confirm node: runtime state 不能为空")
|
||||
}
|
||||
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
|
||||
}
|
||||
184
backend/newAgent/node/deliver.go
Normal file
184
backend/newAgent/node/deliver.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package newagentnode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
const (
|
||||
deliverStageName = "deliver"
|
||||
deliverStatusBlockID = "deliver.status"
|
||||
deliverSpeakBlockID = "deliver.speak"
|
||||
)
|
||||
|
||||
// DeliverNodeInput 描述交付节点单轮运行所需的最小依赖。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只负责生成交付总结并推送给用户,不负责后续流程推进;
|
||||
// 2. RuntimeState 提供计划步骤和执行状态;
|
||||
// 3. ConversationContext 提供执行阶段的对话历史;
|
||||
// 4. 交付完成后标记流程结束。
|
||||
type DeliverNodeInput struct {
|
||||
RuntimeState *newagentmodel.AgentRuntimeState
|
||||
ConversationContext *newagentmodel.ConversationContext
|
||||
Client *newagentllm.Client
|
||||
ChunkEmitter *newagentstream.ChunkEmitter
|
||||
}
|
||||
|
||||
// RunDeliverNode 执行一轮交付节点逻辑。
|
||||
//
|
||||
// 核心职责:
|
||||
// 1. 调 LLM 基于原始计划 + 执行历史生成交付总结;
|
||||
// 2. 伪流式推送总结给用户;
|
||||
// 3. 写入对话历史,保证上下文连续;
|
||||
// 4. 标记流程结束。
|
||||
//
|
||||
// 降级策略:
|
||||
// 1. LLM 调用失败时,回退到机械格式化总结,不中断流程;
|
||||
// 2. 机械总结包含计划步骤列表和完成进度。
|
||||
func RunDeliverNode(ctx context.Context, input DeliverNodeInput) error {
|
||||
runtimeState, conversationContext, emitter, err := prepareDeliverNodeInput(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
flowState := runtimeState.EnsureCommonState()
|
||||
|
||||
// 1. 推送交付阶段状态,让前端知道正在生成总结。
|
||||
if err := emitter.EmitStatus(
|
||||
deliverStatusBlockID,
|
||||
deliverStageName,
|
||||
"summarizing",
|
||||
"正在生成交付总结。",
|
||||
false,
|
||||
); err != nil {
|
||||
return fmt.Errorf("交付阶段状态推送失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 调 LLM 生成交付总结。
|
||||
summary := generateDeliverSummary(ctx, input.Client, flowState, conversationContext)
|
||||
|
||||
// 3. 伪流式推送总结。
|
||||
if strings.TrimSpace(summary) != "" {
|
||||
if err := emitter.EmitPseudoAssistantText(
|
||||
ctx,
|
||||
deliverSpeakBlockID,
|
||||
deliverStageName,
|
||||
summary,
|
||||
newagentstream.DefaultPseudoStreamOptions(),
|
||||
); err != nil {
|
||||
return fmt.Errorf("交付总结推送失败: %w", err)
|
||||
}
|
||||
conversationContext.AppendHistory(schema.AssistantMessage(summary, nil))
|
||||
}
|
||||
|
||||
// 4. 推送最终完成状态。
|
||||
_ = emitter.EmitStatus(
|
||||
deliverStatusBlockID,
|
||||
deliverStageName,
|
||||
"done",
|
||||
"任务已完成。",
|
||||
true,
|
||||
)
|
||||
|
||||
// 5. 标记流程结束。
|
||||
flowState.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateDeliverSummary 尝试调用 LLM 生成交付总结,失败时降级到机械格式化。
|
||||
func generateDeliverSummary(
|
||||
ctx context.Context,
|
||||
client *newagentllm.Client,
|
||||
flowState *newagentmodel.CommonState,
|
||||
conversationContext *newagentmodel.ConversationContext,
|
||||
) string {
|
||||
if client == nil {
|
||||
return buildMechanicalSummary(flowState)
|
||||
}
|
||||
|
||||
messages := newagentprompt.BuildDeliverMessages(flowState, conversationContext)
|
||||
result, err := client.GenerateText(
|
||||
ctx,
|
||||
messages,
|
||||
newagentllm.GenerateOptions{
|
||||
Temperature: 0.5,
|
||||
MaxTokens: 800,
|
||||
Thinking: newagentllm.ThinkingModeDisabled,
|
||||
Metadata: map[string]any{
|
||||
"stage": deliverStageName,
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil || result == nil || strings.TrimSpace(result.Text) == "" {
|
||||
return buildMechanicalSummary(flowState)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(result.Text)
|
||||
}
|
||||
|
||||
// buildMechanicalSummary 在 LLM 不可用时,机械拼接一份最小可用总结。
|
||||
func buildMechanicalSummary(state *newagentmodel.CommonState) string {
|
||||
if state == nil {
|
||||
return "任务流程已结束。"
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
current, total := state.PlanProgress()
|
||||
|
||||
if !state.HasPlan() {
|
||||
return "任务流程已结束。"
|
||||
}
|
||||
|
||||
if state.Exhausted() {
|
||||
sb.WriteString(fmt.Sprintf("任务因执行轮次耗尽提前结束,已完成 %d/%d 步。\n", current, total))
|
||||
} else {
|
||||
sb.WriteString("所有计划步骤已执行完毕。\n")
|
||||
}
|
||||
|
||||
sb.WriteString("\n执行情况:\n")
|
||||
for i, step := range state.PlanSteps {
|
||||
marker := "[ ]"
|
||||
if i < current {
|
||||
marker = "[x]"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s %s\n", marker, strings.TrimSpace(step.Content)))
|
||||
}
|
||||
|
||||
if state.Exhausted() && current < total {
|
||||
sb.WriteString("\n如需继续完成剩余步骤,可以告诉我继续。")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// prepareDeliverNodeInput 校验并准备交付节点的运行态依赖。
|
||||
func prepareDeliverNodeInput(input DeliverNodeInput) (
|
||||
*newagentmodel.AgentRuntimeState,
|
||||
*newagentmodel.ConversationContext,
|
||||
*newagentstream.ChunkEmitter,
|
||||
error,
|
||||
) {
|
||||
if input.RuntimeState == nil {
|
||||
return nil, nil, nil, fmt.Errorf("deliver node: runtime state 不能为空")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package newagentnode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -177,6 +178,11 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
||||
runtimeState.OpenAskUserInteraction(uuid.NewString(), question, strings.TrimSpace(input.ResumeNode))
|
||||
return nil
|
||||
|
||||
case newagentmodel.ExecuteActionConfirm:
|
||||
// LLM 申报了写操作意图,需要用户确认后才能真正执行。
|
||||
// 步骤:1) 把 ToolCallIntent 转成快照暂存;2) 设 Phase → 下游 confirm 节点接管。
|
||||
return handleExecuteActionConfirm(decision, runtimeState, flowState)
|
||||
|
||||
case newagentmodel.ExecuteActionNextPlan:
|
||||
// LLM 判定当前步骤已完成,推进到下一步。
|
||||
// 后端信任 LLM 判断,不做硬校验。
|
||||
@@ -253,6 +259,39 @@ func resolveExecuteAskUserText(decision *newagentmodel.ExecuteDecision) string {
|
||||
return "执行过程中遇到不确定的情况,需要向你确认。"
|
||||
}
|
||||
|
||||
// handleExecuteActionConfirm 处理 LLM 申报的写操作确认请求。
|
||||
//
|
||||
// 步骤:
|
||||
// 1. 把 ToolCallIntent 转成 PendingToolCallSnapshot 暂存到运行态;
|
||||
// 2. 设 Phase = PhaseWaitingConfirm,让下游 confirm 节点接管;
|
||||
// 3. 不执行工具,也不生成确认事件 — 这些都是 confirm 节点的职责。
|
||||
func handleExecuteActionConfirm(
|
||||
decision *newagentmodel.ExecuteDecision,
|
||||
runtimeState *newagentmodel.AgentRuntimeState,
|
||||
flowState *newagentmodel.CommonState,
|
||||
) error {
|
||||
toolCall := decision.ToolCall
|
||||
|
||||
// 序列化工具参数。
|
||||
argsJSON := ""
|
||||
if toolCall.Arguments != nil {
|
||||
if raw, err := json.Marshal(toolCall.Arguments); err == nil {
|
||||
argsJSON = string(raw)
|
||||
}
|
||||
}
|
||||
|
||||
// 暂存到运行态邮箱,confirm 节点会读出来。
|
||||
runtimeState.PendingConfirmTool = &newagentmodel.PendingToolCallSnapshot{
|
||||
ToolName: toolCall.Name,
|
||||
ArgsJSON: argsJSON,
|
||||
Summary: strings.TrimSpace(decision.Speak),
|
||||
}
|
||||
|
||||
// 设 Phase,让 branchAfterExecute 路由到 confirm 节点。
|
||||
flowState.Phase = newagentmodel.PhaseWaitingConfirm
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeToolCall 执行工具调用并记录证据。
|
||||
//
|
||||
// 职责边界:
|
||||
|
||||
153
backend/newAgent/node/interrupt.go
Normal file
153
backend/newAgent/node/interrupt.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package newagentnode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream"
|
||||
)
|
||||
|
||||
const (
|
||||
interruptStageName = "interrupt"
|
||||
interruptSpeakBlockID = "interrupt.speak"
|
||||
interruptStatusBlockID = "interrupt.status"
|
||||
)
|
||||
|
||||
// InterruptNodeInput 描述中断节点单轮运行所需的最小依赖。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 不需要 LLM Client — 所有文本已在 PendingInteraction.DisplayText 里;
|
||||
// 2. RuntimeState 提供 PendingInteraction;
|
||||
// 3. ChunkEmitter 负责推送收尾消息。
|
||||
type InterruptNodeInput struct {
|
||||
RuntimeState *newagentmodel.AgentRuntimeState
|
||||
ConversationContext *newagentmodel.ConversationContext
|
||||
ChunkEmitter *newagentstream.ChunkEmitter
|
||||
}
|
||||
|
||||
// RunInterruptNode 执行一轮中断节点逻辑。
|
||||
//
|
||||
// 核心职责:
|
||||
// 1. ask_user → 把 DisplayText 当普通 assistant 消息伪流式输出,说完就停;
|
||||
// 2. confirm → 确认卡片已由 confirm 节点推送,无需额外输出;
|
||||
// 3. 状态持久化已由 agent_nodes 层统一处理,Interrupt 不再需要自行存快照;
|
||||
// 4. 节点结束后 graph 走 END,当前连接断开。
|
||||
//
|
||||
// 设计原则:
|
||||
// 1. 中断就是正常对话的结束 — 助手说了问题/确认卡片,然后停下来等用户回复;
|
||||
// 2. 用户下次回复时走正常 chat 入口,chat 节点负责 resume;
|
||||
// 3. 不做特殊 UI,不需要前端适配新的交互模式。
|
||||
func RunInterruptNode(ctx context.Context, input InterruptNodeInput) error {
|
||||
runtimeState, conversationContext, emitter, err := prepareInterruptNodeInput(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pending := runtimeState.PendingInteraction
|
||||
if pending == nil {
|
||||
// 无 pending interaction → 不应到达此处,防御性返回。
|
||||
return fmt.Errorf("interrupt node: 无待处理交互")
|
||||
}
|
||||
|
||||
switch pending.Type {
|
||||
case newagentmodel.PendingInteractionTypeAskUser:
|
||||
return handleInterruptAskUser(ctx, pending, conversationContext, emitter)
|
||||
case newagentmodel.PendingInteractionTypeConfirm:
|
||||
return handleInterruptConfirm(pending, emitter)
|
||||
default:
|
||||
// connection_lost 等其他类型 → 仅持久化,不输出。
|
||||
return handleInterruptDefault(pending, emitter)
|
||||
}
|
||||
}
|
||||
|
||||
// handleInterruptAskUser 处理追问型中断。
|
||||
//
|
||||
// 把 PendingInteraction.DisplayText 当普通 assistant 消息伪流式输出,
|
||||
// 写入历史,然后结束。用户体验和正常对话一样 — 助手问了问题,停下来等回复。
|
||||
func handleInterruptAskUser(
|
||||
ctx context.Context,
|
||||
pending *newagentmodel.PendingInteraction,
|
||||
conversationContext *newagentmodel.ConversationContext,
|
||||
emitter *newagentstream.ChunkEmitter,
|
||||
) error {
|
||||
text := pending.DisplayText
|
||||
if text == "" {
|
||||
text = "请补充更多信息。"
|
||||
}
|
||||
|
||||
// 伪流式输出,和 chatReply 一样的体感。
|
||||
if err := emitter.EmitPseudoAssistantText(
|
||||
ctx, interruptSpeakBlockID, interruptStageName,
|
||||
text,
|
||||
newagentstream.DefaultPseudoStreamOptions(),
|
||||
); err != nil {
|
||||
return fmt.Errorf("追问消息推送失败: %w", err)
|
||||
}
|
||||
|
||||
// 写入对话历史,下一轮 resume 时 LLM 能看到这个上下文。
|
||||
conversationContext.AppendHistory(schema.AssistantMessage(text, nil))
|
||||
|
||||
// 状态持久化已由 agent_nodes 层统一处理,此处不再需要自行存快照。
|
||||
|
||||
_ = emitter.EmitStatus(
|
||||
interruptStatusBlockID, interruptStageName,
|
||||
"ask_user", "已追问用户,等待回复。", false,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleInterruptConfirm 处理确认型中断。
|
||||
//
|
||||
// 确认卡片已由 confirm 节点推送,这里只需推送状态通知并持久化。
|
||||
func handleInterruptConfirm(
|
||||
pending *newagentmodel.PendingInteraction,
|
||||
emitter *newagentstream.ChunkEmitter,
|
||||
) error {
|
||||
// 状态持久化已由 agent_nodes 层统一处理,此处不再需要自行存快照。
|
||||
|
||||
_ = emitter.EmitStatus(
|
||||
interruptStatusBlockID, interruptStageName,
|
||||
"confirm", "等待用户确认。", false,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleInterruptDefault 处理其他类型的中断(如 connection_lost)。
|
||||
func handleInterruptDefault(
|
||||
pending *newagentmodel.PendingInteraction,
|
||||
emitter *newagentstream.ChunkEmitter,
|
||||
) error {
|
||||
// 状态持久化已由 agent_nodes 层统一处理,此处不再需要自行存快照。
|
||||
|
||||
_ = emitter.EmitStatus(
|
||||
interruptStatusBlockID, interruptStageName,
|
||||
"interrupted", "会话已中断。", false,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepareInterruptNodeInput 校验并准备中断节点的运行态依赖。
|
||||
func prepareInterruptNodeInput(input InterruptNodeInput) (
|
||||
*newagentmodel.AgentRuntimeState,
|
||||
*newagentmodel.ConversationContext,
|
||||
*newagentstream.ChunkEmitter,
|
||||
error,
|
||||
) {
|
||||
if input.RuntimeState == nil {
|
||||
return nil, nil, nil, fmt.Errorf("interrupt node: runtime state 不能为空")
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user