Files
smartmate/backend/newAgent/tools/read_helpers.go
Losita 21b864390b Version: 0.9.9.dev.260408
后端:
1. 粗排后分流与顺序守卫落地,支持“无明确微调偏好时粗排后直接收口”,并新增 allow_reorder / needs_refine_after_rough_build 语义,打通 chat→rough_build→execute/order_guard→deliver 路由。
2. execute 工具执行链路修复:清理乱码坏块与重复分支;新增 min_context_switch 未授权拦截;补齐 suggested 顺序基线初始化与顺序守卫联动。
3. 新增复合写工具 min_context_switch(减少上下文切换)并接入注册、参数解析、写工具白名单、提示词与文档;仅在用户明确允许打乱顺序时可用。
4. 工具口径升级:find_first_free 支持 day/day_start/day_end 范围参数并统一文案;移除 find_free 兼容别名;读写工具输出统一到“第N天(星期X)”格式。
5. prompt 同步升级:chat/execute/execute_context 增加粗排后是否继续微调、顺序授权、min_context_switch 使用边界与返回示例约束。
6. handoff 文档重命名并重写下班交接重点:下一步聚焦“工具收敛能力研究 + 运行态必要参数重置(不丢运行态)”。
7. 同步更新调试日志文件。
前端:无
仓库:无
2026-04-08 23:55:09 +08:00

250 lines
7.0 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 newagenttools
import (
"fmt"
"sort"
"strings"
)
// ==================== 内部辅助类型 ====================
// taskOnDay 表示某个任务在某一天的一个时段占用。
// 一个任务可能出现在多天每天可能有多段占用如周一1-2节 + 周三3-4节
type taskOnDay struct {
task *ScheduleTask
slotStart int
slotEnd int
}
// freeRange 表示一段连续空闲区间。
type freeRange struct {
day int
slotStart int
slotEnd int
}
// ==================== 格式化辅助函数 ====================
// formatSlotRange 将时段范围格式化为人类可读的字符串。
// start == end 时输出 "3节",否则输出 "1-2节"。
func formatSlotRange(start, end int) string {
if start == end {
return fmt.Sprintf("%d节", start)
}
return fmt.Sprintf("%d-%d节", start, end)
}
// formatTaskLabel 输出任务的简短标签,如 "[1]高等数学"。
// LLM 交互时统一使用此格式引用任务。
func formatTaskLabel(task ScheduleTask) string {
return fmt.Sprintf("[%d]%s", task.StateID, task.Name)
}
// formatTaskLabelWithCategory 输出带类别和锁定标记的标签。
// 如 "[1]高等数学(课程,固定)" 或 "[2]英语(课程)"。
// 用于 get_overview 和 list_tasks 的概要输出。
func formatTaskLabelWithCategory(task ScheduleTask) string {
label := fmt.Sprintf("[%d]%s(%s", task.StateID, task.Name, task.Category)
if task.Locked {
label += ",固定"
}
label += ")"
return label
}
// ==================== 占用计算辅助函数 ====================
// getTasksOnDay 获取某天所有“当前有落位”的任务占用列表。
//
// 说明:
// 1. existing 与 suggested 都属于“有落位”;
// 2. 旧快照里若残留 pending+Slots也会通过 Slots 被兼容识别;
// 3. 嵌入任务(有 EmbedHost 的)也会被返回,因为它们实际共享了该时段。
// 返回值按 slotStart 升序排列。
func getTasksOnDay(state *ScheduleState, day int) []taskOnDay {
var result []taskOnDay
for i := range state.Tasks {
t := &state.Tasks[i]
if !hasSlotOnDay(t, day) {
continue
}
for _, slot := range t.Slots {
if slot.Day == day {
result = append(result, taskOnDay{
task: t,
slotStart: slot.SlotStart,
slotEnd: slot.SlotEnd,
})
}
}
}
// 按 slotStart 升序排列,方便逐段输出。
sort.Slice(result, func(i, j int) bool {
return result[i].slotStart < result[j].slotStart
})
return result
}
// hasSlotOnDay 判断任务是否在某天有时段占用。
func hasSlotOnDay(task *ScheduleTask, day int) bool {
for _, slot := range task.Slots {
if slot.Day == day {
return true
}
}
return false
}
// countDayOccupied 统计某天的已占用时段总数。
// 每个时段slot是独立的节次单位一个 TaskSlot(day=1, start=1, end=2) 占 2 个时段。
// 嵌入任务与宿主共享时段,不重复计算。
func countDayOccupied(state *ScheduleState, day int) int {
occupied := 0
for i := range state.Tasks {
t := &state.Tasks[i]
// 嵌入任务不重复计算占用——它和宿主共享时段。
if t.EmbedHost != nil {
continue
}
for _, slot := range t.Slots {
if slot.Day == day {
occupied += slot.SlotEnd - slot.SlotStart + 1
}
}
}
return occupied
}
// slotOccupiedBy 查询某天某节被哪个任务占用。
// 排除嵌入任务EmbedHost != nil因为嵌入任务与宿主共享时段。
// 返回 nil 表示该节空闲。
func slotOccupiedBy(state *ScheduleState, day, slot int) *ScheduleTask {
for i := range state.Tasks {
t := &state.Tasks[i]
// 嵌入任务不视为独立占用。
if t.EmbedHost != nil {
continue
}
for _, s := range t.Slots {
if s.Day == day && slot >= s.SlotStart && slot <= s.SlotEnd {
return t
}
}
}
return nil
}
// ==================== 空闲区间计算 ====================
// findFreeRangesOnDay 计算某天所有连续空闲区间。
// 算法:
// 1. 构建 12 个时段的占用数组(排除嵌入任务,嵌入任务共享宿主时段)
// 2. 扫描连续空闲段
//
// 返回值按 slotStart 升序排列。
func findFreeRangesOnDay(state *ScheduleState, day int) []freeRange {
// 1. 构建占用数组occupied[slot] = true 表示该节被占用。
occupied := make([]bool, 13) // 下标 1-120 不使用
for i := range state.Tasks {
t := &state.Tasks[i]
// 嵌入任务与宿主共享时段,不算独立占用。
if t.EmbedHost != nil {
continue
}
for _, slot := range t.Slots {
if slot.Day == day {
for s := slot.SlotStart; s <= slot.SlotEnd; s++ {
if s >= 1 && s <= 12 {
occupied[s] = true
}
}
}
}
}
// 2. 扫描连续空闲段。
var ranges []freeRange
start := 0
for s := 1; s <= 12; s++ {
if !occupied[s] {
if start == 0 {
start = s
}
} else {
if start > 0 {
ranges = append(ranges, freeRange{day: day, slotStart: start, slotEnd: s - 1})
start = 0
}
}
}
if start > 0 {
ranges = append(ranges, freeRange{day: day, slotStart: start, slotEnd: 12})
}
return ranges
}
// getEmbeddableTasks 获取所有可嵌入时段的任务列表。
// 条件CanEmbed == true用于 find_first_free 和 get_overview 输出可嵌入位置。
func getEmbeddableTasks(state *ScheduleState) []*ScheduleTask {
var result []*ScheduleTask
for i := range state.Tasks {
t := &state.Tasks[i]
if t.CanEmbed && len(t.Slots) > 0 {
result = append(result, t)
}
}
return result
}
// ==================== 通用输出构建 ====================
// buildOverviewDayLine 构建某天的概况行。
// 格式如第1天占6/12 — [1]高等数学(1-2节) [2]英语(3-4节)
// 空闲天输出如第3天占0/12
func buildOverviewDayLine(state *ScheduleState, day int) string {
occupied := countDayOccupied(state, day)
tasks := getTasksOnDay(state, day)
dayLabel := formatDayLabel(state, day)
var sb strings.Builder
sb.WriteString(fmt.Sprintf("%s占%d/12", dayLabel, occupied))
if len(tasks) > 0 {
sb.WriteString(" — ")
for i, td := range tasks {
if i > 0 {
sb.WriteString(" ")
}
label := formatTaskLabel(*td.task)
// 如果任务可嵌入且宿主未被嵌入,标注"可嵌入"。
suffix := ""
if td.task.CanEmbed && td.task.EmbeddedBy == nil {
suffix = ",可嵌入"
}
sb.WriteString(fmt.Sprintf("%s(%s%s)", label, formatSlotRange(td.slotStart, td.slotEnd), suffix))
}
}
return sb.String()
}
// buildFreeRangeLine 格式化空闲区间行。
// 格式如第3天 第1-6节6时段连续空闲
func buildFreeRangeLine(state *ScheduleState, r freeRange) string {
dur := r.slotEnd - r.slotStart + 1
return fmt.Sprintf("%s第%s%d时段连续空闲", formatDayLabel(state, r.day), formatSlotRange(r.slotStart, r.slotEnd), dur)
}
// formatSourceName 将 source 字段转为用户可读的来源名称。
// "event" → "课程表""task_item" → "任务"。
// 不暴露原始 source 字段值,统一使用中文描述。
func formatSourceName(source string) string {
switch source {
case "event":
return "课程表"
case "task_item":
return "任务"
default:
return source
}
}