Files
smartmate/backend/newAgent/prompt/plan_context.go
LoveLosita 736ba0cff3 Version: 0.9.46.dev.260427
后端:
1. taskclass 执行闭环继续收紧——Plan / Execute 全面切到“最小工具闭环”视角,明确学习目标/总节数/禁排时段/排除星期默认停留 taskclass 域;未给日期范围时禁止擅自补 start_date/end_date,upsert_task_class 重试前先做写前检查并区分“内部表示修正”与“必须追问用户”的关键时间事实
2. QuickTask / TaskQuery 轻量链路继续收敛——新增 model/taskquery_contract.go 统一查询协议,QuickTaskDeps / start.go 改用 model 层参数;删除 query_tasks / quick_note_create 旧工具实现,避免任务查询与随口记再回流 execute 工具链
3. schedule 微调工具继续瘦身——下线 spread_even / min_context_switch 及其复合规划逻辑,清理 analyze_load / analyze_subjects / analyze_context / analyze_tolerance 等历史能力;execute 顺序策略收敛为局部 move / swap,提示词与工具目录仅暴露当前真实可用工具
4. 执行与时间线体验补齐——execute 为流式 speak 补发归一化尾部,避免 deliver 文案黏连;前端时间线新增 interrupt / status 协议识别、工具事件归并与状态过滤,减少 ToolTrace 重复和会话重建误判
前端:
5. AssistantPanel 适配新版 timeline extra 事件——schedule_agent.ts 补齐 interrupt / status kind,工具调用与结果按摘要/参数/工具名合并,恢复历史时不再把协议事件误判成用户消息
2026-04-27 12:20:17 +08:00

224 lines
9.6 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 (
"fmt"
"strconv"
"strings"
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
)
// buildPlanConversationMessage 生成 plan 节点看到的真实对话视图。
func buildPlanConversationMessage(ctx *newagentmodel.ConversationContext) string {
return buildConversationHistoryMessage(ctx, "规划参考对话")
}
// buildPlanWorkspace 渲染 plan 节点自己的工作区。
//
// 设计说明:
// 1. 这里既保留“当前已有计划/任务类约束”,也显式补充“规划视角的工具摘要”;
// 2. planner 需要先理解工具边界,才能把步骤收敛到最小闭环,而不是按抽象语义乱拆;
// 3. 工具摘要不展开全量 schema只提供规划真正需要的负责什么、不负责什么、常见闭环、完成证据、域切换条件。
func buildPlanWorkspace(state *newagentmodel.CommonState) string {
lines := []string{"规划工作区:"}
if state == nil {
lines = append(lines, "- 当前缺少流程状态,请主要依据最近对话与本轮输入继续规划。")
lines = append(lines, buildPlanToolPlanningSummary())
return strings.Join(lines, "\n")
}
if !state.HasPlan() {
lines = append(lines, "- 当前还没有正式计划。")
} else {
lines = append(lines, fmt.Sprintf("- 已有计划:共 %d 步。", len(state.PlanSteps)))
lines = append(lines, renderPlanCurrentStepSummary(state))
lines = append(lines, "计划简表:")
lines = append(lines, renderPlanStepOutline(state.PlanSteps))
}
if taskClassIDs := renderPlanTaskClassIDs(state); taskClassIDs != "" {
lines = append(lines, "- "+taskClassIDs)
}
if taskClassMeta := renderPlanTaskClassMeta(state); taskClassMeta != "" {
lines = append(lines, "任务类约束:")
lines = append(lines, taskClassMeta)
}
lines = append(lines, buildPlanToolPlanningSummary())
return strings.Join(lines, "\n")
}
// renderPlanCurrentStepSummary 返回 plan 节点需要知道的当前步骤进度。
func renderPlanCurrentStepSummary(state *newagentmodel.CommonState) string {
if state == nil || !state.HasPlan() {
return "- 当前步骤:暂无。"
}
current, total := state.PlanProgress()
step, ok := state.CurrentPlanStep()
if !ok {
return fmt.Sprintf("- 当前步骤:计划共 %d 步,当前没有可继续沿用的有效步骤。", total)
}
content := strings.TrimSpace(step.Content)
if content == "" {
content = "(当前步骤正文为空)"
}
summary := fmt.Sprintf("- 当前步骤:第 %d/%d 步,%s", current, total, content)
if doneWhen := strings.TrimSpace(step.DoneWhen); doneWhen != "" {
summary += fmt.Sprintf(";完成判定:%s", doneWhen)
}
return summary
}
// renderPlanStepOutline 将完整计划压成 plan 节点可读的简表。
func renderPlanStepOutline(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 == "" {
content = "(步骤正文为空)"
}
line := fmt.Sprintf("%d. %s", i+1, content)
if doneWhen := strings.TrimSpace(step.DoneWhen); doneWhen != "" {
line += fmt.Sprintf(" | 完成判定:%s", doneWhen)
}
lines = append(lines, line)
}
return strings.Join(lines, "\n")
}
// renderPlanTaskClassIDs 返回批量排课场景下的 task_class_ids 简表。
func renderPlanTaskClassIDs(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, ", "))
}
// renderPlanTaskClassMeta 返回 plan 节点真正需要看的任务类边界。
//
// 说明:
// 1. 这里只保留名称、策略、总时段、日期范围这类规划相关信息;
// 2. 不再把所有字段原样平铺,避免工作区过胖;
// 3. 若某项字段为空,则直接省略,不制造噪声。
func renderPlanTaskClassMeta(state *newagentmodel.CommonState) string {
if state == nil || len(state.TaskClasses) == 0 {
return ""
}
lines := make([]string, 0, len(state.TaskClasses))
for _, tc := range state.TaskClasses {
line := fmt.Sprintf("- [ID=%d] %s", tc.ID, strings.TrimSpace(tc.Name))
if strategy := strings.TrimSpace(tc.Strategy); strategy != "" {
line += fmt.Sprintf(";策略:%s", strategy)
}
if tc.TotalSlots > 0 {
line += fmt.Sprintf(";总时段预算:%d", tc.TotalSlots)
}
if tc.StartDate != "" || tc.EndDate != "" {
line += fmt.Sprintf(";日期范围:%s ~ %s", tc.StartDate, tc.EndDate)
}
if len(tc.ExcludedDaysOfWeek) > 0 {
line += fmt.Sprintf(";排除星期:%v", tc.ExcludedDaysOfWeek)
}
if tc.SubjectType != "" || tc.DifficultyLevel != "" || tc.CognitiveIntensity != "" {
line += fmt.Sprintf(";语义画像:%s/%s/%s",
planSemanticValue(tc.SubjectType),
planSemanticValue(tc.DifficultyLevel),
planSemanticValue(tc.CognitiveIntensity),
)
}
lines = append(lines, line)
}
return strings.Join(lines, "\n")
}
// buildPlanToolPlanningSummary 生成“规划视角的工具摘要”。
//
// 步骤化说明:
// 1. 先讲 domain让 planner 先判断目标应该停留在哪个业务域;
// 2. 再讲 schedule packs让 planner 知道若进入 schedule该选最小哪组能力
// 3. 最后讲 hook 推导规则:因为 context_hook 只有一份,必须和“首个可执行闭环”对齐。
func buildPlanToolPlanningSummary() string {
sections := []string{
"规划视角的工具摘要:",
buildPlanToolDomainTaskClassSummary(),
buildPlanToolDomainScheduleSummary(),
buildPlanToolPackSummary(),
buildPlanContextHookSummary(),
}
return strings.Join(sections, "\n")
}
func buildPlanToolDomainTaskClassSummary() string {
lines := []string{
"1. taskclass 域:",
"- 负责什么:创建 / 更新任务类,沉淀学习目标、总节数、难度、节次偏好、禁排时段、排除星期、内容拆分授权、任务项结构。",
"- 不负责什么:不给出具体日期/节次落位,不负责把任务真正排进日程。",
"- 常见一步闭环:任务类设计或修正通常可由 taskclass 域单步闭环,核心写入动作为 upsert_task_class。",
"- 何时停留在本域:用户仍在描述目标、偏好、约束、拆分方式,而不是要求现在排进日程。",
"- 何时切到下一个域:只有用户明确要求“排进日程 / 给出具体时间安排 / 现在就排一版”,或当前目标本身已变成排程执行。",
"- done_when 证据偏好:优先锚定 upsert_task_class 成功回执、validation.ok=true、validation.issues 已清空。",
}
return strings.Join(lines, "\n")
}
func buildPlanToolDomainScheduleSummary() string {
lines := []string{
"2. schedule 域:",
"- 负责什么:查询日程现状、粗排、具体落位、局部移动/交换、批量同规则调整、排程健康分析。",
"- 不负责什么不凭空补考试时间、DDL、个人空闲、外部时间事实这类信息拿不到时应 ask_user。",
"- 常见一步闭环:单次查询通常一个读工具即可闭环;单次移动/交换/放置通常一个写工具即可闭环;局部分析通常一个 analyze 工具即可闭环。",
"- 何时停留在本域:用户明确要求查询、安排、调整、优化当前日程。",
"- 何时先回 taskclass如果用户还在定义“学什么、学多少、怎么拆、哪些时段不要学”而不是要求立刻排程应先停留在 taskclass。",
"- done_when 证据偏好:优先锚定查询 observation、写工具成功回执、rough_build_done 标记、analyze observation 已能直接支撑下一步。",
}
return strings.Join(lines, "\n")
}
func buildPlanToolPackSummary() string {
lines := []string{
"3. schedule packs 选择参考:",
"- detail_read查看总览、查询区间、看任务详情适合“先读事实再决定”的首步。",
"- mutationplace / move / swap / batch_move / unplace适合真正落日程或调日程。",
"- analyzeanalyze_health / analyze_rhythm适合先判断是否还有优化空间、该往哪里动。",
"- queue适合“按同一规则逐个处理一批任务”的计划不必把整批任务细节都堆进 steps。",
"- web仅补通用学习资料或通识信息不用于补个人时间事实。",
"- deep_analyze适合确实需要更深一层 schedule 分析时再加,默认不要为了“看起来完整”就提前注入。",
"- 选 pack 原则:只选首个可执行 step 真的需要的最小 packs不要为了保险一次全带上。",
}
return strings.Join(lines, "\n")
}
func buildPlanContextHookSummary() string {
lines := []string{
"4. context_hook 推导规则:",
"- 先确定 steps再看第一个可执行 step 需要哪个 domain / 哪组最小 packs最后才写 hook。",
"- 若第一个可执行 step 本质上是任务类写入或修正hook 通常应为 taskclass且一般不需要 packs。",
"- 若第一个可执行 step 是 schedule 查询hook 应为 schedule并优先只带 detail_read。",
"- 若第一个可执行 step 是 schedule 分析hook 应为 schedule并优先带 analyze若分析后立刻要落写再补 mutation。",
"- 若第一个可执行 step 是批量同规则处理hook 应在 schedule 基础上按需加 queue。",
"- hook 只有一份不要求提前覆盖整份计划的所有后续能力execute 可以在后续按计划再切域或补 packs。",
}
return strings.Join(lines, "\n")
}
func planSemanticValue(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return "未标注"
}
return trimmed
}