Files
smartmate/backend/newAgent/tools/write_helpers.go
Losita b1eb6bedf9 Version: 0.9.1.dev.260406
后端:
  1.新建conv/schedule_persist.go:ScheduleState Diff 持久化,事务内逐变更写库,支持 place/move/unplace 三种操作(当前
  event source)
  2.新建conv/schedule_provider.go:ScheduleState 加载适配,从 DB 合并 existing events + pending task items
  3.新建dao/agent_state_store_adapter.go:Redis 状态快照存取适配,实现 AgentStateStore 接口
  4.新建service/agentsvc/agent_newagent.go:newAgent service 集成层,串联 LLM
  客户端、ScheduleProvider、SchedulePersistor 和 ChunkEmitter
  5.更新node/execute.go:接入 SchedulePersistor(写操作确认后持久化)、完善 confirm resume 路径(PendingConfirmTool
  恢复分支)、correction 机制增加连续失败计数上限
  6.更新api/agent.go + cmd/start.go:接入 newAgent service,完成 API 层路由注册
  7.新建node/execute_confirm_flow_test.go + llm_tool_orchestration_test.go:确认回路 7 个测试 + 端到端排课 5
  个测试全部通过
  8.新建newAgent/ARCHITECTURE.md + ROADMAP.md:全链路架构文档和缺口分析
  9.代码审查整理:提取 prompt/base.go(通用 buildStageMessages 等5个辅助)、tools/args.go(参数解析辅助);write_tools
  尾部辅助移入 write_helpers;修复 queryRangeSpecific sb.Reset() 逻辑缺陷和 Unplace guest Duration
  未恢复;ScheduleStateProvider/SchedulePersistor 归入 state_store.go;emitter 内部 Build*Text 函数降级为私有
前端:无
仓库:无
2026-04-06 15:33:34 +08:00

224 lines
6.2 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"
)
// ==================== 写工具专用辅助函数 ====================
// ==================== 校验函数 ====================
// validateDay 校验 day 是否在规划窗口范围内。
func validateDay(state *ScheduleState, day int) error {
if day < 1 || day > state.Window.TotalDays {
return fmt.Errorf("第%d天不在规划窗口范围内1-%d", day, state.Window.TotalDays)
}
return nil
}
// validateSlotRange 校验时段范围是否合法1-12start <= end
func validateSlotRange(start, end int) error {
if start < 1 {
return fmt.Errorf("起始时段 %d 不能小于1", start)
}
if end > 12 {
return fmt.Errorf("结束时段 %d 不能大于12", end)
}
if start > end {
return fmt.Errorf("起始时段 %d 不能大于结束时段 %d", start, end)
}
return nil
}
// checkLocked 检查任务是否被锁定。锁定任务不可移动/交换/移除。
func checkLocked(task ScheduleTask) error {
if task.Locked {
return fmt.Errorf("[%d]%s 是固定课程,不可操作", task.StateID, task.Name)
}
return nil
}
// ==================== 冲突检测 ====================
// findConflict 查找指定范围 [start, end] 内是否有冲突。
// 排除 excludeStateIDs 中的任务(用于 move/swap 排除自身旧位置)。
// 可嵌入宿主can_embed=true不算冲突——嵌入场景由 place 单独处理。
// 返回第一个冲突任务,无冲突返回 nil。
func findConflict(state *ScheduleState, day, start, end int, excludeStateIDs ...int) *ScheduleTask {
// 构建排除集合
exclude := make(map[int]bool, len(excludeStateIDs))
for _, id := range excludeStateIDs {
exclude[id] = true
}
for i := range state.Tasks {
t := &state.Tasks[i]
// 排除指定任务
if exclude[t.StateID] {
continue
}
// 可嵌入宿主不算冲突
if t.CanEmbed {
continue
}
// 嵌入任务与宿主共享时段,不算独立冲突
if t.EmbedHost != nil {
continue
}
// 只检查已安排的任务
if len(t.Slots) == 0 {
continue
}
for _, slot := range t.Slots {
if slot.Day == day {
// 检查范围是否有交集:[start,end] ∩ [slot.SlotStart,slot.SlotEnd]
if start <= slot.SlotEnd && end >= slot.SlotStart {
return t
}
}
}
}
return nil
}
// findEmbedHost 查找指定范围 [start, end] 内是否有可嵌入的宿主。
// 条件can_embed=true 且未被嵌入embedded_by == nil
// 返回第一个匹配的宿主,无匹配返回 nil。
func findEmbedHost(state *ScheduleState, day, start, end int) *ScheduleTask {
for i := range state.Tasks {
t := &state.Tasks[i]
if !t.CanEmbed || t.EmbeddedBy != nil {
continue
}
for _, slot := range t.Slots {
if slot.Day == day {
// 完全包含在宿主时段内才能嵌入
if start >= slot.SlotStart && end <= slot.SlotEnd {
return t
}
}
}
}
return nil
}
// ==================== 计算辅助 ====================
// taskDuration 计算任务所有 Slots 的总时段数。
// 如 Slots = [{1,1,2}, {3,1,2}] → 总时长 = 2+2 = 4。
// 用于 swap 时比较两个任务的时长是否一致。
func taskDuration(task ScheduleTask) int {
total := 0
for _, slot := range task.Slots {
total += slot.SlotEnd - slot.SlotStart + 1
}
return total
}
// countPending 统计当前 state 中待安排任务数量。
func countPending(state *ScheduleState) int {
count := 0
for i := range state.Tasks {
if state.Tasks[i].Status == "pending" {
count++
}
}
return count
}
// ==================== 任务时段辅助 ====================
// formatTaskSlotsBrief 将任务的时段列表格式化为简短描述。
// 如 "第1天(1-2节) 第4天(3-4节)"。
func formatTaskSlotsBrief(slots []TaskSlot) string {
parts := make([]string, 0, len(slots))
for _, slot := range slots {
parts = append(parts, fmt.Sprintf("第%d天第%s", slot.Day, formatSlotRange(slot.SlotStart, slot.SlotEnd)))
}
return strings.Join(parts, " ")
}
// collectAffectedDays 从旧位置和新位置中收集所有涉及的天(去重排序)。
func collectAffectedDays(oldSlots, newSlots []TaskSlot) []int {
days := make(map[int]bool)
for _, s := range oldSlots {
days[s.Day] = true
}
for _, s := range newSlots {
days[s.Day] = true
}
return sortedKeys(days)
}
// collectAffectedDaysFromSlots 从单个 slot 列表中收集涉及的天。
func collectAffectedDaysFromSlots(slots []TaskSlot) []int {
days := make(map[int]bool)
for _, s := range slots {
days[s.Day] = true
}
return sortedKeys(days)
}
// sortedKeys 将 map 的 key 排序后返回。
func sortedKeys(m map[int]bool) []int {
keys := make([]int, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Ints(keys)
return keys
}
// uniqueSorted 对 int 切片去重并排序。
func uniqueSorted(s []int) []int {
seen := make(map[int]bool)
result := make([]int, 0, len(s))
for _, v := range s {
if !seen[v] {
seen[v] = true
result = append(result, v)
}
}
sort.Ints(result)
return result
}
// ==================== 输出格式化 ====================
// formatDayOccupancy 格式化某天的占用摘要。
// 如 "第5天当前占用[3]复习线代(1-3节)占用3/12。"
// 如 "第4天当前占用0/12。"(空天)
func formatDayOccupancy(state *ScheduleState, day int) string {
tasks := getTasksOnDay(state, day)
occupied := countDayOccupied(state, day)
if len(tasks) == 0 {
return fmt.Sprintf("第%d天当前占用0/12。", day)
}
parts := make([]string, 0, len(tasks))
for _, td := range tasks {
label := formatTaskLabel(*td.task)
parts = append(parts, fmt.Sprintf("%s(%s)", label, formatSlotRange(td.slotStart, td.slotEnd)))
}
return fmt.Sprintf("第%d天当前占用%s占用%d/12。", day, strings.Join(parts, " "), occupied)
}
// formatFreeHint 格式化某天的空闲时段提示。
// 如 "空闲时段第5-12节。"
// 无空闲时返回空字符串。
func formatFreeHint(state *ScheduleState, day int) string {
ranges := findFreeRangesOnDay(state, day)
if len(ranges) == 0 {
return ""
}
parts := make([]string, 0, len(ranges))
for _, r := range ranges {
parts = append(parts, formatSlotRange(r.slotStart, r.slotEnd))
}
return fmt.Sprintf("空闲时段:%s。", strings.Join(parts, "、"))
}