Version: 0.9.4.dev.260407
后端: 1.粗排结果/预览语义修复(task_item suggested 保真 + existing/嵌入识别补全) - 更新conv/schedule_state.go:LoadScheduleState 补齐 event.rel_id / schedules.embedded_task_id / task_item.embedded_time 三种“已落位”信号;嵌入任务强制 existing + 继承 host slots;补充 task_item duration/name/slot helper;Diff 相关英文注释改中文 - 更新conv/schedule_preview.go:预览层新增 shouldMarkSuggestedInPreview,pending 任务与 source=task_item 的建议态任务统一输出 suggested 2.newAgent 状态快照增强(ScheduleState/OriginalScheduleState 跨轮恢复) - 更新model/state_store.go:AgentStateSnapshot 新增 ScheduleState / OriginalScheduleState - 更新model/graph_run_state.go:AgentGraphRunInput/AgentGraphState 接入两份 schedule 状态;恢复旧快照时自动补 original clone - 更新service/agentsvc/agent_newagent.go:loadOrCreateRuntimeState 返回并恢复 schedule/original;runNewAgentGraph 透传到 graph - 更新node/agent_nodes.go:saveAgentState 一并保存 schedule/original 到 Redis 快照 3.Execute 链路纠偏(只写内存不落库 + 完整打点 + 恢复消息去重) - 更新node/execute.go:AlwaysExecute/confirm resume 路径取消 PersistScheduleChanges,仅保留内存写;新增 execute LLM 完整上下文日志;新增工具调用前后 state 摘要日志;thinking 模式改为 enabled - 更新node/chat.go:pending resume 不再重复写入同一轮 user message - 更新service/agentsvc/agent_newagent.go:新增 deliver preview write/state 摘要日志,便于排查 suggested 丢失问题 4.AlwaysExecute 贯通 Plan→Graph→Execute - 更新node/plan.go:PlanNodeInput 新增 AlwaysExecute;plan_done 后支持自动确认直接进入执行 - 更新graph/common_graph.go:branchAfterPlan 支持 PhaseExecuting/PhaseDone 分支 5.排课上下文补强(显式注入 task_class_ids,减少 Execute 误 ask_user) - 更新prompt/execute.go:Plan/ReAct 两种 execute prompt 都显式写入任务类 ID,声明“上下文已完整,无需追问” - 更新node/rough_build.go:粗排完成 pinned block 显式标注任务类 ID,避免 Execute 找不到 ID 来源 6.流式输出与预览调试工具修复 - 更新stream/emitter.go:保留换行,修复 pseudo stream 分片后文本黏连/双换行问题 - 更新infra/schedule_preview_viewer.html:升级预览工具,支持 candidate_plans / hybrid_entries 前端:无 仓库: 1.更新了infra内的html,适应了获取日程接口
This commit is contained in:
@@ -85,6 +85,9 @@ func (n *AgentNodes) Confirm(ctx context.Context, st *newagentmodel.AgentGraphSt
|
||||
},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
} else if st.Deps.WriteSchedulePreview != nil && st.ScheduleState == nil {
|
||||
flowState := st.EnsureFlowState()
|
||||
log.Printf("[WARN] deliver: schedule state is nil, skip preview write chat=%s", flowState.ConversationID)
|
||||
}
|
||||
|
||||
saveAgentState(ctx, st)
|
||||
@@ -111,6 +114,7 @@ func (n *AgentNodes) Plan(ctx context.Context, st *newagentmodel.AgentGraphState
|
||||
Client: st.Deps.ResolvePlanClient(),
|
||||
ChunkEmitter: st.EnsureChunkEmitter(),
|
||||
ResumeNode: "plan",
|
||||
AlwaysExecute: st.Request.AlwaysExecute,
|
||||
},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
@@ -293,8 +297,10 @@ func saveAgentState(ctx context.Context, st *newagentmodel.AgentGraphState) {
|
||||
}
|
||||
|
||||
snapshot := &newagentmodel.AgentStateSnapshot{
|
||||
RuntimeState: runtimeState,
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
RuntimeState: runtimeState,
|
||||
ConversationContext: st.EnsureConversationContext(),
|
||||
ScheduleState: st.ScheduleState.Clone(),
|
||||
OriginalScheduleState: st.OriginalScheduleState.Clone(),
|
||||
}
|
||||
|
||||
_ = store.Save(ctx, flowState.ConversationID, snapshot)
|
||||
|
||||
@@ -54,7 +54,7 @@ func RunChatNode(ctx context.Context, input ChatNodeInput) error {
|
||||
|
||||
// 1. 有 pending interaction → 纯状态传递,处理恢复。
|
||||
if runtimeState.HasPendingInteraction() {
|
||||
return handleChatResume(input, runtimeState, conversationContext, emitter)
|
||||
return handleChatResume(input, runtimeState, emitter)
|
||||
}
|
||||
|
||||
// 2. 无 pending → 路由决策(一次快速 LLM 调用,不开 thinking)。
|
||||
@@ -263,16 +263,13 @@ func handleRoutePlan(
|
||||
func handleChatResume(
|
||||
input ChatNodeInput,
|
||||
runtimeState *newagentmodel.AgentRuntimeState,
|
||||
conversationContext *newagentmodel.ConversationContext,
|
||||
emitter *newagentstream.ChunkEmitter,
|
||||
) error {
|
||||
pending := runtimeState.PendingInteraction
|
||||
flowState := runtimeState.EnsureCommonState()
|
||||
|
||||
// 把用户本轮输入写回历史(ask_user 回复、confirm 附言等)。
|
||||
if strings.TrimSpace(input.UserInput) != "" {
|
||||
conversationContext.AppendHistory(schema.UserMessage(input.UserInput))
|
||||
}
|
||||
// 用户输入在 service 层进入 graph 前已经统一追加到 ConversationContext。
|
||||
// 这里不再二次写入,避免 pending 恢复路径把同一轮 user message 追加两次。
|
||||
|
||||
switch pending.Type {
|
||||
case newagentmodel.PendingInteractionTypeAskUser:
|
||||
|
||||
@@ -38,8 +38,8 @@ const (
|
||||
// 3. ConversationContext 提供历史对话与置顶上下文;
|
||||
// 4. ToolRegistry 提供工具注册表;
|
||||
// 5. ScheduleState 提供工具操作的内存数据源(可为 nil,由调用方按需加载);
|
||||
// 6. SchedulePersistor 用于写工具执行后持久化变更;
|
||||
// 7. OriginalScheduleState 是首次加载时的原始快照,用于 diff。
|
||||
// 6. SchedulePersistor 仍保留注入位,但当前阶段不调用,避免写库;
|
||||
// 7. OriginalScheduleState 继续保留,供 Redis 快照恢复时维持“当前态/原始态”成对语义。
|
||||
type ExecuteNodeInput struct {
|
||||
RuntimeState *newagentmodel.AgentRuntimeState
|
||||
ConversationContext *newagentmodel.ConversationContext
|
||||
@@ -138,6 +138,15 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
||||
|
||||
// 5. 构造本轮执行输入,请求 LLM 输出 ExecuteDecision。
|
||||
messages := newagentprompt.BuildExecuteMessages(flowState, conversationContext)
|
||||
log.Printf(
|
||||
"[DEBUG] execute LLM context begin chat=%s round=%d message_count=%d\n%s\n[DEBUG] execute LLM context end chat=%s round=%d",
|
||||
flowState.ConversationID,
|
||||
flowState.RoundUsed,
|
||||
len(messages),
|
||||
formatExecuteLLMMessagesForDebug(messages),
|
||||
flowState.ConversationID,
|
||||
flowState.RoundUsed,
|
||||
)
|
||||
decision, rawResult, err := newagentllm.GenerateJSON[newagentmodel.ExecuteDecision](
|
||||
ctx,
|
||||
input.Client,
|
||||
@@ -316,18 +325,9 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
||||
return nil
|
||||
|
||||
case newagentmodel.ExecuteActionConfirm:
|
||||
// AlwaysExecute=true:跳过确认闸门,直接执行写工具并持久化,不走 confirm 节点。
|
||||
// AlwaysExecute=true:跳过确认闸门,直接执行内存写工具,不走 confirm 节点。
|
||||
if input.AlwaysExecute && decision.ToolCall != nil {
|
||||
if err := executeToolCall(ctx, flowState, conversationContext, decision.ToolCall, emitter, input.ToolRegistry, input.ScheduleState); err != nil {
|
||||
return err
|
||||
}
|
||||
if input.SchedulePersistor != nil && input.OriginalScheduleState != nil {
|
||||
cs := runtimeState.EnsureCommonState()
|
||||
if persistErr := input.SchedulePersistor.PersistScheduleChanges(ctx, input.OriginalScheduleState, input.ScheduleState, cs.UserID); persistErr != nil {
|
||||
log.Printf("[WARN] execute always-execute 持久化失败: %v", persistErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return executeToolCall(ctx, flowState, conversationContext, decision.ToolCall, emitter, input.ToolRegistry, input.ScheduleState)
|
||||
}
|
||||
// AlwaysExecute=false(默认):暂存工具意图,设 Phase → 下游 confirm 节点接管。
|
||||
return handleExecuteActionConfirm(decision, runtimeState, flowState)
|
||||
@@ -504,7 +504,19 @@ func executeToolCall(
|
||||
}
|
||||
|
||||
// 2. 执行工具。
|
||||
beforeDigest := summarizeScheduleStateForDebug(scheduleState)
|
||||
result := registry.Execute(scheduleState, toolName, toolCall.Arguments)
|
||||
afterDigest := summarizeScheduleStateForDebug(scheduleState)
|
||||
log.Printf(
|
||||
"[DEBUG] execute tool chat=%s round=%d tool=%s args=%s before=%s after=%s result_preview=%.200s",
|
||||
flowState.ConversationID,
|
||||
flowState.RoundUsed,
|
||||
toolName,
|
||||
marshalArgsForDebug(toolCall.Arguments),
|
||||
beforeDigest,
|
||||
afterDigest,
|
||||
flattenForLog(result),
|
||||
)
|
||||
|
||||
// 2.5 截断过大的工具结果,防止上下文膨胀导致后续 LLM 调用返回空或超限。
|
||||
const maxToolResultLen = 3000
|
||||
@@ -558,7 +570,7 @@ func executeToolCall(
|
||||
// 1. 从 PendingConfirmTool 读取工具名和参数(已序列化);
|
||||
// 2. 反序列化参数后调用工具执行;
|
||||
// 3. 将结果追加到历史,清空 PendingConfirmTool;
|
||||
// 4. 执行成功后调用 persistor 持久化变更;
|
||||
// 4. 当前阶段只保留内存修改,不在这里落库;
|
||||
// 5. 不调用 LLM,直接返回让下一轮继续。
|
||||
func executePendingTool(
|
||||
ctx context.Context,
|
||||
@@ -598,7 +610,20 @@ func executePendingTool(
|
||||
}
|
||||
|
||||
// 4. 执行工具。
|
||||
beforeDigest := summarizeScheduleStateForDebug(scheduleState)
|
||||
result := registry.Execute(scheduleState, pending.ToolName, args)
|
||||
afterDigest := summarizeScheduleStateForDebug(scheduleState)
|
||||
flowState := runtimeState.EnsureCommonState()
|
||||
log.Printf(
|
||||
"[DEBUG] execute pending tool chat=%s round=%d tool=%s args=%s before=%s after=%s result_preview=%.200s",
|
||||
flowState.ConversationID,
|
||||
flowState.RoundUsed,
|
||||
pending.ToolName,
|
||||
marshalArgsForDebug(args),
|
||||
beforeDigest,
|
||||
afterDigest,
|
||||
flattenForLog(result),
|
||||
)
|
||||
|
||||
// 5. 将工具调用和结果以合法的 assistant+tool 消息对追加到历史。
|
||||
//
|
||||
@@ -630,13 +655,6 @@ func executePendingTool(
|
||||
// 6. 清空临时邮箱,避免重复执行。
|
||||
runtimeState.PendingConfirmTool = nil
|
||||
|
||||
// 7. 持久化变更(如果有 persistor)。
|
||||
if persistor != nil && originalState != nil {
|
||||
if err := persistor.PersistScheduleChanges(ctx, originalState, scheduleState, runtimeState.UserID); err != nil {
|
||||
return fmt.Errorf("持久化日程变更失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -671,3 +689,147 @@ func truncateText(text string, maxLen int) string {
|
||||
}
|
||||
return text[:maxLen-3] + "..."
|
||||
}
|
||||
|
||||
// summarizeScheduleStateForDebug 返回内存日程状态的关键计数,用于判断工具是否真的修改了 state。
|
||||
func summarizeScheduleStateForDebug(state *newagenttools.ScheduleState) string {
|
||||
if state == nil {
|
||||
return "state=nil"
|
||||
}
|
||||
|
||||
total := len(state.Tasks)
|
||||
pendingNoSlot := 0
|
||||
pendingWithSlot := 0
|
||||
existingTotal := 0
|
||||
taskItemWithSlot := 0
|
||||
eventWithSlot := 0
|
||||
|
||||
for i := range state.Tasks {
|
||||
t := &state.Tasks[i]
|
||||
hasSlot := len(t.Slots) > 0
|
||||
|
||||
switch t.Status {
|
||||
case "pending":
|
||||
if hasSlot {
|
||||
pendingWithSlot++
|
||||
} else {
|
||||
pendingNoSlot++
|
||||
}
|
||||
case "existing":
|
||||
existingTotal++
|
||||
}
|
||||
|
||||
if hasSlot {
|
||||
if t.Source == "task_item" {
|
||||
taskItemWithSlot++
|
||||
}
|
||||
if t.Source == "event" {
|
||||
eventWithSlot++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"tasks=%d pending_no_slot=%d pending_with_slot=%d existing=%d task_item_with_slot=%d event_with_slot=%d",
|
||||
total,
|
||||
pendingNoSlot,
|
||||
pendingWithSlot,
|
||||
existingTotal,
|
||||
taskItemWithSlot,
|
||||
eventWithSlot,
|
||||
)
|
||||
}
|
||||
|
||||
// marshalArgsForDebug 将工具参数序列化为日志可读的短文本。
|
||||
func marshalArgsForDebug(args map[string]any) string {
|
||||
if len(args) == 0 {
|
||||
return "{}"
|
||||
}
|
||||
raw, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return "<marshal_error>"
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
// flattenForLog 将多行文本压成单行,避免日志换行影响排查。
|
||||
func flattenForLog(text string) string {
|
||||
text = strings.ReplaceAll(text, "\n", " ")
|
||||
text = strings.ReplaceAll(text, "\r", " ")
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
// formatExecuteLLMMessagesForDebug 将本轮送入 LLM 的完整消息上下文展开成可读多行日志。
|
||||
//
|
||||
// 说明:
|
||||
// 1. 按消息索引逐条输出,便于和上游上下文构造步骤逐项对齐;
|
||||
// 2. 完整输出 content / reasoning_content / tool_calls / extra,不做截断;
|
||||
// 3. 仅用于调试打点,不参与业务决策。
|
||||
func formatExecuteLLMMessagesForDebug(messages []*schema.Message) string {
|
||||
if len(messages) == 0 {
|
||||
return "(empty messages)"
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for i, msg := range messages {
|
||||
sb.WriteString(fmt.Sprintf("----- message[%d] -----\n", i))
|
||||
if msg == nil {
|
||||
sb.WriteString("role: <nil>\n\n")
|
||||
continue
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("role: %s\n", msg.Role))
|
||||
|
||||
if strings.TrimSpace(msg.ToolCallID) != "" {
|
||||
sb.WriteString(fmt.Sprintf("tool_call_id: %s\n", msg.ToolCallID))
|
||||
}
|
||||
if strings.TrimSpace(msg.ToolName) != "" {
|
||||
sb.WriteString(fmt.Sprintf("tool_name: %s\n", msg.ToolName))
|
||||
}
|
||||
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
sb.WriteString("tool_calls:\n")
|
||||
for j, call := range msg.ToolCalls {
|
||||
sb.WriteString(fmt.Sprintf(" - [%d] id=%s type=%s function=%s\n", j, call.ID, call.Type, call.Function.Name))
|
||||
sb.WriteString(" arguments:\n")
|
||||
sb.WriteString(indentMultilineForDebug(call.Function.Arguments, " "))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(msg.ReasoningContent) != "" {
|
||||
sb.WriteString("reasoning_content:\n")
|
||||
sb.WriteString(indentMultilineForDebug(msg.ReasoningContent, " "))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
sb.WriteString("content:\n")
|
||||
sb.WriteString(indentMultilineForDebug(msg.Content, " "))
|
||||
sb.WriteString("\n")
|
||||
|
||||
if len(msg.Extra) > 0 {
|
||||
sb.WriteString("extra:\n")
|
||||
raw, err := json.MarshalIndent(msg.Extra, "", " ")
|
||||
if err != nil {
|
||||
sb.WriteString(indentMultilineForDebug("<marshal_error>", " "))
|
||||
} else {
|
||||
sb.WriteString(indentMultilineForDebug(string(raw), " "))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// indentMultilineForDebug 为多行文本统一添加前缀缩进,避免日志折行后难以阅读。
|
||||
func indentMultilineForDebug(text, prefix string) string {
|
||||
if text == "" {
|
||||
return prefix + "<empty>"
|
||||
}
|
||||
lines := strings.Split(text, "\n")
|
||||
for i := range lines {
|
||||
lines[i] = prefix + lines[i]
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ type PlanNodeInput struct {
|
||||
Client *newagentllm.Client
|
||||
ChunkEmitter *newagentstream.ChunkEmitter
|
||||
ResumeNode string
|
||||
AlwaysExecute bool // true 时计划生成后自动确认,不进入 confirm 节点
|
||||
}
|
||||
|
||||
// RunPlanNode 执行一轮规划节点逻辑。
|
||||
@@ -166,6 +167,18 @@ func RunPlanNode(ctx context.Context, input PlanNodeInput) error {
|
||||
flowState.TaskClassIDs = decision.TaskClassIDs
|
||||
}
|
||||
}
|
||||
// always_execute 开启时,计划层跳过确认闸门,直接进入执行阶段。
|
||||
// 这样可以与 Execute 节点的“写工具跳过确认”语义保持一致。
|
||||
if input.AlwaysExecute {
|
||||
flowState.ConfirmPlan()
|
||||
_ = emitter.EmitStatus(
|
||||
planStatusBlockID,
|
||||
planStageName,
|
||||
"plan_auto_confirmed",
|
||||
"计划已自动确认,开始执行。",
|
||||
false,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
// 1. LLM 输出了不支持的 action,不应直接报错终止,而应给它修正机会。
|
||||
|
||||
@@ -3,6 +3,8 @@ package newagentnode
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
@@ -82,10 +84,18 @@ func RunRoughBuildNode(ctx context.Context, st *newagentmodel.AgentGraphState) e
|
||||
|
||||
// 8. 把粗排完成信息写入 pinned context,让 Execute 阶段的 LLM 直接进入验证和微调。
|
||||
stillPending := countPendingTasks(scheduleState)
|
||||
|
||||
// 构造任务类 ID 字符串,供 pinned block 明确标注,避免 Execute LLM 因找不到 task_class_id 来源而 ask_user。
|
||||
idParts := make([]string, len(taskClassIDs))
|
||||
for i, id := range taskClassIDs {
|
||||
idParts[i] = strconv.Itoa(id)
|
||||
}
|
||||
idStr := strings.Join(idParts, ", ")
|
||||
|
||||
var pinnedContent string
|
||||
if stillPending > 0 {
|
||||
pinnedContent = fmt.Sprintf(
|
||||
"后端已自动运行粗排算法,初始排课方案已写入日程状态(共 %d 个任务已预排)。\n"+
|
||||
"后端已自动运行粗排算法(任务类 ID:[%s]),初始排课方案已写入日程状态(共 %d 个任务已预排)。\n"+
|
||||
"注意:仍有 %d 个任务未被粗排覆盖,处于待安排(pending)状态,必须在微调阶段手动安排完毕。\n\n"+
|
||||
"处理 pending 任务的正确操作顺序:\n"+
|
||||
"1. 调用 get_overview 或 find_free 确认可用空位(不要反复调用 list_tasks,list_tasks 只能看任务列表,看不出空位)\n"+
|
||||
@@ -93,14 +103,14 @@ func RunRoughBuildNode(ctx context.Context, st *newagentmodel.AgentGraphState) e
|
||||
"3. 重复上述步骤,直到 get_overview 显示待安排任务剩余为 0\n\n"+
|
||||
"微调完成的判定标准:所有 pending 任务均已 place(待安排任务剩余=0),且现有排课无明显失衡。\n"+
|
||||
"无需再次触发粗排。",
|
||||
len(placements), stillPending,
|
||||
idStr, len(placements), stillPending,
|
||||
)
|
||||
} else {
|
||||
pinnedContent = fmt.Sprintf(
|
||||
"后端已自动运行粗排算法,初始排课方案已写入日程状态(共 %d 个任务已预排,无待安排任务)。\n"+
|
||||
"后端已自动运行粗排算法(任务类 ID:[%s]),初始排课方案已写入日程状态(共 %d 个任务已预排,无待安排任务)。\n"+
|
||||
"请直接调用 get_overview 查看预排结果,然后用 move/swap 微调不合理的位置。\n"+
|
||||
"无需再次触发粗排。",
|
||||
len(placements),
|
||||
idStr, len(placements),
|
||||
)
|
||||
}
|
||||
st.EnsureConversationContext().UpsertPinnedBlock(newagentmodel.ContextBlock{
|
||||
|
||||
Reference in New Issue
Block a user