Files
smartmate/backend/newAgent/prompt/execute_context.go
Losita 070d4c3459 Version: 0.9.15.dev.260412
后端:
1. 排程工具从 tools/ 根目录拆分为 tools/schedule 独立子包
- 12 个排程工具文件等价迁入 tools/schedule/,tools/ 根目录仅保留 registry.go 作为统一注册入口
- 所有依赖方(conv / model / node / prompt / service)import 统一切到 schedule 子包
2. Web 搜索工具链落地(tools/web 子包)
- 新增 web_search(结构化检索)与 web_fetch(正文抓取)两个读工具,支持博查 API / mock 降级
- 启动流程按配置选择 provider,未识别类型自动降级为 mock,不阻断主流程
- 执行提示补齐 web 工具使用约束与返回值示例
- config.example.yaml 补齐 websearch 配置段
前端:无
仓库:无
2026-04-12 19:02:54 +08:00

851 lines
30 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package newagentprompt
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
"github.com/cloudwego/eino/schema"
)
const (
executeHistoryKindKey = "newagent_history_kind"
executeHistoryKindCorrectionUser = "llm_correction_prompt"
executeHistoryKindLoopClosed = "execute_loop_closed"
executeHistoryKindStepAdvanced = "execute_step_advanced"
// executeLoopWindowLimit 控制“当轮 ReAct Loop 窗口”最多保留多少条记录。
// 采用固定窗口能避免上下文无上限增长,且可保持“最近行为”可追踪。
executeLoopWindowLimit = 8
// executeTrimmedObservationText 是重复工具压缩后的 observation 占位文案。
// 当同工具在窗口内出现多次时,只保留最新一条真实结果,其余旧结果统一替换为该文案。
executeTrimmedObservationText = "当前工具调用结果已经被使用过,当前无需使用,为节省上下文空间,已折叠"
)
type executeToolSchemaDoc struct {
Name string `json:"name"`
Parameters map[string]any `json:"parameters"`
}
type executeLoopRecord struct {
Thought string
ToolName string
ToolArgs string
Observation string
}
const executeMessage1MaxRunes = 1400
// buildExecuteStageMessages 组装 execute 阶段 4 条消息骨架。
//
// 消息结构(固定):
// 1. message[0] 固定 prompt规则 + 微调硬引导 + 输出约束 + 工具简表)
// 2. message[1] 历史上下文(聊天摘要 + 早期 ReAct 摘要)
// 3. message[2] 当轮 ReAct Loop 窗口thought/reason + tool_call + observation 绑定展示)
// 4. message[3] 当前执行状态(含初始目标、结束判断原则、非目标)
func buildExecuteStageMessages(
stageSystemPrompt string,
state *newagentmodel.CommonState,
ctx *newagentmodel.ConversationContext,
runtimeUserPrompt string,
) []*schema.Message {
msg0 := buildExecuteMessage0(stageSystemPrompt, ctx)
msg1 := buildExecuteMessage1V3(ctx)
msg2 := buildExecuteMessage2V3(ctx)
msg3 := buildExecuteMessage3(state, ctx, runtimeUserPrompt)
return []*schema.Message{
schema.SystemMessage(msg0),
{Role: schema.Assistant, Content: msg1},
{Role: schema.Assistant, Content: msg2},
schema.SystemMessage(msg3),
}
}
// buildExecuteMessage0 生成固定规则消息,并附带工具简表。
func buildExecuteMessage0(stageSystemPrompt string, ctx *newagentmodel.ConversationContext) string {
base := strings.TrimSpace(mergeSystemPrompts(ctx, stageSystemPrompt))
if base == "" {
base = "你是 SmartFlow NewAgent 执行器,请继续 execute 阶段。"
}
toolCatalog := renderExecuteToolCatalogCompact(ctx)
if toolCatalog == "" {
return base
}
return base + "\n\n" + toolCatalog
}
// buildExecuteMessage1 生成历史上下文短摘要。
func buildExecuteMessage1(ctx *newagentmodel.ConversationContext) string {
lines := []string{"历史上下文(仅供参考):"}
if ctx == nil {
lines = append(lines,
"- 用户目标:暂无可用历史输入。",
"- 阶段锚点:按当前工具事实推进执行。",
"- 早期 ReAct 摘要:暂无。",
)
return strings.Join(lines, "\n")
}
history := ctx.HistorySnapshot()
firstUser, lastUser := pickExecuteUserInputs(history)
switch {
case firstUser == "":
lines = append(lines, "- 用户目标:暂无可用历史输入。")
case lastUser != "" && lastUser != firstUser:
lines = append(lines, "- 用户目标:"+firstUser+";最近补充:"+lastUser)
default:
lines = append(lines, "- 用户目标:"+firstUser)
}
if hasExecuteRoughBuildDone(ctx) {
lines = append(lines, "- 阶段锚点:粗排已完成,本轮仅做微调,不重新 place。")
} else {
lines = append(lines, "- 阶段锚点:按当前工具事实推进,不做无依据操作。")
}
allLoops := collectExecuteLoopRecords(history)
lines = append(lines, "- 早期 ReAct 摘要:"+buildEarlyExecuteReactSummary(allLoops, executeLoopWindowLimit))
return strings.Join(lines, "\n")
}
// buildExecuteMessage2 生成当轮 ReAct Loop 窗口。
//
// 规则:
// 1. 每条记录都展示 thought/reason + tool_call + observation
// 2. 对窗口内重复工具应用压缩:同工具只保留最新一条真实 observation
// 3. 被压缩的旧 observation 统一替换为占位文案,避免语义断裂。
func buildExecuteMessage2(ctx *newagentmodel.ConversationContext) string {
lines := []string{"当轮 ReAct Loop 记录(窗口):"}
if ctx == nil {
lines = append(lines, "- 暂无可用 ReAct 记录。")
return strings.Join(lines, "\n")
}
allLoops := collectExecuteLoopRecords(ctx.HistorySnapshot())
if len(allLoops) == 0 {
lines = append(lines, "- 暂无可用 ReAct 记录。")
return strings.Join(lines, "\n")
}
windowLoops := tailExecuteLoops(allLoops, executeLoopWindowLimit)
windowLoops = compressExecuteLoopObservationsByTool(windowLoops)
for i, loop := range windowLoops {
lines = append(lines, fmt.Sprintf("%d) thought/reason%s", i+1, loop.Thought))
lines = append(lines, fmt.Sprintf(" tool_call%s", renderExecuteToolCallText(loop.ToolName, loop.ToolArgs)))
lines = append(lines, fmt.Sprintf(" observation%s", loop.Observation))
}
return strings.Join(lines, "\n")
}
// buildExecuteMessage3 生成当前执行状态与执行锚点。
// buildExecuteMessage1V2 生成历史摘要:
// 1. 已收口的 loop 归档到 msg1
// 2. 当前活跃 loop 只保留“早期摘要”;
// 3. 最终对 msg1 做统一长度裁剪,控制 token 开销。
func buildExecuteMessage1V2(ctx *newagentmodel.ConversationContext) string {
lines := []string{"历史上下文(仅供参考):"}
if ctx == nil {
lines = append(lines,
"- 用户目标:暂无可用历史输入。",
"- 阶段锚点:按当前工具事实推进执行。",
"- 历史归档 ReAct 摘要:暂无。",
"- 当前循环早期摘要:暂无。",
)
return trimExecuteMessage1ByBudget(strings.Join(lines, "\n"))
}
history := ctx.HistorySnapshot()
firstUser, lastUser := pickExecuteUserInputs(history)
switch {
case firstUser == "":
lines = append(lines, "- 用户目标:暂无可用历史输入。")
case lastUser != "" && lastUser != firstUser:
lines = append(lines, "- 用户目标:"+firstUser+";最近补充:"+lastUser)
default:
lines = append(lines, "- 用户目标:"+firstUser)
}
if hasExecuteRoughBuildDone(ctx) {
lines = append(lines, "- 阶段锚点:粗排已完成,本轮仅做微调,不重新 place。")
} else {
lines = append(lines, "- 阶段锚点:按当前工具事实推进,不做无依据操作。")
}
archivedLoops, activeLoops := splitExecuteLoopRecordsByBoundary(history)
lines = append(lines, "- 历史归档 ReAct 摘要:"+buildEarlyExecuteReactSummary(archivedLoops, 0))
lines = append(lines, "- 当前循环早期摘要:"+buildEarlyExecuteReactSummary(activeLoops, executeLoopWindowLimit))
return trimExecuteMessage1ByBudget(strings.Join(lines, "\n"))
}
// buildExecuteMessage2V2 仅展示“当前活跃 loop”的窗口记录。
func buildExecuteMessage2V2(ctx *newagentmodel.ConversationContext) string {
lines := []string{"当轮 ReAct Loop 记录(窗口):"}
if ctx == nil {
lines = append(lines, "- 暂无可用 ReAct 记录。")
return strings.Join(lines, "\n")
}
_, activeLoops := splitExecuteLoopRecordsByBoundary(ctx.HistorySnapshot())
if len(activeLoops) == 0 {
lines = append(lines, "- 暂无可用 ReAct 记录。")
return strings.Join(lines, "\n")
}
windowLoops := tailExecuteLoops(activeLoops, executeLoopWindowLimit)
windowLoops = compressExecuteLoopObservationsByTool(windowLoops)
for i, loop := range windowLoops {
lines = append(lines, fmt.Sprintf("%d) thought/reason%s", i+1, loop.Thought))
lines = append(lines, fmt.Sprintf(" tool_call%s", renderExecuteToolCallText(loop.ToolName, loop.ToolArgs)))
lines = append(lines, fmt.Sprintf(" observation%s", loop.Observation))
}
return strings.Join(lines, "\n")
}
// splitExecuteLoopRecordsByBoundary 按“已收口标记”拆分归档/活跃 ReAct 记录。
//
// 规则:
// 1. 标记之前的记录归档到 msg1
// 2. 标记之后的记录作为活跃 loop 进入 msg2
// 3. 若没有标记,则全部视为活跃记录(兼容旧会话快照)。
func splitExecuteLoopRecordsByBoundary(history []*schema.Message) (archived []executeLoopRecord, active []executeLoopRecord) {
if len(history) == 0 {
return nil, nil
}
boundary := findLatestExecuteBoundaryMarker(history)
if boundary < 0 {
return nil, collectExecuteLoopRecords(history)
}
if boundary > 0 {
archived = collectExecuteLoopRecords(history[:boundary])
}
if boundary+1 < len(history) {
active = collectExecuteLoopRecords(history[boundary+1:])
}
return archived, active
}
func findLatestExecuteBoundaryMarker(history []*schema.Message) int {
for i := len(history) - 1; i >= 0; i-- {
msg := history[i]
if msg == nil || msg.Extra == nil {
continue
}
kind, ok := msg.Extra[executeHistoryKindKey].(string)
if !ok {
continue
}
switch strings.TrimSpace(kind) {
case executeHistoryKindLoopClosed, executeHistoryKindStepAdvanced:
return i
}
}
return -1
}
func trimExecuteMessage1ByBudget(content string) string {
content = strings.TrimSpace(content)
if content == "" {
return ""
}
runes := []rune(content)
if len(runes) <= executeMessage1MaxRunes {
return content
}
if executeMessage1MaxRunes <= 3 {
return string(runes[:executeMessage1MaxRunes])
}
return string(runes[:executeMessage1MaxRunes-3]) + "..."
}
// buildExecuteMessage1V3 负责把“上一轮 loop 归档”并入 msg1并统一做长度裁剪。
func buildExecuteMessage1V3(ctx *newagentmodel.ConversationContext) string {
lines := []string{"历史上下文(仅供参考):"}
if ctx == nil {
lines = append(lines,
"- 用户目标:暂无可用历史输入。",
"- 阶段锚点:按当前工具事实推进执行。",
"- 历史归档 ReAct 摘要:暂无。",
"- 历史归档 ReAct 窗口:暂无。",
"- 当前循环早期摘要:暂无。",
)
return trimExecuteMessage1ByBudget(strings.Join(lines, "\n"))
}
history := ctx.HistorySnapshot()
firstUser, lastUser := pickExecuteUserInputs(history)
switch {
case firstUser == "":
lines = append(lines, "- 用户目标:暂无可用历史输入。")
case lastUser != "" && lastUser != firstUser:
lines = append(lines, "- 用户目标:"+firstUser+";最近补充:"+lastUser)
default:
lines = append(lines, "- 用户目标:"+firstUser)
}
if hasExecuteRoughBuildDone(ctx) {
lines = append(lines, "- 阶段锚点:粗排已完成,本轮仅做微调,不重新 place。")
} else {
lines = append(lines, "- 阶段锚点:按当前工具事实推进,不做无依据操作。")
}
// 1. 通过收口标记拆分“归档 loop / 当前活跃 loop”。
// 2. 归档 loop 的窗口条目直接并入 msg1满足“上一轮 msg2 挪入 msg1”。
// 3. 当前活跃 loop 在 msg1 只保留早期摘要,详细窗口交给 msg2。
archivedLoops, activeLoops := splitExecuteLoopRecordsByBoundary(history)
lines = append(lines, "- 历史归档 ReAct 摘要:"+buildEarlyExecuteReactSummary(archivedLoops, executeLoopWindowLimit))
lines = append(lines, renderArchivedExecuteLoopWindowForMessage1V3(archivedLoops))
lines = append(lines, "- 当前循环早期摘要:"+buildEarlyExecuteReactSummary(activeLoops, executeLoopWindowLimit))
return trimExecuteMessage1ByBudget(strings.Join(lines, "\n"))
}
// buildExecuteMessage2V3 仅承载“当前活跃 loop”的窗口。
// 若是新一轮刚开始(活跃 loop 为空),明确返回“已清空”状态。
func buildExecuteMessage2V3(ctx *newagentmodel.ConversationContext) string {
lines := []string{"当轮 ReAct Loop 记录(窗口):"}
if ctx == nil {
lines = append(lines, "- 暂无可用 ReAct 记录。")
return strings.Join(lines, "\n")
}
_, activeLoops := splitExecuteLoopRecordsByBoundary(ctx.HistorySnapshot())
if len(activeLoops) == 0 {
lines = append(lines, "- 已清空(新一轮 loop 准备中)。")
return strings.Join(lines, "\n")
}
windowLoops := tailExecuteLoops(activeLoops, executeLoopWindowLimit)
windowLoops = compressExecuteLoopObservationsByTool(windowLoops)
for i, loop := range windowLoops {
lines = append(lines, fmt.Sprintf("%d) thought/reason%s", i+1, loop.Thought))
lines = append(lines, fmt.Sprintf(" tool_call%s", renderExecuteToolCallText(loop.ToolName, loop.ToolArgs)))
lines = append(lines, fmt.Sprintf(" observation%s", loop.Observation))
}
return strings.Join(lines, "\n")
}
func renderArchivedExecuteLoopWindowForMessage1V3(records []executeLoopRecord) string {
if len(records) == 0 {
return "- 历史归档 ReAct 窗口:暂无。"
}
windowLoops := tailExecuteLoops(records, executeLoopWindowLimit)
windowLoops = compressExecuteLoopObservationsByTool(windowLoops)
lines := []string{"历史归档 ReAct 窗口(由上一轮 msg2 并入):"}
for i, loop := range windowLoops {
lines = append(lines, fmt.Sprintf("%d) thought/reason%s", i+1, loop.Thought))
lines = append(lines, fmt.Sprintf(" tool_call%s", renderExecuteToolCallText(loop.ToolName, loop.ToolArgs)))
lines = append(lines, fmt.Sprintf(" observation%s", loop.Observation))
}
return strings.Join(lines, "\n")
}
func buildExecuteMessage3(state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext, runtimeUserPrompt string) string {
lines := []string{"当前执行状态:"}
roundUsed, maxRounds := 0, newagentmodel.DefaultMaxRounds
modeText := "自由执行(无预定义步骤)"
if state != nil {
roundUsed = state.RoundUsed
if state.MaxRounds > 0 {
maxRounds = state.MaxRounds
}
if state.HasPlan() {
modeText = "计划执行(有预定义步骤)"
}
}
lines = append(lines,
fmt.Sprintf("- 当前轮次:%d/%d", roundUsed, maxRounds),
"- 当前模式:"+modeText,
)
initialGoal, currentGoal := extractExecuteGoalAnchors(ctx)
if currentGoal == "" {
currentGoal = "暂无可用目标描述,请按当前上下文稳步推进。"
}
lines = append(lines, "执行锚点:")
lines = append(lines, "- 当前用户诉求:"+currentGoal)
if initialGoal != "" && initialGoal != currentGoal {
lines = append(lines, "- 首轮目标来源:"+initialGoal)
}
// 1. 有 plan 时,把当前步骤与完成判定强制写入 msg3。
// 2. 该锚点用于约束模型“只推进当前步骤”,避免退化成泛化 ReAct。
// 3. 当前步骤不可读时给出兜底指引,避免引用旧步骤。
if state != nil && state.HasPlan() {
current, total := state.PlanProgress()
lines = append(lines, "计划步骤锚点(强约束):")
if step, ok := state.CurrentPlanStep(); ok {
stepContent := strings.TrimSpace(step.Content)
if stepContent == "" {
stepContent = "(当前步骤内容为空)"
}
doneWhen := strings.TrimSpace(step.DoneWhen)
if doneWhen == "" {
doneWhen = "(未提供 done_when需基于步骤目标给出可验证完成证据"
}
lines = append(lines, fmt.Sprintf("- 当前步骤:第 %d/%d 步", current, total))
lines = append(lines, "- 当前步骤内容:"+stepContent)
lines = append(lines, "- 当前步骤完成判定(done_when)"+doneWhen)
lines = append(lines, "- 动作纪律1未满足 done_when 时,只能 continue / confirm / ask_user禁止 next_plan")
lines = append(lines, "- 动作纪律2满足 done_when 时,优先 next_plan并在 goal_check 对照 done_when 给证据")
lines = append(lines, "- 动作纪律3禁止跳到后续步骤执行")
} else {
lines = append(lines, "- 当前计划步骤不可读;请先判断是否已完成全部计划")
lines = append(lines, "- 若已完成全部计划,输出 done 并给出 goal_check 证据")
}
}
if taskClassText := renderExecuteTaskClassIDs(state); taskClassText != "" {
lines = append(lines, "- 目标任务类:"+taskClassText)
}
lines = append(lines, "- 啥时候结束Loop你可以根据工具调用记录自行判断。")
lines = append(lines, "- 非目标:不重新粗排、不修改无关任务类。")
if hasExecuteRoughBuildDone(ctx) {
lines = append(lines, "- 阶段约束:粗排已完成,本轮只微调 suggestedexisting 仅作已安排事实参考,不作为可移动目标。")
}
lines = append(lines, "- 参数纪律:工具参数必须严格使用 schema 字段;若返回“参数非法”,需先改参再继续。")
if state != nil {
if state.AllowReorder {
lines = append(lines, "- 顺序策略:用户已明确允许打乱顺序,可在必要时使用 min_context_switch。")
} else {
lines = append(lines, "- 顺序策略:默认保持 suggested 相对顺序,禁止调用 min_context_switch。")
}
}
// 兼容上层传入的执行指令;若为空则使用固定收口指令。
instruction := strings.TrimSpace(runtimeUserPrompt)
if instruction == "" {
instruction = "请继续当前任务执行阶段,严格输出 JSON。"
} else {
instruction = firstExecuteLine(instruction)
}
lines = append(lines, "本轮指令:"+instruction)
return strings.Join(lines, "\n")
}
// renderExecuteToolCatalogCompact 将工具 schema 渲染成简表,避免大段 JSON 示例占用上下文。
func renderExecuteToolCatalogCompact(ctx *newagentmodel.ConversationContext) string {
if ctx == nil {
return ""
}
schemas := ctx.ToolSchemasSnapshot()
if len(schemas) == 0 {
return ""
}
lines := []string{"可用工具(简表):"}
for i, schemaItem := range schemas {
name := strings.TrimSpace(schemaItem.Name)
desc := strings.TrimSpace(schemaItem.Desc)
if name == "" {
continue
}
if desc == "" {
desc = "无描述"
}
lines = append(lines, fmt.Sprintf("%d. %s%s", i+1, name, desc))
doc := parseExecuteToolSchema(schemaItem.SchemaText)
paramSummary := renderExecuteToolParamSummary(doc.Parameters)
lines = append(lines, " 参数:"+paramSummary)
returnType, returnSample := renderExecuteToolReturnHint(name)
lines = append(lines, " 返回类型:"+returnType)
lines = append(lines, " 返回示例:"+returnSample)
}
return strings.Join(lines, "\n")
}
// renderExecuteToolReturnHint 返回工具的“返回类型 + 最小示例”。
//
// 说明:
// 1. 所有工具当前都返回 string但部分是“JSON 字符串”,这里补齐内容形态示例减少模型盲猜;
// 2. 示例只保留最小片段,避免工具说明过长挤占上下文窗口。
func renderExecuteToolReturnHint(toolName string) (returnType string, sample string) {
returnType = "string自然语言文本"
switch strings.ToLower(strings.TrimSpace(toolName)) {
case "get_overview":
return returnType, "规划窗口共27天...课程占位条目34个...任务清单(全量,已过滤课程)..."
case "list_tasks":
return returnType, "已预排任务共24个 [35]第一章随机事件与概率 — 已预排至 第3天第5-6节..."
case "get_task_info":
return returnType, "[35]第一章随机事件与概率 | 状态:已预排(suggested) | 占用时段第3天第5-6节"
case "query_available_slots":
return "stringJSON字符串", `{"tool":"query_available_slots","count":12,"strict_count":8,"embedded_count":4,"slots":[{"day":5,"week":12,"day_of_week":3,"slot_start":1,"slot_end":2,"slot_type":"empty"}]}`
case "query_target_tasks":
return "stringJSON字符串", `{"tool":"query_target_tasks","count":6,"status":"suggested","enqueue":true,"enqueued":6,"queue":{"pending_count":6},"items":[{"task_id":35,"name":"示例任务","status":"suggested","slots":[{"day":3,"week":12,"day_of_week":1,"slot_start":5,"slot_end":6}]}]}`
case "queue_pop_head":
return "stringJSON字符串", `{"tool":"queue_pop_head","has_head":true,"pending_count":5,"current":{"task_id":35,"name":"示例任务","status":"suggested","slots":[{"day":3,"week":12,"day_of_week":1,"slot_start":5,"slot_end":6}]}}`
case "queue_status":
return "stringJSON字符串", `{"tool":"queue_status","pending_count":5,"completed_count":1,"skipped_count":0,"current_task_id":35,"current_attempt":1}`
case "queue_apply_head_move":
return "stringJSON字符串", `{"tool":"queue_apply_head_move","success":true,"task_id":35,"pending_count":4,"completed_count":2,"result":"已将 [35]... 从第3天第5-6节移至第5天第3-4节。"}`
case "queue_skip_head":
return "stringJSON字符串", `{"tool":"queue_skip_head","success":true,"skipped_task_id":35,"pending_count":4,"skipped_count":1}`
case "query_range":
return returnType, "第5天第3-6节第3节空、第4节空..."
case "place":
return returnType, "已将 [35]... 预排到第5天第3-4节。"
case "move":
return returnType, "已将 [35]... 从第3天第5-6节移至第5天第3-4节。"
case "swap":
return returnType, "交换完成:[35]... ↔ [36]..."
case "batch_move":
return returnType, "批量移动完成2个任务全部成功。单次最多2条"
case "spread_even":
return returnType, "均匀化调整完成:共处理 6 个任务,候选坑位 24 个。"
case "min_context_switch":
return returnType, "最少上下文切换重排完成:共处理 6 个任务,上下文切换次数 5 -> 2。"
case "unplace":
return returnType, "已将 [35]... 移除,恢复为待安排状态。"
case "web_search":
return "stringJSON字符串", `{"tool":"web_search","query":"检索关键词","count":2,"items":[{"title":"搜索结果标题","url":"https://example.com/page","snippet":"摘要片段...","domain":"example.com","published_at":"2025-04-10"}]}`
case "web_fetch":
return "stringJSON字符串", `{"tool":"web_fetch","url":"https://example.com/page","title":"页面标题","content":"正文内容...","truncated":false}`
default:
return returnType, "自然语言结果(成功/失败原因/关键数据摘要)。"
}
}
func parseExecuteToolSchema(schemaText string) executeToolSchemaDoc {
doc := executeToolSchemaDoc{Parameters: map[string]any{}}
schemaText = strings.TrimSpace(schemaText)
if schemaText == "" {
return doc
}
if err := json.Unmarshal([]byte(schemaText), &doc); err != nil {
return doc
}
if doc.Parameters == nil {
doc.Parameters = map[string]any{}
}
return doc
}
func renderExecuteToolParamSummary(parameters map[string]any) string {
if len(parameters) == 0 {
return "{}"
}
keys := make([]string, 0, len(parameters))
for key := range parameters {
keys = append(keys, key)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, key := range keys {
status := "可选"
typeText := ""
switch typed := parameters[key].(type) {
case string:
status = "必填"
typeText = strings.TrimSpace(typed)
case map[string]any:
if required, ok := typed["required"].(bool); ok && required {
status = "必填"
}
typeText = strings.TrimSpace(asExecuteString(typed["type"]))
if enumRaw, ok := typed["enum"].([]any); ok && len(enumRaw) > 0 {
enumText := make([]string, 0, len(enumRaw))
for _, item := range enumRaw {
enumText = append(enumText, fmt.Sprintf("%v", item))
}
if typeText == "" {
typeText = "enum"
}
typeText += ":" + strings.Join(enumText, "/")
}
}
if typeText == "" {
parts = append(parts, fmt.Sprintf("%s(%s)", key, status))
continue
}
parts = append(parts, fmt.Sprintf("%s(%s,%s)", key, status, typeText))
}
return strings.Join(parts, "")
}
// collectExecuteLoopRecords 从历史中提取 ReAct 记录。
//
// 提取策略:
// 1. 以 assistant tool_call 消息为主键;
// 2. 关联同 ToolCallID 的 tool result 作为 observation
// 3. 向前回溯最近一条 assistant 文本消息作为 thought/reason。
func collectExecuteLoopRecords(history []*schema.Message) []executeLoopRecord {
if len(history) == 0 {
return nil
}
toolResultByCallID := make(map[string]*schema.Message, len(history))
for _, msg := range history {
if msg == nil || msg.Role != schema.Tool {
continue
}
callID := strings.TrimSpace(msg.ToolCallID)
if callID == "" {
continue
}
toolResultByCallID[callID] = msg
}
records := make([]executeLoopRecord, 0, len(history))
for i, msg := range history {
if msg == nil || msg.Role != schema.Assistant || len(msg.ToolCalls) == 0 {
continue
}
thought := findExecuteThoughtBefore(history, i)
for _, call := range msg.ToolCalls {
toolName := strings.TrimSpace(call.Function.Name)
if toolName == "" {
toolName = "unknown_tool"
}
toolArgs := compactExecuteText(call.Function.Arguments, 160)
if toolArgs == "" {
toolArgs = "{}"
}
observation := "该工具调用尚未返回结果。"
callID := strings.TrimSpace(call.ID)
if callID != "" {
if resultMsg, ok := toolResultByCallID[callID]; ok && resultMsg != nil {
text := strings.TrimSpace(resultMsg.Content)
if text != "" {
observation = text
}
}
}
records = append(records, executeLoopRecord{
Thought: thought,
ToolName: toolName,
ToolArgs: toolArgs,
Observation: observation,
})
}
}
return records
}
func findExecuteThoughtBefore(history []*schema.Message, index int) string {
for i := index - 1; i >= 0; i-- {
msg := history[i]
if msg == nil || msg.Role != schema.Assistant {
continue
}
if len(msg.ToolCalls) > 0 {
continue
}
content := compactExecuteText(msg.Content, 140)
if content == "" {
continue
}
return content
}
return "(未记录)"
}
func tailExecuteLoops(records []executeLoopRecord, limit int) []executeLoopRecord {
if len(records) == 0 {
return nil
}
if limit <= 0 || len(records) <= limit {
result := make([]executeLoopRecord, len(records))
copy(result, records)
return result
}
result := make([]executeLoopRecord, limit)
copy(result, records[len(records)-limit:])
return result
}
// compressExecuteLoopObservationsByTool 对窗口内重复工具做 observation 压缩。
//
// 规则:
// 1. 以“工具名”作为压缩键;
// 2. 同工具仅保留最新一条 observation 原文;
// 3. 旧记录保持 thought/tool_call不丢记录仅替换 observation。
func compressExecuteLoopObservationsByTool(records []executeLoopRecord) []executeLoopRecord {
if len(records) == 0 {
return records
}
latestIndexByTool := make(map[string]int, len(records))
for i := len(records) - 1; i >= 0; i-- {
key := strings.ToLower(strings.TrimSpace(records[i].ToolName))
if key == "" {
key = "unknown_tool"
}
if _, exists := latestIndexByTool[key]; !exists {
latestIndexByTool[key] = i
}
}
result := make([]executeLoopRecord, len(records))
copy(result, records)
for i := range result {
key := strings.ToLower(strings.TrimSpace(result[i].ToolName))
if key == "" {
key = "unknown_tool"
}
if latestIndexByTool[key] != i {
result[i].Observation = executeTrimmedObservationText
}
}
return result
}
func renderExecuteToolCallText(toolName, toolArgs string) string {
toolName = strings.TrimSpace(toolName)
if toolName == "" {
toolName = "unknown_tool"
}
toolArgs = strings.TrimSpace(toolArgs)
if toolArgs == "" {
toolArgs = "{}"
}
return toolName + "(" + toolArgs + ")"
}
func buildEarlyExecuteReactSummary(records []executeLoopRecord, windowLimit int) string {
if len(records) == 0 {
return "暂无。"
}
if len(records) <= windowLimit {
return "无(当前窗口已覆盖全部 ReAct 记录)。"
}
early := records[:len(records)-windowLimit]
toolCounts := make(map[string]int, len(early))
for _, record := range early {
key := strings.TrimSpace(record.ToolName)
if key == "" {
key = "unknown_tool"
}
toolCounts[key]++
}
names := make([]string, 0, len(toolCounts))
for name := range toolCounts {
names = append(names, name)
}
sort.Strings(names)
parts := make([]string, 0, len(names))
for _, name := range names {
parts = append(parts, fmt.Sprintf("%s×%d", name, toolCounts[name]))
}
return fmt.Sprintf("已折叠 %d 条旧记录,涉及:%s。", len(early), strings.Join(parts, "、"))
}
func extractExecuteGoalAnchors(ctx *newagentmodel.ConversationContext) (initial string, current string) {
if ctx == nil {
return "", ""
}
history := ctx.HistorySnapshot()
firstUser, lastUser := pickExecuteUserInputs(history)
return firstUser, lastUser
}
func hasExecuteRoughBuildDone(ctx *newagentmodel.ConversationContext) bool {
if ctx == nil {
return false
}
for _, block := range ctx.PinnedBlocksSnapshot() {
if strings.TrimSpace(block.Key) == "rough_build_done" {
return true
}
}
return false
}
func pickExecuteUserInputs(history []*schema.Message) (first string, last string) {
realUsers := make([]string, 0, 2)
for _, msg := range history {
if msg == nil || msg.Role != schema.User {
continue
}
if isExecuteCorrectionPrompt(msg) {
continue
}
text := compactExecuteText(msg.Content, 120)
if text == "" {
continue
}
realUsers = append(realUsers, text)
}
if len(realUsers) == 0 {
return "", ""
}
return realUsers[0], realUsers[len(realUsers)-1]
}
func isExecuteCorrectionPrompt(msg *schema.Message) bool {
if msg == nil || msg.Role != schema.User {
return false
}
if msg.Extra != nil {
if kind, ok := msg.Extra[executeHistoryKindKey].(string); ok && strings.TrimSpace(kind) == executeHistoryKindCorrectionUser {
return true
}
}
content := strings.TrimSpace(msg.Content)
return strings.Contains(content, "请重新分析当前状态,输出正确的内容。")
}
func compactExecuteText(content string, maxLen int) string {
content = firstExecuteLine(content)
content = strings.TrimSpace(content)
if content == "" {
return ""
}
runes := []rune(content)
if len(runes) <= maxLen {
return content
}
if maxLen <= 3 {
return string(runes[:maxLen])
}
return string(runes[:maxLen-3]) + "..."
}
func firstExecuteLine(content string) string {
content = strings.TrimSpace(content)
if content == "" {
return ""
}
lines := strings.Split(content, "\n")
return strings.TrimSpace(lines[0])
}
func asExecuteString(value any) string {
if text, ok := value.(string); ok {
return text
}
return ""
}
func renderExecuteTaskClassIDs(state *newagentmodel.CommonState) string {
if state == nil || len(state.TaskClassIDs) == 0 {
return ""
}
parts := make([]string, len(state.TaskClassIDs))
for i, id := range state.TaskClassIDs {
parts[i] = strconv.Itoa(id)
}
return fmt.Sprintf("task_class_ids=[%s]", strings.Join(parts, ","))
}