Version: 0.7.1.dev.260321
feat(agent): ✨ 重构智能排程分流与双通道交付,补齐周级预算并接入连续微调复用 - 🔀 通用路由升级为 action 分流(chat/quick_note_create/task_query/schedule_plan),路由失败直接返回内部错误,不再回落聊天 - 🧭 智能排程链路重构:统一图编排与节点职责,完善日级/周级调优协作与提示词约束 - 📊 周级预算改为“有效周保底 + 负载加权分配”,避免有效周零预算并提升资源利用率 - ⚙️ 日级并发优化细化:按天拆分 DayGroup 并发执行,低收益天(suggested<=2)跳过,单天失败仅回退该天结果并继续全局 - 🧵 周级并发优化细化:按周并发 worker 执行,单周“单步动作”循环(每轮仅 1 个 Move/Swap 或 done),失败周保留原方案不影响其它周 - 🛰️ 新增排程预览双通道:聊天主链路输出终审文本,结构化 candidate_plans 通过 /api/v1/agent/schedule-preview 拉取 - 🗃️ 增补 Redis 预览缓存读写与清理逻辑,新增对应 API、路由、模型与错误码支持 - ♻️ 接入连续对话微调复用:命中同会话历史预览时复用上轮 HybridEntries,避免每轮重跑粗排 - 🛡️ 增加复用保护:仅当本轮与上轮 task_class_ids 集合一致才复用;不一致回退全量粗排 - 🧰 扩展预览缓存字段(task_class_ids/hybrid_entries/allocated_items),支撑微调承接链路 - 🗺️ 更新 README 5.4 Mermaid(总分流图 + 智能排程流转图)并补充决策文档 - ⚠️ 新增“连续微调复用”链路我尚未完成测试,且文档状态目前较为混乱,待连续对话微调功能真正测试完成后再统一更新
This commit is contained in:
@@ -3,35 +3,50 @@ package scheduleplan
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
)
|
||||
|
||||
// SchedulePlanToolDeps 描述"智能排程工具包"需要的外部依赖。
|
||||
// SchedulePlanToolDeps 描述“智能排程 graph”运行所需的外部业务依赖。
|
||||
//
|
||||
// 设计目标:
|
||||
// 1) 通过函数注入把 agent 包与 service/dao 解耦,避免循环依赖;
|
||||
// 2) 每个函数对应一个可独立 mock 的业务能力;
|
||||
// 3) 后续可按需扩展(如局部修补、任务类自动生成等)。
|
||||
// 职责边界:
|
||||
// 1. 只负责声明“需要哪些能力”,不负责具体实现(实现由 service 层注入)。
|
||||
// 2. 只收口函数签名,不承载业务状态,避免跨请求共享可变数据。
|
||||
// 3. 当前统一采用 task_class_ids 语义,不再依赖单 task_class_id 主路径。
|
||||
type SchedulePlanToolDeps struct {
|
||||
// SmartPlanningRaw 调用粗排算法,同时返回展示结构和已分配的任务项。
|
||||
// 返回值:
|
||||
// - []UserWeekSchedule:展示型结构,供 SSE 阶段推送给前端预览;
|
||||
// - []TaskClassItem:已分配的任务项(EmbeddedTime 已回填),供 ReAct 精排使用。
|
||||
SmartPlanningRaw func(ctx context.Context, userID, taskClassID int) ([]model.UserWeekSchedule, []model.TaskClassItem, error)
|
||||
// SmartPlanningMultiRaw 是可选依赖:
|
||||
// 1) 用于需要单独输出“粗排预览”时复用;
|
||||
// 2) 当前主链路已由 HybridScheduleWithPlanMulti 覆盖,可不注入。
|
||||
SmartPlanningMultiRaw func(ctx context.Context, userID int, taskClassIDs []int) ([]model.UserWeekSchedule, []model.TaskClassItem, error)
|
||||
|
||||
// HybridScheduleWithPlan 构建混合日程(既有日程 + 粗排建议),供 ReAct 精排使用。
|
||||
HybridScheduleWithPlan func(ctx context.Context, userID, taskClassID int) ([]model.HybridScheduleEntry, []model.TaskClassItem, error)
|
||||
// HybridScheduleWithPlanMulti 把“既有日程 + 粗排结果”合并成统一的 HybridScheduleEntry 切片,
|
||||
// 供 daily/weekly ReAct 节点在内存中继续优化。
|
||||
HybridScheduleWithPlanMulti func(ctx context.Context, userID int, taskClassIDs []int) ([]model.HybridScheduleEntry, []model.TaskClassItem, error)
|
||||
|
||||
// ResolvePlanningWindow 根据 task_class_ids 解析“全局排程窗口”的相对周/天边界。
|
||||
//
|
||||
// 返回语义:
|
||||
// 1. startWeek/startDay:窗口起点(含);
|
||||
// 2. endWeek/endDay:窗口终点(含);
|
||||
// 3. error:解析失败(如任务类不存在、日期非法)。
|
||||
//
|
||||
// 用途:
|
||||
// 1. 给周级 Move 工具加硬边界,避免把任务移动到窗口外的天数;
|
||||
// 2. 解决“首尾不足一周”场景下的周内越界问题。
|
||||
ResolvePlanningWindow func(ctx context.Context, userID int, taskClassIDs []int) (startWeek, startDay, endWeek, endDay int, err error)
|
||||
}
|
||||
|
||||
// validate 校验依赖完整性,缺失任意一个都无法完成排程链路。
|
||||
// validate 校验依赖完整性。
|
||||
//
|
||||
// 失败处理:
|
||||
// 1. 任意依赖缺失都直接返回错误,避免 graph 运行到中途才 panic。
|
||||
// 2. 调用方(runSchedulePlanFlow)收到错误后会走回退链路,不影响普通聊天可用性。
|
||||
func (d SchedulePlanToolDeps) validate() error {
|
||||
if d.SmartPlanningRaw == nil {
|
||||
return errors.New("schedule plan tool deps: SmartPlanningRaw is nil")
|
||||
}
|
||||
if d.HybridScheduleWithPlan == nil {
|
||||
return errors.New("schedule plan tool deps: HybridScheduleWithPlan is nil")
|
||||
if d.HybridScheduleWithPlanMulti == nil {
|
||||
return errors.New("schedule plan tool deps: HybridScheduleWithPlanMulti is nil")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -59,3 +74,74 @@ func ExtraInt(extra map[string]any, key string) (int, bool) {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// ExtraIntSlice 从 extra map 中安全提取整数切片。
|
||||
//
|
||||
// 兼容输入:
|
||||
// 1) []any(JSON 数组反序列化后的常见类型);
|
||||
// 2) []int;
|
||||
// 3) []float64;
|
||||
// 4) 逗号分隔字符串(例如 "1,2,3")。
|
||||
//
|
||||
// 返回语义:
|
||||
// 1) ok=true:至少成功解析出一个整数;
|
||||
// 2) ok=false:字段不存在或全部解析失败。
|
||||
func ExtraIntSlice(extra map[string]any, key string) ([]int, bool) {
|
||||
v, exists := extra[key]
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
parseOne := func(raw any) (int, error) {
|
||||
switch n := raw.(type) {
|
||||
case int:
|
||||
return n, nil
|
||||
case float64:
|
||||
return int(n), nil
|
||||
case string:
|
||||
i, err := strconv.Atoi(n)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return i, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported type: %T", raw)
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]int, 0)
|
||||
switch arr := v.(type) {
|
||||
case []int:
|
||||
for _, item := range arr {
|
||||
out = append(out, item)
|
||||
}
|
||||
case []float64:
|
||||
for _, item := range arr {
|
||||
out = append(out, int(item))
|
||||
}
|
||||
case []any:
|
||||
for _, item := range arr {
|
||||
if parsed, err := parseOne(item); err == nil {
|
||||
out = append(out, parsed)
|
||||
}
|
||||
}
|
||||
case string:
|
||||
parts := strings.Split(arr, ",")
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if parsed, err := strconv.Atoi(part); err == nil {
|
||||
out = append(out, parsed)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user