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永远只适合做选择题、判断题,不适合做开放创新题。
This commit is contained in:
494
backend/newAgent/tools/task_class_write.go
Normal file
494
backend/newAgent/tools/task_class_write.go
Normal file
@@ -0,0 +1,494 @@
|
||||
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-2,2->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)
|
||||
}
|
||||
Reference in New Issue
Block a user