Files
smartmate/backend/newAgent/tools/task_class_write.go
Losita 66c06eed0a Version: 0.9.45.dev.260427
后端:
1. execute 主链路重构为“上下文工具域 + 主动优化候选闭环”——移除 order_guard,粗排后默认进入主动微调,先诊断再从后端候选中选择 move/swap,避免 LLM 自由全局乱搜
2. 工具体系升级为动态注入协议——新增 context_tools_add / remove、工具域与二级包映射、主动优化白名单;schedule / taskclass / web 工具按域按包暴露,msg0 规则包与 execute 上下文同步重写
3. analyze_health 升级为主动优化唯一裁判入口——补齐 rhythm / tightness / profile / feasibility 指标、候选扫描与复诊打分、停滞信号、forced imperfection 判定,并把连续优化状态写回运行态
4. 任务类能力并入新 Agent 执行链——新增 upsert_task_class 写工具与启动注入事务写入;任务类模型补充学科画像与整天屏蔽配置,粗排支持 excluded_days_of_week,steady 策略改为基于目标位置/单日负载/分散度/缓冲的候选打分
5. 运行态与路由补齐优化模式语义——新增 active tool domain/packs、pending context hook、active optimize only、taskclass 写入回盘快照;区分 first_full / global_reopt / local_adjust,并完善首次粗排后默认 refine 的判定

前端:
6. 助手时间线渲染细化——推理内容改为独立 reasoning block,支持与工具/状态/正文按时序交错展示,自动收口折叠,修正 confirm reject 恢复动作

仓库:
7. newAgent 文档整体迁入 docs/backend,补充主动优化执行规划与顺序约束拆解文档,删除旧调试日志文件

PS:这次科研了2天,总算是有些进展了——LLM永远只适合做选择题、判断题,不适合做开放创新题。
2026-04-27 01:09:37 +08:00

495 lines
15 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 (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/LoveLosita/smartflow/backend/model"
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
)
// TaskClassUpsertInput 描述任务类写库工具的标准化入参。
//
// 职责边界:
// 1. ID=0 表示创建ID>0 表示更新;
// 2. Request 直接复用现有 UserAddTaskClassRequest 语义,避免多套字段定义漂移;
// 3. Source 用于记录字段来源chat/memory/web不参与业务校验。
type TaskClassUpsertInput struct {
ID int
Request model.UserAddTaskClassRequest
Source string
}
// TaskClassUpsertPersistResult 描述任务类写入持久层后的结果。
type TaskClassUpsertPersistResult struct {
TaskClassID int
Created bool
}
// TaskClassWriteDeps 描述任务类写库工具依赖。
//
// 职责边界:
// 1. 工具层只负责参数标准化与结果包装,不直接依赖 DAO
// 2. UpsertTaskClass 由启动层注入,便于后续替换为 service/DAO 统一实现。
type TaskClassWriteDeps struct {
UpsertTaskClass func(userID int, input TaskClassUpsertInput) (TaskClassUpsertPersistResult, error)
}
type taskClassValidationResult struct {
OK bool `json:"ok"`
Issues []string `json:"issues"`
}
type taskClassUpsertToolResult struct {
Tool string `json:"tool"`
Success bool `json:"success"`
TaskClassID int `json:"task_class_id,omitempty"`
Created bool `json:"created,omitempty"`
Validation taskClassValidationResult `json:"validation"`
Error string `json:"error"`
ErrorCode string `json:"error_code"`
}
// NewTaskClassUpsertToolHandler 创建 upsert_task_class 工具 handler。
//
// 职责边界:
// 1. 只做参数解析、合法性校验、调用依赖、返回统一 JSON
// 2. 不负责草案生成,草案由 prompt+LLM 完成;
// 3. 不依赖 ScheduleState可在纯聊天场景调用execute 会注入 _user_id
func NewTaskClassUpsertToolHandler(deps TaskClassWriteDeps) ToolHandler {
return func(state *schedule.ScheduleState, args map[string]any) string {
_ = state
if deps.UpsertTaskClass == nil {
return marshalTaskClassUpsertResult(taskClassUpsertToolResult{
Tool: "upsert_task_class",
Success: false,
Validation: taskClassValidationResult{OK: false, Issues: []string{"任务类写库依赖未注入"}},
Error: "任务类写库依赖未注入",
ErrorCode: "dependency_missing",
})
}
userID, ok := readUpsertUserID(args["_user_id"])
if !ok || userID <= 0 {
return marshalTaskClassUpsertResult(taskClassUpsertToolResult{
Tool: "upsert_task_class",
Success: false,
Validation: taskClassValidationResult{OK: false, Issues: []string{"无法识别用户身份"}},
Error: "工具调用失败:无法识别用户身份",
ErrorCode: "missing_user_id",
})
}
input, parseErr := parseTaskClassUpsertInput(args)
if parseErr != nil {
return marshalTaskClassUpsertResult(taskClassUpsertToolResult{
Tool: "upsert_task_class",
Success: false,
Validation: taskClassValidationResult{OK: false, Issues: []string{parseErr.Error()}},
Error: parseErr.Error(),
ErrorCode: "invalid_args",
})
}
issues := validateTaskClassUpsertRequest(input.Request, input.ID)
if len(issues) > 0 {
return marshalTaskClassUpsertResult(taskClassUpsertToolResult{
Tool: "upsert_task_class",
Success: false,
Validation: taskClassValidationResult{OK: false, Issues: issues},
Error: strings.Join(issues, ""),
ErrorCode: "validation_failed",
})
}
result, err := deps.UpsertTaskClass(userID, input)
if err != nil {
return marshalTaskClassUpsertResult(taskClassUpsertToolResult{
Tool: "upsert_task_class",
Success: false,
Validation: taskClassValidationResult{OK: false, Issues: []string{"持久化写入失败"}},
Error: err.Error(),
ErrorCode: "persist_failed",
})
}
if result.TaskClassID <= 0 {
return marshalTaskClassUpsertResult(taskClassUpsertToolResult{
Tool: "upsert_task_class",
Success: false,
Validation: taskClassValidationResult{OK: false, Issues: []string{"未返回有效 task_class_id"}},
Error: "写入后未返回有效 task_class_id",
ErrorCode: "invalid_persist_result",
})
}
return marshalTaskClassUpsertResult(taskClassUpsertToolResult{
Tool: "upsert_task_class",
Success: true,
TaskClassID: result.TaskClassID,
Created: result.Created,
Validation: taskClassValidationResult{OK: true, Issues: []string{}},
Error: "",
ErrorCode: "",
})
}
}
func parseTaskClassUpsertInput(args map[string]any) (TaskClassUpsertInput, error) {
id := 0
if rawID, exists := args["id"]; exists {
parsedID, ok := readUpsertInt(rawID)
if !ok {
return TaskClassUpsertInput{}, fmt.Errorf("id 参数类型非法,必须为整数")
}
id = parsedID
}
taskClassRaw, ok := args["task_class"]
if !ok {
return TaskClassUpsertInput{}, fmt.Errorf("缺少必填参数 task_class")
}
taskClassMap, ok := taskClassRaw.(map[string]any)
if !ok {
return TaskClassUpsertInput{}, fmt.Errorf("task_class 参数类型非法,必须是对象")
}
// 允许顶层 items 覆盖 task_class.items便于 LLM 在生成参数时拆分表达。
if rawItems, exists := args["items"]; exists {
taskClassMap["items"] = rawItems
}
normalizeTaskClassPayload(taskClassMap)
rawJSON, err := json.Marshal(taskClassMap)
if err != nil {
return TaskClassUpsertInput{}, fmt.Errorf("task_class 参数序列化失败")
}
var request model.UserAddTaskClassRequest
if err := json.Unmarshal(rawJSON, &request); err != nil {
return TaskClassUpsertInput{}, fmt.Errorf("task_class 参数解析失败:%v", err)
}
source := ""
if rawSource, exists := args["source"]; exists {
if text, ok := rawSource.(string); ok {
source = strings.TrimSpace(text)
}
}
normalizeTaskClassSemanticRequest(&request)
return TaskClassUpsertInput{
ID: id,
Request: request,
Source: source,
}, nil
}
// normalizeTaskClassPayload 对 LLM 常见“近义字段/错层字段”做轻量兼容归一化。
//
// 职责边界:
// 1. 负责把已知等价字段映射到后端真实契约,减少“明明填了却校验失败”的误伤;
// 2. 不负责兜底补齐业务必填项(如 mode/config这些仍由校验层决定是否报错
// 3. 仅处理本工具已观测到的高频偏差,避免过度“自动纠错”掩盖真实输入问题。
func normalizeTaskClassPayload(taskClassMap map[string]any) {
if len(taskClassMap) == 0 {
return
}
// 1. 兼容日期字段错层:
// 1.1 若顶层 start_date/end_date 缺失;
// 1.2 且 config.start_date/config.end_date 有值;
// 1.3 则抬升到顶层,匹配 UserAddTaskClassRequest 契约。
configMap, _ := readAnyMap(taskClassMap["config"])
promoteStringField(taskClassMap, configMap, "start_date")
promoteStringField(taskClassMap, configMap, "end_date")
// 2. 兼容 items 的语义字段:
// 2.1 content 缺失时,尝试从 description/title/name 回填;
// 2.2 order 缺失或非法时,按当前顺序补 1..N
// 2.3 失败时不抛错,留给校验层输出明确问题。
normalizeTaskClassItems(taskClassMap)
}
func promoteStringField(top map[string]any, config map[string]any, key string) {
if top == nil {
return
}
if strings.TrimSpace(readAnyString(top[key])) != "" {
return
}
if strings.TrimSpace(readAnyString(config[key])) == "" {
return
}
top[key] = strings.TrimSpace(readAnyString(config[key]))
}
func normalizeTaskClassItems(taskClassMap map[string]any) {
rawItems, exists := taskClassMap["items"]
if !exists {
return
}
itemList, ok := rawItems.([]any)
if !ok {
return
}
for idx := range itemList {
itemMap, ok := itemList[idx].(map[string]any)
if !ok {
continue
}
if !hasPositiveInt(itemMap["order"]) {
itemMap["order"] = idx + 1
}
if strings.TrimSpace(readAnyString(itemMap["content"])) == "" {
content := firstNonEmptyString(
readAnyString(itemMap["content"]),
readAnyString(itemMap["description"]),
readAnyString(itemMap["title"]),
readAnyString(itemMap["name"]),
)
if strings.TrimSpace(content) != "" {
itemMap["content"] = strings.TrimSpace(content)
}
}
itemList[idx] = itemMap
}
taskClassMap["items"] = itemList
}
func readAnyMap(raw any) (map[string]any, bool) {
if raw == nil {
return nil, false
}
value, ok := raw.(map[string]any)
return value, ok
}
func readAnyString(raw any) string {
switch value := raw.(type) {
case string:
return value
default:
return ""
}
}
// normalizeTaskClassSemanticRequest 归一化任务类语义画像字段。
//
// 职责边界:
// 1. 负责把 LLM 或用户可能给出的中文/近义值收口成稳定枚举;
// 2. 不负责补默认值,字段缺失仍由上层决定是否接受;
// 3. 归一化失败时保留原值,交给校验层输出明确错误。
func normalizeTaskClassSemanticRequest(req *model.UserAddTaskClassRequest) {
if req == nil {
return
}
if normalized := normalizeSubjectType(req.SubjectType); normalized != "" {
req.SubjectType = normalized
}
if normalized := normalizeLevelValue(req.DifficultyLevel); normalized != "" {
req.DifficultyLevel = normalized
}
if normalized := normalizeLevelValue(req.CognitiveIntensity); normalized != "" {
req.CognitiveIntensity = normalized
}
}
func normalizeSubjectType(raw string) string {
value := strings.TrimSpace(strings.ToLower(raw))
switch value {
case "quantitative", "计算型", "计算", "理工", "理工型":
return "quantitative"
case "memory", "记忆型", "记忆", "背诵型", "背诵":
return "memory"
case "reading", "阅读型", "阅读":
return "reading"
case "mixed", "混合型", "混合":
return "mixed"
default:
return ""
}
}
func normalizeLevelValue(raw string) string {
value := strings.TrimSpace(strings.ToLower(raw))
switch value {
case "low", "低":
return "low"
case "medium", "中", "中等":
return "medium"
case "high", "高":
return "high"
default:
return ""
}
}
func firstNonEmptyString(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}
func hasPositiveInt(raw any) bool {
switch value := raw.(type) {
case int:
return value > 0
case int8:
return value > 0
case int16:
return value > 0
case int32:
return value > 0
case int64:
return value > 0
case float32:
return int(value) > 0
case float64:
return int(value) > 0
default:
return false
}
}
func validateTaskClassUpsertRequest(req model.UserAddTaskClassRequest, id int) []string {
issues := make([]string, 0)
if id < 0 {
issues = append(issues, "id 不能小于 0")
}
if strings.TrimSpace(req.Name) == "" {
issues = append(issues, "name 不能为空")
}
mode := strings.TrimSpace(strings.ToLower(req.Mode))
if mode != "auto" && mode != "manual" {
issues = append(issues, "mode 仅支持 auto/manual")
}
if mode == "auto" {
if strings.TrimSpace(req.StartDate) == "" || strings.TrimSpace(req.EndDate) == "" {
issues = append(issues, "auto 模式必须提供 start_date/end_date")
} else {
startDate, err1 := time.ParseInLocation("2006-01-02", strings.TrimSpace(req.StartDate), time.Local)
endDate, err2 := time.ParseInLocation("2006-01-02", strings.TrimSpace(req.EndDate), time.Local)
if err1 != nil || err2 != nil {
issues = append(issues, "start_date/end_date 日期格式非法,需为 YYYY-MM-DD")
} else if startDate.After(endDate) {
issues = append(issues, "start_date 不能晚于 end_date")
}
}
}
if strings.TrimSpace(req.SubjectType) != "" && normalizeSubjectType(req.SubjectType) == "" {
issues = append(issues, "subject_type 仅支持 quantitative/memory/reading/mixed")
}
if strings.TrimSpace(req.DifficultyLevel) != "" && normalizeLevelValue(req.DifficultyLevel) == "" {
issues = append(issues, "difficulty_level 仅支持 low/medium/high")
}
if strings.TrimSpace(req.CognitiveIntensity) != "" && normalizeLevelValue(req.CognitiveIntensity) == "" {
issues = append(issues, "cognitive_intensity 仅支持 low/medium/high")
}
if strings.TrimSpace(req.SubjectType) == "" {
issues = append(issues, "subject_type 不能为空")
}
if strings.TrimSpace(req.DifficultyLevel) == "" {
issues = append(issues, "difficulty_level 不能为空")
}
if strings.TrimSpace(req.CognitiveIntensity) == "" {
issues = append(issues, "cognitive_intensity 不能为空")
}
if req.Config.TotalSlots <= 0 {
issues = append(issues, "config.total_slots 必须大于 0")
}
strategy := strings.TrimSpace(strings.ToLower(req.Config.Strategy))
if strategy != "steady" && strategy != "rapid" {
issues = append(issues, "config.strategy 仅支持 steady/rapid")
}
for _, section := range req.Config.ExcludedSlots {
// 1. excluded_slots 在粗排算法中按“半天块索引”解释,而不是原子节次;
// 2. 每个块固定映射 2 节1->1-22->3-4...6->11-12
// 3. 若放行 7~12会在 buildTimeGrid 扩展时生成 13~24 节,触发数组越界。
if section < 1 || section > 6 {
issues = append(issues, "config.excluded_slots 仅允许 1~6半天块索引每块=2节")
break
}
}
for _, dayOfWeek := range req.Config.ExcludedDaysOfWeek {
// 1. excluded_days_of_week 属于“整天不可排”的硬约束;
// 2. 仅允许 1~7对应周一到周日
// 3. 非法值会导致粗排过滤口径失真,因此统一在写入口拦截。
if dayOfWeek < 1 || dayOfWeek > 7 {
issues = append(issues, "config.excluded_days_of_week 仅允许 1~7周一到周日")
break
}
}
if len(req.Items) == 0 {
issues = append(issues, "items 不能为空")
}
for index, item := range req.Items {
if item.Order <= 0 {
issues = append(issues, fmt.Sprintf("items[%d].order 必须大于 0", index))
}
if strings.TrimSpace(item.Content) == "" {
issues = append(issues, fmt.Sprintf("items[%d].content 不能为空", index))
}
}
return uniqueTaskClassIssues(issues)
}
func uniqueTaskClassIssues(issues []string) []string {
if len(issues) == 0 {
return issues
}
seen := make(map[string]struct{}, len(issues))
out := make([]string, 0, len(issues))
for _, issue := range issues {
trimmed := strings.TrimSpace(issue)
if trimmed == "" {
continue
}
if _, ok := seen[trimmed]; ok {
continue
}
seen[trimmed] = struct{}{}
out = append(out, trimmed)
}
return out
}
func readUpsertUserID(raw any) (int, bool) {
return readUpsertInt(raw)
}
func readUpsertInt(raw any) (int, bool) {
switch value := raw.(type) {
case int:
return value, true
case int8:
return int(value), true
case int16:
return int(value), true
case int32:
return int(value), true
case int64:
return int(value), true
case float64:
return int(value), true
case float32:
return int(value), true
default:
return 0, false
}
}
func marshalTaskClassUpsertResult(result taskClassUpsertToolResult) string {
raw, err := json.Marshal(result)
if err != nil {
return `{"tool":"upsert_task_class","success":false,"error":"result encode failed","error_code":"encode_failed"}`
}
return string(raw)
}