Version: 0.9.15.dev.260412
后端: 1. 排程工具从 tools/ 根目录拆分为 tools/schedule 独立子包 - 12 个排程工具文件等价迁入 tools/schedule/,tools/ 根目录仅保留 registry.go 作为统一注册入口 - 所有依赖方(conv / model / node / prompt / service)import 统一切到 schedule 子包 2. Web 搜索工具链落地(tools/web 子包) - 新增 web_search(结构化检索)与 web_fetch(正文抓取)两个读工具,支持博查 API / mock 降级 - 启动流程按配置选择 provider,未识别类型自动降级为 mock,不阻断主流程 - 执行提示补齐 web 工具使用约束与返回值示例 - config.example.yaml 补齐 websearch 配置段 前端:无 仓库:无
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/LoveLosita/smartflow/backend/middleware"
|
||||
newagentconv "github.com/LoveLosita/smartflow/backend/newAgent/conv"
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/web"
|
||||
"github.com/LoveLosita/smartflow/backend/pkg"
|
||||
"github.com/LoveLosita/smartflow/backend/routers"
|
||||
"github.com/LoveLosita/smartflow/backend/service"
|
||||
@@ -139,8 +140,33 @@ func Start() {
|
||||
|
||||
// newAgent 依赖接线。
|
||||
agentService.SetAgentStateStore(dao.NewAgentStateStoreAdapter(cacheRepo))
|
||||
|
||||
// 1. WebSearch provider 初始化:根据配置选择 mock/bocha;
|
||||
// 2. provider 为 nil 时,web_search / web_fetch 返回"暂未启用",不阻断主流程。
|
||||
var webSearchProvider web.SearchProvider
|
||||
webProvider := viper.GetString("websearch.provider")
|
||||
switch webProvider {
|
||||
case "bocha":
|
||||
bochaKey := viper.GetString("websearch.apiKey")
|
||||
if bochaKey == "" {
|
||||
log.Println("WebSearch: 博查 API Key 为空,降级为 mock")
|
||||
webSearchProvider = &web.MockProvider{}
|
||||
} else {
|
||||
webSearchProvider = web.NewBochaProvider(bochaKey, "")
|
||||
log.Println("WebSearch provider: bocha")
|
||||
}
|
||||
case "mock", "":
|
||||
webSearchProvider = &web.MockProvider{}
|
||||
log.Println("WebSearch provider: mock(模拟模式)")
|
||||
default:
|
||||
// 未识别的 provider 类型降级为 mock 并输出警告。
|
||||
log.Printf("WebSearch provider %q 未识别,降级为 mock", webProvider)
|
||||
webSearchProvider = &web.MockProvider{}
|
||||
}
|
||||
|
||||
agentService.SetToolRegistry(newagenttools.NewDefaultRegistryWithDeps(newagenttools.DefaultRegistryDeps{
|
||||
RAGRuntime: ragRuntime,
|
||||
RAGRuntime: ragRuntime,
|
||||
WebSearchProvider: webSearchProvider,
|
||||
}))
|
||||
agentService.SetScheduleProvider(newagentconv.NewScheduleProvider(scheduleRepo, taskClassRepo))
|
||||
agentService.SetSchedulePersistor(newagentconv.NewSchedulePersistorAdapter(manager))
|
||||
|
||||
@@ -103,5 +103,10 @@ memory:
|
||||
claimBatch: 1
|
||||
|
||||
websearch:
|
||||
provider: mock # 可选:mock | bocha(mock 为空实现,跑通链路用)
|
||||
apiKey: "" # 搜索供应商 API Key(bocha 模式必填,否则降级为 mock)
|
||||
timeout: 10s # 单次搜索请求超时
|
||||
fetchTimeout: 15s # 单次 URL 抓取超时
|
||||
fetchMaxChars: 4000 # 抓取正文最大字符数
|
||||
rag:
|
||||
enabled: false
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
baseconv "github.com/LoveLosita/smartflow/backend/conv"
|
||||
"github.com/LoveLosita/smartflow/backend/dao"
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
)
|
||||
|
||||
// SchedulePersistorAdapter 实现 model.SchedulePersistor 接口。
|
||||
@@ -22,7 +22,7 @@ func NewSchedulePersistorAdapter(manager *dao.RepoManager) *SchedulePersistorAda
|
||||
}
|
||||
|
||||
// PersistScheduleChanges 实现 model.SchedulePersistor 接口。
|
||||
func (a *SchedulePersistorAdapter) PersistScheduleChanges(ctx context.Context, original, modified *newagenttools.ScheduleState, userID int) error {
|
||||
func (a *SchedulePersistorAdapter) PersistScheduleChanges(ctx context.Context, original, modified *schedule.ScheduleState, userID int) error {
|
||||
return PersistScheduleChanges(ctx, a.manager, original, modified, userID)
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ func (a *SchedulePersistorAdapter) PersistScheduleChanges(ctx context.Context, o
|
||||
func PersistScheduleChanges(
|
||||
ctx context.Context,
|
||||
manager *dao.RepoManager,
|
||||
original *newagenttools.ScheduleState,
|
||||
modified *newagenttools.ScheduleState,
|
||||
original *schedule.ScheduleState,
|
||||
modified *schedule.ScheduleState,
|
||||
userID int,
|
||||
) error {
|
||||
changes := DiffScheduleState(original, modified)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
)
|
||||
|
||||
// ScheduleStateToPreview 将 newAgent 的 ScheduleState 转换为前端预览缓存格式。
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
// 3. Day → (Week, DayOfWeek) 通过 ScheduleState.DayToWeekDay 转换;
|
||||
// 4. 转换失败的 slot(day_index 无效)静默跳过。
|
||||
func ScheduleStateToPreview(
|
||||
state *newagenttools.ScheduleState,
|
||||
state *schedule.ScheduleState,
|
||||
userID int,
|
||||
conversationID string,
|
||||
taskClassIDs []int,
|
||||
@@ -30,7 +30,7 @@ func ScheduleStateToPreview(
|
||||
for i := range state.Tasks {
|
||||
t := &state.Tasks[i]
|
||||
// 待安排且无位置的任务不生成 entry。
|
||||
if newagenttools.IsPendingTask(*t) {
|
||||
if schedule.IsPendingTask(*t) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -116,6 +116,6 @@ func ScheduleStateToPreview(
|
||||
// 1. 新语义下,显式 suggested 直接输出为建议态;
|
||||
// 2. 兼容旧快照:pending+Slots、existing+Duration>0 的 task_item 也继续按 suggested 输出;
|
||||
// 3. 这样前端预览口径可以在迁移期保持稳定,不会因为状态枚举切换而抖动。
|
||||
func shouldMarkSuggestedInPreview(t newagenttools.ScheduleTask) bool {
|
||||
return newagenttools.IsSuggestedTask(t)
|
||||
func shouldMarkSuggestedInPreview(t schedule.ScheduleTask) bool {
|
||||
return schedule.IsSuggestedTask(t)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
baseconv "github.com/LoveLosita/smartflow/backend/conv"
|
||||
"github.com/LoveLosita/smartflow/backend/dao"
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
)
|
||||
|
||||
// ScheduleProvider 实现 model.ScheduleStateProvider 接口。
|
||||
@@ -39,7 +39,7 @@ func NewScheduleProvider(scheduleDAO *dao.ScheduleDAO, taskClassDAO *dao.TaskCla
|
||||
// 2. task class 无日期信息时,降级到当前周 7 天(兼容普通查询场景)。
|
||||
//
|
||||
// 日程加载策略:对窗口内每周分别调用 GetUserWeeklySchedule 并合并结果。
|
||||
func (p *ScheduleProvider) LoadScheduleState(ctx context.Context, userID int) (*newagenttools.ScheduleState, error) {
|
||||
func (p *ScheduleProvider) LoadScheduleState(ctx context.Context, userID int) (*schedule.ScheduleState, error) {
|
||||
// 1. 加载用户所有任务类(含 Items 预加载)。
|
||||
taskClasses, err := p.loadCompleteTaskClasses(ctx, userID)
|
||||
if err != nil {
|
||||
@@ -59,7 +59,7 @@ func (p *ScheduleProvider) LoadScheduleStateForTaskClasses(
|
||||
ctx context.Context,
|
||||
userID int,
|
||||
taskClassIDs []int,
|
||||
) (*newagenttools.ScheduleState, error) {
|
||||
) (*schedule.ScheduleState, error) {
|
||||
if len(taskClassIDs) == 0 {
|
||||
return p.LoadScheduleState(ctx, userID)
|
||||
}
|
||||
@@ -82,7 +82,7 @@ func (p *ScheduleProvider) loadScheduleStateWithTaskClasses(
|
||||
ctx context.Context,
|
||||
userID int,
|
||||
taskClasses []model.TaskClass,
|
||||
) (*newagenttools.ScheduleState, error) {
|
||||
) (*schedule.ScheduleState, error) {
|
||||
// 1. 确定规划窗口:优先使用 task class 日期范围,降级到当前周。
|
||||
windowDays, weeks := buildWindowFromTaskClasses(taskClasses)
|
||||
if len(windowDays) == 0 {
|
||||
@@ -236,7 +236,7 @@ func (p *ScheduleProvider) loadCompleteTaskClassesByIDs(
|
||||
}
|
||||
|
||||
// LoadTaskClassMetas 加载指定任务类的约束元数据(不含 Items、不含日程),供 Plan 阶段提前消费。
|
||||
func (p *ScheduleProvider) LoadTaskClassMetas(ctx context.Context, userID int, taskClassIDs []int) ([]newagenttools.TaskClassMeta, error) {
|
||||
func (p *ScheduleProvider) LoadTaskClassMetas(ctx context.Context, userID int, taskClassIDs []int) ([]schedule.TaskClassMeta, error) {
|
||||
if len(taskClassIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -244,9 +244,9 @@ func (p *ScheduleProvider) LoadTaskClassMetas(ctx context.Context, userID int, t
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载任务类元数据失败: %w", err)
|
||||
}
|
||||
metas := make([]newagenttools.TaskClassMeta, 0, len(complete))
|
||||
metas := make([]schedule.TaskClassMeta, 0, len(complete))
|
||||
for _, tc := range complete {
|
||||
meta := newagenttools.TaskClassMeta{
|
||||
meta := schedule.TaskClassMeta{
|
||||
ID: tc.ID,
|
||||
Name: derefString(tc.Name),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"sort"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
)
|
||||
|
||||
// WindowDay 表示排课窗口中的一天(相对周 + 周几)。
|
||||
@@ -24,20 +24,20 @@ func LoadScheduleState(
|
||||
taskClasses []model.TaskClass,
|
||||
extraItemCategories map[int]string,
|
||||
windowDays []WindowDay,
|
||||
) *newagenttools.ScheduleState {
|
||||
state := &newagenttools.ScheduleState{
|
||||
Window: newagenttools.ScheduleWindow{
|
||||
) *schedule.ScheduleState {
|
||||
state := &schedule.ScheduleState{
|
||||
Window: schedule.ScheduleWindow{
|
||||
TotalDays: len(windowDays),
|
||||
DayMapping: make([]newagenttools.DayMapping, len(windowDays)),
|
||||
DayMapping: make([]schedule.DayMapping, len(windowDays)),
|
||||
},
|
||||
Tasks: make([]newagenttools.ScheduleTask, 0),
|
||||
Tasks: make([]schedule.ScheduleTask, 0),
|
||||
}
|
||||
|
||||
// 1. 构建 day_index 与 (week, day_of_week) 的双向转换基础索引。
|
||||
dayLookup := make(map[[2]int]int, len(windowDays))
|
||||
for i, wd := range windowDays {
|
||||
dayIndex := i + 1
|
||||
state.Window.DayMapping[i] = newagenttools.DayMapping{
|
||||
state.Window.DayMapping[i] = schedule.DayMapping{
|
||||
DayIndex: dayIndex,
|
||||
Week: wd.Week,
|
||||
DayOfWeek: wd.DayOfWeek,
|
||||
@@ -118,7 +118,7 @@ func LoadScheduleState(
|
||||
}
|
||||
|
||||
locked := event.Type == "course" && !event.CanBeEmbedded
|
||||
var slots []newagenttools.TaskSlot
|
||||
var slots []schedule.TaskSlot
|
||||
for _, g := range groups {
|
||||
if len(g.sections) == 0 {
|
||||
continue
|
||||
@@ -131,12 +131,12 @@ func LoadScheduleState(
|
||||
continue
|
||||
}
|
||||
if day, ok := dayLookup[[2]int{g.week, g.dayOfWeek}]; ok {
|
||||
slots = append(slots, newagenttools.TaskSlot{Day: day, SlotStart: start, SlotEnd: end})
|
||||
slots = append(slots, schedule.TaskSlot{Day: day, SlotStart: start, SlotEnd: end})
|
||||
}
|
||||
start, end = sec, sec
|
||||
}
|
||||
if day, ok := dayLookup[[2]int{g.week, g.dayOfWeek}]; ok {
|
||||
slots = append(slots, newagenttools.TaskSlot{Day: day, SlotStart: start, SlotEnd: end})
|
||||
slots = append(slots, schedule.TaskSlot{Day: day, SlotStart: start, SlotEnd: end})
|
||||
}
|
||||
}
|
||||
sort.Slice(slots, func(i, j int) bool {
|
||||
@@ -147,7 +147,7 @@ func LoadScheduleState(
|
||||
})
|
||||
|
||||
stateID := nextStateID
|
||||
state.Tasks = append(state.Tasks, newagenttools.ScheduleTask{
|
||||
state.Tasks = append(state.Tasks, schedule.ScheduleTask{
|
||||
StateID: stateID,
|
||||
Source: "event",
|
||||
SourceID: eventID,
|
||||
@@ -207,12 +207,12 @@ func LoadScheduleState(
|
||||
}
|
||||
|
||||
if hostStateID, ok := itemIDToEmbedHostStateID[item.ID]; ok {
|
||||
hostSlots := []newagenttools.TaskSlot(nil)
|
||||
hostSlots := []schedule.TaskSlot(nil)
|
||||
if hostTask := state.TaskByStateID(hostStateID); hostTask != nil {
|
||||
hostSlots = cloneTaskSlots(hostTask.Slots)
|
||||
}
|
||||
stateID := nextStateID
|
||||
state.Tasks = append(state.Tasks, newagenttools.ScheduleTask{
|
||||
state.Tasks = append(state.Tasks, schedule.ScheduleTask{
|
||||
StateID: stateID,
|
||||
Source: "task_item",
|
||||
SourceID: item.ID,
|
||||
@@ -230,7 +230,7 @@ func LoadScheduleState(
|
||||
|
||||
if slots, ok := slotsFromTargetTime(item.EmbeddedTime, dayLookup); ok {
|
||||
stateID := nextStateID
|
||||
state.Tasks = append(state.Tasks, newagenttools.ScheduleTask{
|
||||
state.Tasks = append(state.Tasks, schedule.ScheduleTask{
|
||||
StateID: stateID,
|
||||
Source: "task_item",
|
||||
SourceID: item.ID,
|
||||
@@ -251,7 +251,7 @@ func LoadScheduleState(
|
||||
}
|
||||
|
||||
stateID := nextStateID
|
||||
state.Tasks = append(state.Tasks, newagenttools.ScheduleTask{
|
||||
state.Tasks = append(state.Tasks, schedule.ScheduleTask{
|
||||
StateID: stateID,
|
||||
Source: "task_item",
|
||||
SourceID: item.ID,
|
||||
@@ -269,7 +269,7 @@ func LoadScheduleState(
|
||||
|
||||
// 仅当该任务类仍有 pending item 时,才把约束暴露给 LLM。
|
||||
if pendingCount > 0 {
|
||||
meta := newagenttools.TaskClassMeta{
|
||||
meta := schedule.TaskClassMeta{
|
||||
ID: tc.ID,
|
||||
Name: catName,
|
||||
}
|
||||
@@ -328,12 +328,12 @@ func LoadScheduleState(
|
||||
if cat, exists := itemCategoryLookup[itemID]; exists && cat != "" {
|
||||
category = cat
|
||||
}
|
||||
hostSlots := []newagenttools.TaskSlot(nil)
|
||||
hostSlots := []schedule.TaskSlot(nil)
|
||||
if hostTask != nil {
|
||||
hostSlots = cloneTaskSlots(hostTask.Slots)
|
||||
}
|
||||
guestStateID = nextStateID
|
||||
state.Tasks = append(state.Tasks, newagenttools.ScheduleTask{
|
||||
state.Tasks = append(state.Tasks, schedule.ScheduleTask{
|
||||
StateID: guestStateID,
|
||||
Source: "task_item",
|
||||
SourceID: itemID,
|
||||
@@ -412,7 +412,7 @@ func taskItemName(item model.TaskClassItem) string {
|
||||
func slotsFromTargetTime(
|
||||
target *model.TargetTime,
|
||||
dayLookup map[[2]int]int,
|
||||
) ([]newagenttools.TaskSlot, bool) {
|
||||
) ([]schedule.TaskSlot, bool) {
|
||||
if target == nil {
|
||||
return nil, false
|
||||
}
|
||||
@@ -423,7 +423,7 @@ func slotsFromTargetTime(
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return []newagenttools.TaskSlot{
|
||||
return []schedule.TaskSlot{
|
||||
{
|
||||
Day: day,
|
||||
SlotStart: target.SectionFrom,
|
||||
@@ -471,8 +471,8 @@ type ScheduleChange struct {
|
||||
|
||||
// DiffScheduleState 比较 original 与 modified,返回需要持久化的变更集合。
|
||||
func DiffScheduleState(
|
||||
original *newagenttools.ScheduleState,
|
||||
modified *newagenttools.ScheduleState,
|
||||
original *schedule.ScheduleState,
|
||||
modified *schedule.ScheduleState,
|
||||
) []ScheduleChange {
|
||||
if original == nil || modified == nil {
|
||||
return nil
|
||||
@@ -534,8 +534,8 @@ func DiffScheduleState(
|
||||
}
|
||||
|
||||
// indexByStateID 将任务列表按 state_id 建立索引。
|
||||
func indexByStateID(state *newagenttools.ScheduleState) map[int]*newagenttools.ScheduleTask {
|
||||
m := make(map[int]*newagenttools.ScheduleTask, len(state.Tasks))
|
||||
func indexByStateID(state *schedule.ScheduleState) map[int]*schedule.ScheduleTask {
|
||||
m := make(map[int]*schedule.ScheduleTask, len(state.Tasks))
|
||||
for i := range state.Tasks {
|
||||
m[state.Tasks[i].StateID] = &state.Tasks[i]
|
||||
}
|
||||
@@ -543,7 +543,7 @@ func indexByStateID(state *newagenttools.ScheduleState) map[int]*newagenttools.S
|
||||
}
|
||||
|
||||
// slotsEqual 判断两个压缩槽位切片是否完全一致。
|
||||
func slotsEqual(a, b []newagenttools.TaskSlot) bool {
|
||||
func slotsEqual(a, b []schedule.TaskSlot) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
@@ -556,18 +556,18 @@ func slotsEqual(a, b []newagenttools.TaskSlot) bool {
|
||||
}
|
||||
|
||||
// cloneTaskSlots 深拷贝槽位切片。
|
||||
func cloneTaskSlots(src []newagenttools.TaskSlot) []newagenttools.TaskSlot {
|
||||
func cloneTaskSlots(src []schedule.TaskSlot) []schedule.TaskSlot {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
dst := make([]newagenttools.TaskSlot, len(src))
|
||||
dst := make([]schedule.TaskSlot, len(src))
|
||||
copy(dst, src)
|
||||
return dst
|
||||
}
|
||||
|
||||
// resolveHostEventID 通过任务的 EmbedHost 反查宿主 event_id。
|
||||
// 非嵌入任务或宿主不存在时返回 0。
|
||||
func resolveHostEventID(task *newagenttools.ScheduleTask, state *newagenttools.ScheduleState) int {
|
||||
func resolveHostEventID(task *schedule.ScheduleTask, state *schedule.ScheduleState) int {
|
||||
if task == nil || task.EmbedHost == nil {
|
||||
return 0
|
||||
}
|
||||
@@ -579,7 +579,7 @@ func resolveHostEventID(task *newagenttools.ScheduleTask, state *newagenttools.S
|
||||
}
|
||||
|
||||
// expandToCoords 将压缩槽位展开成逐节坐标,便于后续持久化层处理。
|
||||
func expandToCoords(slots []newagenttools.TaskSlot, state *newagenttools.ScheduleState) []SlotCoord {
|
||||
func expandToCoords(slots []schedule.TaskSlot, state *schedule.ScheduleState) []SlotCoord {
|
||||
var coords []SlotCoord
|
||||
for _, slot := range slots {
|
||||
week, dow, ok := state.DayToWeekDay(slot.Day)
|
||||
|
||||
@@ -3,7 +3,7 @@ package model
|
||||
import (
|
||||
"strings"
|
||||
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
)
|
||||
|
||||
// Phase 表示 agent 主循环当前所处的大阶段。
|
||||
@@ -92,7 +92,7 @@ type CommonState struct {
|
||||
TaskClassIDs []int `json:"task_class_ids,omitempty"`
|
||||
// TaskClasses 本次排课涉及的任务类约束元数据(含日期、策略、时段预算等),
|
||||
// 在 Service 层从 DB 加载并注入,供 Plan prompt 直接消费,避免 LLM 因信息不足而追问用户。
|
||||
TaskClasses []newagenttools.TaskClassMeta `json:"task_classes,omitempty"`
|
||||
TaskClasses []schedule.TaskClassMeta `json:"task_classes,omitempty"`
|
||||
|
||||
// NeedsRoughBuild 由 Plan 节点在 plan_done 时写入,标记 Confirm 后是否需要走粗排节点。
|
||||
// 粗排节点执行完毕后会将此字段重置为 false。
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
infrallm "github.com/LoveLosita/smartflow/backend/infra/llm"
|
||||
newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream"
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
)
|
||||
|
||||
// AgentGraphRequest 描述一次 agent graph 运行的请求级输入。
|
||||
@@ -48,7 +49,7 @@ type RoughBuildFunc func(ctx context.Context, userID int, taskClassIDs []int) ([
|
||||
// 由 service 层封装 cacheDAO 后注入,execute/deliver 节点可按需调用:
|
||||
// 1. execute 写工具后可实时刷新,保障前端及时看到最新调整;
|
||||
// 2. deliver 结束时再做最终覆盖写,保障收口状态一致。
|
||||
type WriteSchedulePreviewFunc func(ctx context.Context, state *newagenttools.ScheduleState, userID int, conversationID string, taskClassIDs []int) error
|
||||
type WriteSchedulePreviewFunc func(ctx context.Context, state *schedule.ScheduleState, userID int, conversationID string, taskClassIDs []int) error
|
||||
|
||||
// AgentGraphDeps 描述 graph/node 层运行时真正依赖的可插拔能力。
|
||||
//
|
||||
@@ -151,8 +152,8 @@ func (d *AgentGraphDeps) ResolveDeliverClient() *infrallm.Client {
|
||||
type AgentGraphRunInput struct {
|
||||
RuntimeState *AgentRuntimeState
|
||||
ConversationContext *ConversationContext
|
||||
ScheduleState *newagenttools.ScheduleState
|
||||
OriginalScheduleState *newagenttools.ScheduleState
|
||||
ScheduleState *schedule.ScheduleState
|
||||
OriginalScheduleState *schedule.ScheduleState
|
||||
Request AgentGraphRequest
|
||||
Deps AgentGraphDeps
|
||||
}
|
||||
@@ -168,8 +169,8 @@ type AgentGraphState struct {
|
||||
ConversationContext *ConversationContext
|
||||
Request AgentGraphRequest
|
||||
Deps AgentGraphDeps
|
||||
ScheduleState *newagenttools.ScheduleState // 工具操作的内存数据源,Execute 节点按需加载
|
||||
OriginalScheduleState *newagenttools.ScheduleState // 首次加载时的原始快照,供 diff 用
|
||||
ScheduleState *schedule.ScheduleState // 工具操作的内存数据源,Execute 节点按需加载
|
||||
OriginalScheduleState *schedule.ScheduleState // 首次加载时的原始快照,供 diff 用
|
||||
}
|
||||
|
||||
// NewAgentGraphState 把入口参数整理成 graph 内部状态。
|
||||
@@ -239,7 +240,7 @@ func (s *AgentGraphState) ResolveToolRegistry() *newagenttools.ToolRegistry {
|
||||
|
||||
// EnsureScheduleState 确保 ScheduleState 已加载。
|
||||
// 首次调用时通过 ScheduleProvider 从 DB 加载,后续复用内存中的 state。
|
||||
func (s *AgentGraphState) EnsureScheduleState(ctx context.Context) (*newagenttools.ScheduleState, error) {
|
||||
func (s *AgentGraphState) EnsureScheduleState(ctx context.Context) (*schedule.ScheduleState, error) {
|
||||
if s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -251,8 +252,8 @@ func (s *AgentGraphState) EnsureScheduleState(ctx context.Context) (*newagenttoo
|
||||
// 3. 因此这里在“已恢复出 ScheduleState、但缺 original”时补一份克隆兜底。
|
||||
s.OriginalScheduleState = s.ScheduleState.Clone()
|
||||
}
|
||||
newagenttools.FilterScheduleStateForTaskClassScope(s.ScheduleState, flowState.TaskClassIDs)
|
||||
newagenttools.FilterScheduleStateForTaskClassScope(s.OriginalScheduleState, flowState.TaskClassIDs)
|
||||
schedule.FilterScheduleStateForTaskClassScope(s.ScheduleState, flowState.TaskClassIDs)
|
||||
schedule.FilterScheduleStateForTaskClassScope(s.OriginalScheduleState, flowState.TaskClassIDs)
|
||||
return s.ScheduleState, nil
|
||||
}
|
||||
if s.Deps.ScheduleProvider == nil {
|
||||
@@ -260,7 +261,7 @@ func (s *AgentGraphState) EnsureScheduleState(ctx context.Context) (*newagenttoo
|
||||
}
|
||||
userID := flowState.UserID
|
||||
var (
|
||||
state *newagenttools.ScheduleState
|
||||
state *schedule.ScheduleState
|
||||
err error
|
||||
)
|
||||
// 1. 若 provider 支持按 task_class_ids 精确加载,则优先走 scoped 入口。
|
||||
@@ -277,7 +278,7 @@ func (s *AgentGraphState) EnsureScheduleState(ctx context.Context) (*newagenttoo
|
||||
s.ScheduleState = state
|
||||
// 保存原始快照,供后续 diff 使用。
|
||||
s.OriginalScheduleState = state.Clone()
|
||||
newagenttools.FilterScheduleStateForTaskClassScope(s.ScheduleState, flowState.TaskClassIDs)
|
||||
newagenttools.FilterScheduleStateForTaskClassScope(s.OriginalScheduleState, flowState.TaskClassIDs)
|
||||
schedule.FilterScheduleStateForTaskClassScope(s.ScheduleState, flowState.TaskClassIDs)
|
||||
schedule.FilterScheduleStateForTaskClassScope(s.OriginalScheduleState, flowState.TaskClassIDs)
|
||||
return state, nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package model
|
||||
import (
|
||||
"context"
|
||||
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
)
|
||||
|
||||
// AgentStateSnapshot 是需要持久化的 agent 运行态最小快照。
|
||||
@@ -14,10 +14,10 @@ import (
|
||||
// 3. 不保存 Deps(依赖注入,每次由 Service 层重建);
|
||||
// 4. 不保存 ToolSchemas(每次请求由 Service 层重新注入)。
|
||||
type AgentStateSnapshot struct {
|
||||
RuntimeState *AgentRuntimeState `json:"runtime_state"`
|
||||
ConversationContext *ConversationContext `json:"conversation_context"`
|
||||
ScheduleState *newagenttools.ScheduleState `json:"schedule_state,omitempty"`
|
||||
OriginalScheduleState *newagenttools.ScheduleState `json:"original_schedule_state,omitempty"`
|
||||
RuntimeState *AgentRuntimeState `json:"runtime_state"`
|
||||
ConversationContext *ConversationContext `json:"conversation_context"`
|
||||
ScheduleState *schedule.ScheduleState `json:"schedule_state,omitempty"`
|
||||
OriginalScheduleState *schedule.ScheduleState `json:"original_schedule_state,omitempty"`
|
||||
}
|
||||
|
||||
// AgentStateStore 定义 agent 状态持久化的最小接口。
|
||||
@@ -58,9 +58,9 @@ type AgentStateStore interface {
|
||||
// 由 DAO 层或 Service 层实现,注入到 AgentGraphDeps 中。
|
||||
// 使用接口而非具体 DAO 类型,避免 model → dao 的循环依赖。
|
||||
type ScheduleStateProvider interface {
|
||||
LoadScheduleState(ctx context.Context, userID int) (*newagenttools.ScheduleState, error)
|
||||
LoadScheduleState(ctx context.Context, userID int) (*schedule.ScheduleState, error)
|
||||
// LoadTaskClassMetas 只加载指定任务类的约束元数据,供 Plan 节点提前消费。
|
||||
LoadTaskClassMetas(ctx context.Context, userID int, taskClassIDs []int) ([]newagenttools.TaskClassMeta, error)
|
||||
LoadTaskClassMetas(ctx context.Context, userID int, taskClassIDs []int) ([]schedule.TaskClassMeta, error)
|
||||
}
|
||||
|
||||
// ScopedScheduleStateProvider 定义“按本轮任务类范围加载 ScheduleState”的可选增强接口。
|
||||
@@ -70,12 +70,12 @@ type ScheduleStateProvider interface {
|
||||
// 2. 不负责:改变既有 ScheduleStateProvider 的基础能力,老实现仍可只实现 LoadScheduleState;
|
||||
// 3. 兜底策略:若调用方拿到的 provider 不实现该接口,则回退到全量 LoadScheduleState,再走工具层 scope 裁剪。
|
||||
type ScopedScheduleStateProvider interface {
|
||||
LoadScheduleStateForTaskClasses(ctx context.Context, userID int, taskClassIDs []int) (*newagenttools.ScheduleState, error)
|
||||
LoadScheduleStateForTaskClasses(ctx context.Context, userID int, taskClassIDs []int) (*schedule.ScheduleState, error)
|
||||
}
|
||||
|
||||
// SchedulePersistor 定义持久化 ScheduleState 变更的接口。
|
||||
// 由 Service 层或 DAO 层实现,注入到 AgentGraphDeps 中。
|
||||
// 使用接口而非具体 DAO 类型,避免 model → dao 的循环依赖。
|
||||
type SchedulePersistor interface {
|
||||
PersistScheduleChanges(ctx context.Context, original, modified *newagenttools.ScheduleState, userID int) error
|
||||
PersistScheduleChanges(ctx context.Context, original, modified *schedule.ScheduleState, userID int) error
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"log"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
)
|
||||
|
||||
// AgentNodes 是 newAgent 通用图的节点容器。
|
||||
@@ -185,7 +185,7 @@ func (n *AgentNodes) Execute(ctx context.Context, st *newagentmodel.AgentGraphSt
|
||||
}
|
||||
|
||||
// 按需加载 ScheduleState(首次执行时从 DB 加载,后续复用内存中的 state)。
|
||||
var scheduleState *newagenttools.ScheduleState
|
||||
var scheduleState *schedule.ScheduleState
|
||||
if ss, loadErr := st.EnsureScheduleState(ctx); loadErr != nil {
|
||||
return nil, fmt.Errorf("execute node: 加载日程状态失败: %w", loadErr)
|
||||
} else if ss != nil {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
newagentprompt "github.com/LoveLosita/smartflow/backend/newAgent/prompt"
|
||||
newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream"
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@@ -52,10 +53,10 @@ type ExecuteNodeInput struct {
|
||||
ChunkEmitter *newagentstream.ChunkEmitter
|
||||
ResumeNode string
|
||||
ToolRegistry *newagenttools.ToolRegistry
|
||||
ScheduleState *newagenttools.ScheduleState
|
||||
ScheduleState *schedule.ScheduleState
|
||||
SchedulePersistor newagentmodel.SchedulePersistor
|
||||
WriteSchedulePreview newagentmodel.WriteSchedulePreviewFunc
|
||||
OriginalScheduleState *newagenttools.ScheduleState
|
||||
OriginalScheduleState *schedule.ScheduleState
|
||||
AlwaysExecute bool // true 时写工具跳过确认闸门直接执行
|
||||
}
|
||||
|
||||
@@ -127,7 +128,7 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
|
||||
// 1. RoundUsed==0 说明当前还未消耗执行预算;
|
||||
// 2. 此时清理不会影响断线恢复中的中间进度(恢复场景通常 RoundUsed>0)。
|
||||
if input.ScheduleState != nil && flowState.RoundUsed == 0 {
|
||||
newagenttools.ResetTaskProcessingQueue(input.ScheduleState)
|
||||
schedule.ResetTaskProcessingQueue(input.ScheduleState)
|
||||
}
|
||||
if !flowState.AllowReorder && len(flowState.SuggestedOrderBaseline) == 0 {
|
||||
flowState.SuggestedOrderBaseline = buildSuggestedOrderSnapshot(input.ScheduleState)
|
||||
@@ -978,7 +979,7 @@ func renderExecuteStepScope(scope *executeStepScope) string {
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func buildScopeDaySet(state *newagenttools.ScheduleState, scope *executeStepScope) map[int]struct{} {
|
||||
func buildScopeDaySet(state *schedule.ScheduleState, scope *executeStepScope) map[int]struct{} {
|
||||
result := make(map[int]struct{}, 16)
|
||||
if state == nil || scope == nil {
|
||||
return result
|
||||
@@ -991,7 +992,7 @@ func buildScopeDaySet(state *newagenttools.ScheduleState, scope *executeStepScop
|
||||
return result
|
||||
}
|
||||
|
||||
func dayMatchesScope(state *newagenttools.ScheduleState, scope *executeStepScope, day int) bool {
|
||||
func dayMatchesScope(state *schedule.ScheduleState, scope *executeStepScope, day int) bool {
|
||||
if state == nil || scope == nil {
|
||||
return true
|
||||
}
|
||||
@@ -1016,7 +1017,7 @@ func dayMatchesScope(state *newagenttools.ScheduleState, scope *executeStepScope
|
||||
return true
|
||||
}
|
||||
|
||||
func estimateCandidateDaysFromArgs(state *newagenttools.ScheduleState, args map[string]any) (map[int]struct{}, bool, error) {
|
||||
func estimateCandidateDaysFromArgs(state *schedule.ScheduleState, args map[string]any) (map[int]struct{}, bool, error) {
|
||||
result := make(map[int]struct{}, 16)
|
||||
if state == nil {
|
||||
return result, false, fmt.Errorf("日程状态为空")
|
||||
@@ -1328,7 +1329,7 @@ func executeToolCall(
|
||||
toolCall *newagentmodel.ToolCallIntent,
|
||||
emitter *newagentstream.ChunkEmitter,
|
||||
registry *newagenttools.ToolRegistry,
|
||||
scheduleState *newagenttools.ScheduleState,
|
||||
scheduleState *schedule.ScheduleState,
|
||||
writePreview newagentmodel.WriteSchedulePreviewFunc,
|
||||
) error {
|
||||
if toolCall == nil {
|
||||
@@ -1454,9 +1455,9 @@ func executePendingTool(
|
||||
runtimeState *newagentmodel.AgentRuntimeState,
|
||||
conversationContext *newagentmodel.ConversationContext,
|
||||
registry *newagenttools.ToolRegistry,
|
||||
scheduleState *newagenttools.ScheduleState,
|
||||
scheduleState *schedule.ScheduleState,
|
||||
persistor newagentmodel.SchedulePersistor,
|
||||
originalState *newagenttools.ScheduleState,
|
||||
originalState *schedule.ScheduleState,
|
||||
writePreview newagentmodel.WriteSchedulePreviewFunc,
|
||||
emitter *newagentstream.ChunkEmitter,
|
||||
) error {
|
||||
@@ -1539,7 +1540,7 @@ func executePendingTool(
|
||||
func tryWritePreviewAfterWriteTool(
|
||||
ctx context.Context,
|
||||
flowState *newagentmodel.CommonState,
|
||||
scheduleState *newagenttools.ScheduleState,
|
||||
scheduleState *schedule.ScheduleState,
|
||||
registry *newagenttools.ToolRegistry,
|
||||
toolName string,
|
||||
writePreview newagentmodel.WriteSchedulePreviewFunc,
|
||||
@@ -1601,7 +1602,7 @@ func truncateText(text string, maxLen int) string {
|
||||
}
|
||||
|
||||
// summarizeScheduleStateForDebug 返回内存日程状态的关键计数,用于判断工具是否真的修改了 state。
|
||||
func summarizeScheduleStateForDebug(state *newagenttools.ScheduleState) string {
|
||||
func summarizeScheduleStateForDebug(state *schedule.ScheduleState) string {
|
||||
if state == nil {
|
||||
return "state=nil"
|
||||
}
|
||||
@@ -1618,11 +1619,11 @@ func summarizeScheduleStateForDebug(state *newagenttools.ScheduleState) string {
|
||||
hasSlot := len(t.Slots) > 0
|
||||
|
||||
switch {
|
||||
case newagenttools.IsPendingTask(*t):
|
||||
case schedule.IsPendingTask(*t):
|
||||
pendingNoSlot++
|
||||
case newagenttools.IsSuggestedTask(*t):
|
||||
case schedule.IsSuggestedTask(*t):
|
||||
suggestedTotal++
|
||||
case newagenttools.IsExistingTask(*t):
|
||||
case schedule.IsExistingTask(*t):
|
||||
existingTotal++
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,7 +21,7 @@ type suggestedOrderItem struct {
|
||||
Day int
|
||||
SlotStart int
|
||||
SlotEnd int
|
||||
Slots []newagenttools.TaskSlot
|
||||
Slots []schedule.TaskSlot
|
||||
}
|
||||
|
||||
type orderRestoreResult struct {
|
||||
@@ -126,7 +126,7 @@ func RunOrderGuardNode(ctx context.Context, st *newagentmodel.AgentGraphState) e
|
||||
// 1. 这里只关心 suggested 任务,因为顺序守卫目标是约束“本轮建议层”的相对次序;
|
||||
// 2. 多 slot 任务取“最早 slot”作为排序锚点,保证排序键稳定;
|
||||
// 3. 返回值是 state_id 列表,便于写入 CommonState 做跨节点持久化。
|
||||
func buildSuggestedOrderSnapshot(state *newagenttools.ScheduleState) []int {
|
||||
func buildSuggestedOrderSnapshot(state *schedule.ScheduleState) []int {
|
||||
items := buildSuggestedOrderItems(state)
|
||||
order := make([]int, 0, len(items))
|
||||
for _, item := range items {
|
||||
@@ -141,7 +141,7 @@ func buildSuggestedOrderSnapshot(state *newagenttools.ScheduleState) []int {
|
||||
// 1. 统一封装顺序守卫和自动复原都需要的排序素材,避免两处逻辑口径漂移;
|
||||
// 2. 排序键保持与历史实现一致:day -> slot_start -> slot_end -> state_id;
|
||||
// 3. 每项附带完整 slots 快照,供“坑位复用式复原”直接使用。
|
||||
func buildSuggestedOrderItems(state *newagenttools.ScheduleState) []suggestedOrderItem {
|
||||
func buildSuggestedOrderItems(state *schedule.ScheduleState) []suggestedOrderItem {
|
||||
if state == nil || len(state.Tasks) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -149,7 +149,7 @@ func buildSuggestedOrderItems(state *newagenttools.ScheduleState) []suggestedOrd
|
||||
items := make([]suggestedOrderItem, 0, len(state.Tasks))
|
||||
for i := range state.Tasks {
|
||||
task := state.Tasks[i]
|
||||
if !newagenttools.IsSuggestedTask(task) || len(task.Slots) == 0 {
|
||||
if !schedule.IsSuggestedTask(task) || len(task.Slots) == 0 {
|
||||
continue
|
||||
}
|
||||
day, slotStart, slotEnd := earliestTaskSlot(task.Slots)
|
||||
@@ -178,7 +178,7 @@ func buildSuggestedOrderItems(state *newagenttools.ScheduleState) []suggestedOrd
|
||||
return items
|
||||
}
|
||||
|
||||
func earliestTaskSlot(slots []newagenttools.TaskSlot) (day int, slotStart int, slotEnd int) {
|
||||
func earliestTaskSlot(slots []schedule.TaskSlot) (day int, slotStart int, slotEnd int) {
|
||||
if len(slots) == 0 {
|
||||
return 0, 0, 0
|
||||
}
|
||||
@@ -250,7 +250,7 @@ func detectRelativeOrderViolation(baseline []int, current []int) (bool, string)
|
||||
// 2. 复用 current 的“坑位序列”(时段集合),按 baseline 顺序重新回填任务;
|
||||
// 3. 回填前校验时长兼容,避免把长任务塞进短坑位;
|
||||
// 4. 回填后再次校验顺序;若失败则回滚,保证状态不会半成功。
|
||||
func restoreSuggestedOrderByBaseline(state *newagenttools.ScheduleState, baseline []int) orderRestoreResult {
|
||||
func restoreSuggestedOrderByBaseline(state *schedule.ScheduleState, baseline []int) orderRestoreResult {
|
||||
if state == nil {
|
||||
return orderRestoreResult{Restored: false, Detail: "schedule_state=nil"}
|
||||
}
|
||||
@@ -306,7 +306,7 @@ func restoreSuggestedOrderByBaseline(state *newagenttools.ScheduleState, baselin
|
||||
}
|
||||
|
||||
// 1. 先构建“当前坑位序列”。
|
||||
slotPool := make([][]newagenttools.TaskSlot, 0, len(filteredCurrent))
|
||||
slotPool := make([][]schedule.TaskSlot, 0, len(filteredCurrent))
|
||||
for _, currentID := range filteredCurrent {
|
||||
item, ok := itemByID[currentID]
|
||||
if !ok {
|
||||
@@ -343,7 +343,7 @@ func restoreSuggestedOrderByBaseline(state *newagenttools.ScheduleState, baselin
|
||||
}
|
||||
|
||||
// 3. 执行回填,并在失败时支持回滚。
|
||||
beforeSlots := make(map[int][]newagenttools.TaskSlot, len(baselineInScope))
|
||||
beforeSlots := make(map[int][]schedule.TaskSlot, len(baselineInScope))
|
||||
changed := 0
|
||||
for i, targetID := range baselineInScope {
|
||||
task := state.TaskByStateID(targetID)
|
||||
@@ -401,16 +401,16 @@ func sameIDOrder(left, right []int) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func cloneTaskSlots(slots []newagenttools.TaskSlot) []newagenttools.TaskSlot {
|
||||
func cloneTaskSlots(slots []schedule.TaskSlot) []schedule.TaskSlot {
|
||||
if len(slots) == 0 {
|
||||
return nil
|
||||
}
|
||||
copied := make([]newagenttools.TaskSlot, len(slots))
|
||||
copied := make([]schedule.TaskSlot, len(slots))
|
||||
copy(copied, slots)
|
||||
return copied
|
||||
}
|
||||
|
||||
func equalTaskSlots(left, right []newagenttools.TaskSlot) bool {
|
||||
func equalTaskSlots(left, right []schedule.TaskSlot) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
@@ -428,7 +428,7 @@ func equalTaskSlots(left, right []newagenttools.TaskSlot) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func expectedTaskDuration(task newagenttools.ScheduleTask) int {
|
||||
func expectedTaskDuration(task schedule.ScheduleTask) int {
|
||||
if task.Duration > 0 {
|
||||
return task.Duration
|
||||
}
|
||||
@@ -438,7 +438,7 @@ func expectedTaskDuration(task newagenttools.ScheduleTask) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func totalSlotDuration(slots []newagenttools.TaskSlot) int {
|
||||
func totalSlotDuration(slots []schedule.TaskSlot) int {
|
||||
total := 0
|
||||
for _, slot := range slots {
|
||||
total += slot.SlotEnd - slot.SlotStart + 1
|
||||
@@ -446,7 +446,7 @@ func totalSlotDuration(slots []newagenttools.TaskSlot) int {
|
||||
return total
|
||||
}
|
||||
|
||||
func isSlotsCompatibleWithTask(task newagenttools.ScheduleTask, slots []newagenttools.TaskSlot) bool {
|
||||
func isSlotsCompatibleWithTask(task schedule.ScheduleTask, slots []schedule.TaskSlot) bool {
|
||||
if len(slots) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -205,20 +205,20 @@ func RunRoughBuildNode(ctx context.Context, st *newagentmodel.AgentGraphState) e
|
||||
// 1. 第一轮修复后,粗排成功会把任务直接标记为 suggested;
|
||||
// 2. 为兼容旧快照,仍按“pending 且 Slots 为空”认定真正未覆盖;
|
||||
// 3. 只要这里仍大于 0,就应视为粗排异常,而不是交给 LLM 补排。
|
||||
func countPendingTasks(state *newagenttools.ScheduleState, taskClassIDs []int) int {
|
||||
func countPendingTasks(state *schedule.ScheduleState, taskClassIDs []int) int {
|
||||
if state == nil {
|
||||
return 0
|
||||
}
|
||||
count := 0
|
||||
for i := range state.Tasks {
|
||||
task := state.Tasks[i]
|
||||
if !newagenttools.IsPendingTask(task) {
|
||||
if !schedule.IsPendingTask(task) {
|
||||
continue
|
||||
}
|
||||
if len(taskClassIDs) > 0 && !newagenttools.IsTaskInRequestedClassScope(task, taskClassIDs) {
|
||||
if len(taskClassIDs) > 0 && !schedule.IsTaskInRequestedClassScope(task, taskClassIDs) {
|
||||
continue
|
||||
}
|
||||
if newagenttools.IsPendingTask(task) {
|
||||
if schedule.IsPendingTask(task) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
@@ -234,7 +234,7 @@ func countPendingTasks(state *newagenttools.ScheduleState, taskClassIDs []int) i
|
||||
// 4. suggested 表示“粗排建议位”,后续可用 move/swap/unplace 微调;
|
||||
// 5. 转换失败的条目静默跳过,不中断整体流程。
|
||||
func applyRoughBuildPlacements(
|
||||
state *newagenttools.ScheduleState,
|
||||
state *schedule.ScheduleState,
|
||||
placements []newagentmodel.RoughBuildPlacement,
|
||||
) roughBuildApplyStats {
|
||||
stats := roughBuildApplyStats{}
|
||||
@@ -262,10 +262,10 @@ func applyRoughBuildPlacements(
|
||||
matched := false
|
||||
for _, index := range taskIndexByItemID[p.TaskItemID] {
|
||||
t := &state.Tasks[index]
|
||||
t.Slots = []newagenttools.TaskSlot{
|
||||
t.Slots = []schedule.TaskSlot{
|
||||
{Day: day, SlotStart: p.SectionFrom, SlotEnd: p.SectionTo},
|
||||
}
|
||||
t.Status = newagenttools.TaskStatusSuggested
|
||||
t.Status = schedule.TaskStatusSuggested
|
||||
stats.AppliedCount++
|
||||
matched = true
|
||||
break
|
||||
@@ -294,7 +294,7 @@ func appendPlacementSample(samples []string, placement newagentmodel.RoughBuildP
|
||||
}
|
||||
|
||||
// summarizeRoughBuildWindow 提供 DayMapping 的紧凑摘要,便于判断窗口是否退化到错误周。
|
||||
func summarizeRoughBuildWindow(state *newagenttools.ScheduleState) string {
|
||||
func summarizeRoughBuildWindow(state *schedule.ScheduleState) string {
|
||||
if state == nil || len(state.Window.DayMapping) == 0 {
|
||||
return "empty"
|
||||
}
|
||||
@@ -311,7 +311,7 @@ func summarizeRoughBuildWindow(state *newagenttools.ScheduleState) string {
|
||||
}
|
||||
|
||||
// collectScopedTaskSamples 提供当前 state 中可用于匹配的 task_item 样本,便于排查 ID 对不上。
|
||||
func collectScopedTaskSamples(state *newagenttools.ScheduleState, taskClassIDs []int) []string {
|
||||
func collectScopedTaskSamples(state *schedule.ScheduleState, taskClassIDs []int) []string {
|
||||
if state == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -321,7 +321,7 @@ func collectScopedTaskSamples(state *newagenttools.ScheduleState, taskClassIDs [
|
||||
if task.Source != "task_item" {
|
||||
continue
|
||||
}
|
||||
if len(taskClassIDs) > 0 && !newagenttools.IsTaskInRequestedClassScope(task, taskClassIDs) {
|
||||
if len(taskClassIDs) > 0 && !schedule.IsTaskInRequestedClassScope(task, taskClassIDs) {
|
||||
continue
|
||||
}
|
||||
samples = append(samples, fmt.Sprintf(
|
||||
|
||||
@@ -34,6 +34,8 @@ const executeSystemPromptWithPlan = `
|
||||
12. 不要把超过 2 条任务打包到 batch_move;大批量调整请改走队列逐项处理。
|
||||
13. 不要在未获取队首(queue_pop_head)时直接调用 queue_apply_head_move。
|
||||
14. 工具参数必须严格使用 schema 字段,禁止自造别名;例如 day_from/day_to 非法,必须改用 day_start/day_end。
|
||||
15. web_search 仅在“制定学习计划需要查外部资料”时使用(如考试日期、课程信息、校历政策等);日程排布本身(place/move/swap)不需要搜索。
|
||||
16. web_search 拿到 summary 后通常已够用;仅当需要页面详细内容时才调用 web_fetch。
|
||||
|
||||
执行规则:
|
||||
1. 只输出严格 JSON,不要输出 markdown,不要在 JSON 外补充文本。
|
||||
@@ -41,8 +43,8 @@ const executeSystemPromptWithPlan = `
|
||||
3. 写操作:action=confirm + tool_call。
|
||||
4. 缺关键上下文且无法通过工具补齐:action=ask_user。
|
||||
5. 仅当当前步骤完成时输出 action=next_plan,并在 goal_check 对照 done_when 给出证据。
|
||||
6. 仅当整体任务完成时输出 action=done,并在 goal_check 总结完成证据。
|
||||
7. 流程应正式终止时输出 action=abort。`
|
||||
6. 仅当整体任务完成时输出 action=done,并在 goal_check 总结完成证据。
|
||||
7. 流程应正式终止时输出 action=abort。`
|
||||
|
||||
const executeSystemPromptReAct = `
|
||||
你是 SmartFlow NewAgent 的执行器,当前处于自由执行模式(无预定义 plan 步骤)。
|
||||
@@ -75,6 +77,8 @@ const executeSystemPromptReAct = `
|
||||
11. 不要在同一轮构造大规模 batch_move;batch_move 最多 2 条,超过请走队列逐项处理。
|
||||
12. 未调用 queue_pop_head 获取 current 前,不要调用 queue_apply_head_move。
|
||||
13. 工具参数必须严格使用 schema 字段,禁止自造别名;例如 day_from/day_to 非法,必须改用 day_start/day_end。
|
||||
14. web_search 仅在"制定学习计划需要查外部资料"时使用(如考试日期、课程信息、校历政策等);日程排布本身(place/move/swap)不需要搜索。
|
||||
15. web_search 拿到 summary 后通常已够用;仅当需要页面详细内容时才调用 web_fetch。
|
||||
|
||||
执行规则:
|
||||
1. 只输出严格 JSON,不要输出 markdown,不要在 JSON 外补充文本。
|
||||
|
||||
@@ -507,6 +507,10 @@ func renderExecuteToolReturnHint(toolName string) (returnType string, sample str
|
||||
return returnType, "最少上下文切换重排完成:共处理 6 个任务,上下文切换次数 5 -> 2。"
|
||||
case "unplace":
|
||||
return returnType, "已将 [35]... 移除,恢复为待安排状态。"
|
||||
case "web_search":
|
||||
return "string(JSON字符串)", `{"tool":"web_search","query":"检索关键词","count":2,"items":[{"title":"搜索结果标题","url":"https://example.com/page","snippet":"摘要片段...","domain":"example.com","published_at":"2025-04-10"}]}`
|
||||
case "web_fetch":
|
||||
return "string(JSON字符串)", `{"tool":"web_fetch","url":"https://example.com/page","title":"页面标题","content":"正文内容...","truncated":false}`
|
||||
default:
|
||||
return returnType, "自然语言结果(成功/失败原因/关键数据摘要)。"
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import (
|
||||
"strings"
|
||||
|
||||
infrarag "github.com/LoveLosita/smartflow/backend/infra/rag"
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/web"
|
||||
)
|
||||
|
||||
// ToolHandler 是所有工具的统一执行签名。
|
||||
type ToolHandler func(state *ScheduleState, args map[string]any) string
|
||||
type ToolHandler func(state *schedule.ScheduleState, args map[string]any) string
|
||||
|
||||
// ToolSchemaEntry 是注入给模型的工具说明快照。
|
||||
type ToolSchemaEntry struct {
|
||||
@@ -26,6 +28,9 @@ type ToolSchemaEntry struct {
|
||||
// 3. 后续新增读工具时,应优先在这里扩展依赖而不是走包级全局变量。
|
||||
type DefaultRegistryDeps struct {
|
||||
RAGRuntime infrarag.Runtime
|
||||
|
||||
// WebSearchProvider Web 搜索供应商。为 nil 时 web_search / web_fetch 返回"暂未启用",不阻断主流程。
|
||||
WebSearchProvider web.SearchProvider
|
||||
}
|
||||
|
||||
// ToolRegistry 管理工具注册、查找与执行。
|
||||
@@ -60,7 +65,7 @@ func (r *ToolRegistry) Register(name, desc, schemaText string, handler ToolHandl
|
||||
}
|
||||
|
||||
// Execute 执行指定工具。
|
||||
func (r *ToolRegistry) Execute(state *ScheduleState, toolName string, args map[string]any) string {
|
||||
func (r *ToolRegistry) Execute(state *schedule.ScheduleState, toolName string, args map[string]any) string {
|
||||
handler, ok := r.handlers[toolName]
|
||||
if !ok {
|
||||
return fmt.Sprintf("工具调用失败:未知工具 %q。可用工具:%s", toolName, strings.Join(r.ToolNames(), "、"))
|
||||
@@ -123,72 +128,72 @@ func NewDefaultRegistryWithDeps(deps DefaultRegistryDeps) *ToolRegistry {
|
||||
r.Register("get_overview",
|
||||
"获取规划窗口总览(任务视角,全量返回):保留课程占位统计,展开任务清单(过滤课程明细)。",
|
||||
`{"name":"get_overview","parameters":{}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
return GetOverview(state)
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
return schedule.GetOverview(state)
|
||||
},
|
||||
)
|
||||
|
||||
r.Register("query_range",
|
||||
"查看某天或某时段的细粒度占用详情。day 必填,slot_start/slot_end 选填(不填查整天)。",
|
||||
`{"name":"query_range","parameters":{"day":{"type":"int","required":true},"slot_start":{"type":"int"},"slot_end":{"type":"int"}}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
day, ok := argsInt(args, "day")
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
day, ok := schedule.ArgsInt(args, "day")
|
||||
if !ok {
|
||||
return "查询失败:缺少必填参数 day。"
|
||||
}
|
||||
return QueryRange(state, day, argsIntPtr(args, "slot_start"), argsIntPtr(args, "slot_end"))
|
||||
return schedule.QueryRange(state, day, schedule.ArgsIntPtr(args, "slot_start"), schedule.ArgsIntPtr(args, "slot_end"))
|
||||
},
|
||||
)
|
||||
|
||||
r.Register("query_available_slots",
|
||||
"查询候选空位池(先返回纯空位,不足再补可嵌入位),适合 move 前的落点筛选。",
|
||||
`{"name":"query_available_slots","parameters":{"span":{"type":"int"},"duration":{"type":"int"},"limit":{"type":"int"},"allow_embed":{"type":"bool"},"day":{"type":"int"},"day_start":{"type":"int"},"day_end":{"type":"int"},"day_scope":{"type":"string","enum":["all","workday","weekend"]},"day_of_week":{"type":"array","items":{"type":"int"}},"week":{"type":"int"},"week_filter":{"type":"array","items":{"type":"int"}},"week_from":{"type":"int"},"week_to":{"type":"int"},"slot_type":{"type":"string"},"slot_types":{"type":"array","items":{"type":"string"}},"exclude_sections":{"type":"array","items":{"type":"int"}},"after_section":{"type":"int"},"before_section":{"type":"int"},"section_from":{"type":"int"},"section_to":{"type":"int"}}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
return QueryAvailableSlots(state, args)
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
return schedule.QueryAvailableSlots(state, args)
|
||||
},
|
||||
)
|
||||
|
||||
r.Register("query_target_tasks",
|
||||
"查询候选任务集合,可按 status/week/day/task_id/category 筛选;默认自动入队,供后续 queue_pop_head 逐项处理。",
|
||||
`{"name":"query_target_tasks","parameters":{"status":{"type":"string","enum":["all","existing","suggested","pending"]},"category":{"type":"string"},"limit":{"type":"int"},"day_scope":{"type":"string","enum":["all","workday","weekend"]},"day":{"type":"int"},"day_start":{"type":"int"},"day_end":{"type":"int"},"day_of_week":{"type":"array","items":{"type":"int"}},"week":{"type":"int"},"week_filter":{"type":"array","items":{"type":"int"}},"week_from":{"type":"int"},"week_to":{"type":"int"},"task_ids":{"type":"array","items":{"type":"int"}},"task_id":{"type":"int"},"task_item_ids":{"type":"array","items":{"type":"int"}},"task_item_id":{"type":"int"},"enqueue":{"type":"bool"},"reset_queue":{"type":"bool"}}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
return QueryTargetTasks(state, args)
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
return schedule.QueryTargetTasks(state, args)
|
||||
},
|
||||
)
|
||||
|
||||
r.Register("queue_pop_head",
|
||||
"弹出并返回当前队首任务;若已有 current 则复用,保证一次只处理一个任务。",
|
||||
`{"name":"queue_pop_head","parameters":{}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
return QueuePopHead(state, args)
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
return schedule.QueuePopHead(state, args)
|
||||
},
|
||||
)
|
||||
|
||||
r.Register("queue_status",
|
||||
"查看当前待处理队列状态(pending/current/completed/skipped)。",
|
||||
`{"name":"queue_status","parameters":{}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
return QueueStatus(state, args)
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
return schedule.QueueStatus(state, args)
|
||||
},
|
||||
)
|
||||
|
||||
r.Register("list_tasks",
|
||||
"列出任务清单,可按类别和状态过滤。category 传任务类名称,status 仅支持单值 all/existing/suggested/pending。",
|
||||
`{"name":"list_tasks","parameters":{"category":{"type":"string"},"status":{"type":"string","enum":["all","existing","suggested","pending"]}}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
return ListTasks(state, argsStringPtr(args, "category"), argsStringPtr(args, "status"))
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
return schedule.ListTasks(state, schedule.ArgsStringPtr(args, "category"), schedule.ArgsStringPtr(args, "status"))
|
||||
},
|
||||
)
|
||||
|
||||
r.Register("get_task_info",
|
||||
"查询单个任务详细信息,包括类别、状态、占用时段、嵌入关系。",
|
||||
`{"name":"get_task_info","parameters":{"task_id":{"type":"int","required":true}}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
taskID, ok := argsInt(args, "task_id")
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
taskID, ok := schedule.ArgsInt(args, "task_id")
|
||||
if !ok {
|
||||
return "查询失败:缺少必填参数 task_id。"
|
||||
}
|
||||
return GetTaskInfo(state, taskID)
|
||||
return schedule.GetTaskInfo(state, taskID)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -196,120 +201,142 @@ func NewDefaultRegistryWithDeps(deps DefaultRegistryDeps) *ToolRegistry {
|
||||
r.Register("place",
|
||||
"将一个待安排任务预排到指定位置。自动检测可嵌入宿主。task_id/day/slot_start 必填。",
|
||||
`{"name":"place","parameters":{"task_id":{"type":"int","required":true},"day":{"type":"int","required":true},"slot_start":{"type":"int","required":true}}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
taskID, ok := argsInt(args, "task_id")
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
taskID, ok := schedule.ArgsInt(args, "task_id")
|
||||
if !ok {
|
||||
return "放置失败:缺少必填参数 task_id。"
|
||||
}
|
||||
day, ok := argsInt(args, "day")
|
||||
day, ok := schedule.ArgsInt(args, "day")
|
||||
if !ok {
|
||||
return "放置失败:缺少必填参数 day。"
|
||||
}
|
||||
slotStart, ok := argsInt(args, "slot_start")
|
||||
slotStart, ok := schedule.ArgsInt(args, "slot_start")
|
||||
if !ok {
|
||||
return "放置失败:缺少必填参数 slot_start。"
|
||||
}
|
||||
return Place(state, taskID, day, slotStart)
|
||||
return schedule.Place(state, taskID, day, slotStart)
|
||||
},
|
||||
)
|
||||
|
||||
r.Register("move",
|
||||
"将一个已预排任务(仅 suggested)移动到新位置。existing 属于已安排事实层,不参与 move。task_id/new_day/new_slot_start 必填。",
|
||||
`{"name":"move","parameters":{"task_id":{"type":"int","required":true},"new_day":{"type":"int","required":true},"new_slot_start":{"type":"int","required":true}}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
taskID, ok := argsInt(args, "task_id")
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
taskID, ok := schedule.ArgsInt(args, "task_id")
|
||||
if !ok {
|
||||
return "移动失败:缺少必填参数 task_id。"
|
||||
}
|
||||
newDay, ok := argsInt(args, "new_day")
|
||||
newDay, ok := schedule.ArgsInt(args, "new_day")
|
||||
if !ok {
|
||||
return "移动失败:缺少必填参数 new_day。"
|
||||
}
|
||||
newSlotStart, ok := argsInt(args, "new_slot_start")
|
||||
newSlotStart, ok := schedule.ArgsInt(args, "new_slot_start")
|
||||
if !ok {
|
||||
return "移动失败:缺少必填参数 new_slot_start。"
|
||||
}
|
||||
return Move(state, taskID, newDay, newSlotStart)
|
||||
return schedule.Move(state, taskID, newDay, newSlotStart)
|
||||
},
|
||||
)
|
||||
|
||||
r.Register("swap",
|
||||
"交换两个已落位任务的位置。两个任务必须时长相同。task_a/task_b 必填。",
|
||||
`{"name":"swap","parameters":{"task_a":{"type":"int","required":true},"task_b":{"type":"int","required":true}}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
taskA, ok := argsInt(args, "task_a")
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
taskA, ok := schedule.ArgsInt(args, "task_a")
|
||||
if !ok {
|
||||
return "交换失败:缺少必填参数 task_a。"
|
||||
}
|
||||
taskB, ok := argsInt(args, "task_b")
|
||||
taskB, ok := schedule.ArgsInt(args, "task_b")
|
||||
if !ok {
|
||||
return "交换失败:缺少必填参数 task_b。"
|
||||
}
|
||||
return Swap(state, taskA, taskB)
|
||||
return schedule.Swap(state, taskA, taskB)
|
||||
},
|
||||
)
|
||||
|
||||
r.Register("batch_move",
|
||||
"原子性批量移动多个任务(仅 suggested,最多2条),全部成功才生效。若含 existing/pending 或任一冲突将整批失败回滚。",
|
||||
`{"name":"batch_move","parameters":{"moves":{"type":"array","required":true,"items":{"task_id":"int","new_day":"int","new_slot_start":"int"}}}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
moves, err := argsMoveList(args)
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
moves, err := schedule.ArgsMoveList(args)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("批量移动失败:%s", err.Error())
|
||||
}
|
||||
return BatchMove(state, moves)
|
||||
return schedule.BatchMove(state, moves)
|
||||
},
|
||||
)
|
||||
|
||||
r.Register("queue_apply_head_move",
|
||||
"将当前队首任务移动到指定位置并自动出队。仅作用于 current,不接受 task_id。new_day/new_slot_start 必填。",
|
||||
`{"name":"queue_apply_head_move","parameters":{"new_day":{"type":"int","required":true},"new_slot_start":{"type":"int","required":true}}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
return QueueApplyHeadMove(state, args)
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
return schedule.QueueApplyHeadMove(state, args)
|
||||
},
|
||||
)
|
||||
|
||||
r.Register("queue_skip_head",
|
||||
"跳过当前队首任务(不改日程),将其标记为 skipped 并继续后续队列。",
|
||||
`{"name":"queue_skip_head","parameters":{"reason":{"type":"string"}}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
return QueueSkipHead(state, args)
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
return schedule.QueueSkipHead(state, args)
|
||||
},
|
||||
)
|
||||
|
||||
r.Register("min_context_switch",
|
||||
"在指定任务集合内重排 suggested 任务,尽量让同类任务连续以减少上下文切换。仅在用户明确允许打乱顺序时使用。task_ids 必填(兼容 task_id)。",
|
||||
`{"name":"min_context_switch","parameters":{"task_ids":{"type":"array","required":true,"items":{"type":"int"}},"task_id":{"type":"int"}}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
taskIDs, err := parseMinContextSwitchTaskIDs(args)
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
taskIDs, err := schedule.ParseMinContextSwitchTaskIDs(args)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("减少上下文切换失败:%s。", err.Error())
|
||||
}
|
||||
return MinContextSwitch(state, taskIDs)
|
||||
return schedule.MinContextSwitch(state, taskIDs)
|
||||
},
|
||||
)
|
||||
|
||||
r.Register("spread_even",
|
||||
"在给定任务集合内做均匀化铺开:先按筛选条件收集候选坑位,再规划并原子落地。task_ids 必填(兼容 task_id)。",
|
||||
`{"name":"spread_even","parameters":{"task_ids":{"type":"array","required":true,"items":{"type":"int"}},"task_id":{"type":"int"},"limit":{"type":"int"},"allow_embed":{"type":"bool"},"day":{"type":"int"},"day_start":{"type":"int"},"day_end":{"type":"int"},"day_scope":{"type":"string","enum":["all","workday","weekend"]},"day_of_week":{"type":"array","items":{"type":"int"}},"week":{"type":"int"},"week_filter":{"type":"array","items":{"type":"int"}},"week_from":{"type":"int"},"week_to":{"type":"int"},"slot_type":{"type":"string"},"slot_types":{"type":"array","items":{"type":"string"}},"exclude_sections":{"type":"array","items":{"type":"int"}},"after_section":{"type":"int"},"before_section":{"type":"int"}}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
taskIDs, err := parseSpreadEvenTaskIDs(args)
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
taskIDs, err := schedule.ParseSpreadEvenTaskIDs(args)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("均匀化调整失败:%s。", err.Error())
|
||||
}
|
||||
return SpreadEven(state, taskIDs, args)
|
||||
return schedule.SpreadEven(state, taskIDs, args)
|
||||
},
|
||||
)
|
||||
|
||||
r.Register("unplace",
|
||||
"将一个已落位任务移除,恢复为待安排状态。会自动清理嵌入关系。task_id 必填。",
|
||||
`{"name":"unplace","parameters":{"task_id":{"type":"int","required":true}}}`,
|
||||
func(state *ScheduleState, args map[string]any) string {
|
||||
taskID, ok := argsInt(args, "task_id")
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
taskID, ok := schedule.ArgsInt(args, "task_id")
|
||||
if !ok {
|
||||
return "移除失败:缺少必填参数 task_id。"
|
||||
}
|
||||
return Unplace(state, taskID)
|
||||
return schedule.Unplace(state, taskID)
|
||||
},
|
||||
)
|
||||
|
||||
// --- Web 搜索读工具 ---
|
||||
// 1. provider 为 nil 时 handler 返回"暂未启用"的 observation,不会阻断主流程;
|
||||
// 2. 两个工具均为读操作,走 action=continue + tool_call 模式。
|
||||
webSearchHandler := web.NewSearchToolHandler(deps.WebSearchProvider)
|
||||
webFetchHandler := web.NewFetchToolHandler(web.NewFetcher())
|
||||
|
||||
r.Register("web_search",
|
||||
"Web 搜索:根据 query 返回结构化检索结果(标题/摘要/URL/来源域名/时间)。query 必填。",
|
||||
`{"name":"web_search","parameters":{"query":{"type":"string","required":true},"top_k":{"type":"int"},"domain_allow":{"type":"array","items":{"type":"string"}},"recency_days":{"type":"int"}}}`,
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
return webSearchHandler.Handle(args)
|
||||
},
|
||||
)
|
||||
|
||||
r.Register("web_fetch",
|
||||
"抓取指定 URL 的正文内容并做最小 HTML 清洗。url 必填。",
|
||||
`{"name":"web_fetch","parameters":{"url":{"type":"string","required":true},"max_chars":{"type":"int"}}}`,
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
return webFetchHandler.Handle(args)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package newagenttools
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package newagenttools
|
||||
package schedule
|
||||
|
||||
import "fmt"
|
||||
|
||||
@@ -7,7 +7,7 @@ import "fmt"
|
||||
// JSON 反序列化后数字默认为 float64,字符串为 string,需要类型断言。
|
||||
|
||||
// argsInt 从 map 中提取 int 值。支持 float64(JSON 反序列化的默认类型)。
|
||||
func argsInt(args map[string]any, key string) (int, bool) {
|
||||
func ArgsInt(args map[string]any, key string) (int, bool) {
|
||||
v, ok := args[key]
|
||||
if !ok {
|
||||
return 0, false
|
||||
@@ -22,7 +22,7 @@ func argsInt(args map[string]any, key string) (int, bool) {
|
||||
}
|
||||
|
||||
// argsString 从 map 中提取 string 值。
|
||||
func argsString(args map[string]any, key string) (string, bool) {
|
||||
func ArgsString(args map[string]any, key string) (string, bool) {
|
||||
v, ok := args[key]
|
||||
if !ok {
|
||||
return "", false
|
||||
@@ -32,8 +32,8 @@ func argsString(args map[string]any, key string) (string, bool) {
|
||||
}
|
||||
|
||||
// argsIntPtr 从 map 中提取可选 int 值,不存在返回 nil。
|
||||
func argsIntPtr(args map[string]any, key string) *int {
|
||||
v, ok := argsInt(args, key)
|
||||
func ArgsIntPtr(args map[string]any, key string) *int {
|
||||
v, ok := ArgsInt(args, key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -41,8 +41,8 @@ func argsIntPtr(args map[string]any, key string) *int {
|
||||
}
|
||||
|
||||
// argsStringPtr 从 map 中提取可选 string 值,不存在返回 nil。
|
||||
func argsStringPtr(args map[string]any, key string) *string {
|
||||
v, ok := argsString(args, key)
|
||||
func ArgsStringPtr(args map[string]any, key string) *string {
|
||||
v, ok := ArgsString(args, key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func argsStringPtr(args map[string]any, key string) *string {
|
||||
}
|
||||
|
||||
// argsIntSlice 从 map 中提取 int 数组,支持 []any / []int / []float64。
|
||||
func argsIntSlice(args map[string]any, key string) ([]int, bool) {
|
||||
func ArgsIntSlice(args map[string]any, key string) ([]int, bool) {
|
||||
v, ok := args[key]
|
||||
if !ok {
|
||||
return nil, false
|
||||
@@ -88,7 +88,7 @@ func argsIntSlice(args map[string]any, key string) ([]int, bool) {
|
||||
}
|
||||
|
||||
// argsMoveList 从 map 中提取 batch_move 的 moves 数组。
|
||||
func argsMoveList(args map[string]any) ([]MoveRequest, error) {
|
||||
func ArgsMoveList(args map[string]any) ([]MoveRequest, error) {
|
||||
v, ok := args["moves"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("缺少 moves 参数")
|
||||
@@ -103,15 +103,15 @@ func argsMoveList(args map[string]any) ([]MoveRequest, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("moves[%d] 不是有效对象", i)
|
||||
}
|
||||
taskID, ok := argsInt(m, "task_id")
|
||||
taskID, ok := ArgsInt(m, "task_id")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("moves[%d].task_id 缺失或无效", i)
|
||||
}
|
||||
newDay, ok := argsInt(m, "new_day")
|
||||
newDay, ok := ArgsInt(m, "new_day")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("moves[%d].new_day 缺失或无效", i)
|
||||
}
|
||||
newSlotStart, ok := argsInt(m, "new_slot_start")
|
||||
newSlotStart, ok := ArgsInt(m, "new_slot_start")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("moves[%d].new_slot_start 缺失或无效", i)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package newagenttools
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -305,19 +305,19 @@ func SpreadEven(state *ScheduleState, taskIDs []int, args map[string]any) string
|
||||
return strings.TrimSpace(sb.String())
|
||||
}
|
||||
|
||||
func parseMinContextSwitchTaskIDs(args map[string]any) ([]int, error) {
|
||||
return parseCompositeTaskIDs(args)
|
||||
func ParseMinContextSwitchTaskIDs(args map[string]any) ([]int, error) {
|
||||
return ParseCompositeTaskIDs(args)
|
||||
}
|
||||
|
||||
func parseSpreadEvenTaskIDs(args map[string]any) ([]int, error) {
|
||||
return parseCompositeTaskIDs(args)
|
||||
func ParseSpreadEvenTaskIDs(args map[string]any) ([]int, error) {
|
||||
return ParseCompositeTaskIDs(args)
|
||||
}
|
||||
|
||||
func parseCompositeTaskIDs(args map[string]any) ([]int, error) {
|
||||
if ids, ok := argsIntSlice(args, "task_ids"); ok && len(ids) > 0 {
|
||||
func ParseCompositeTaskIDs(args map[string]any) ([]int, error) {
|
||||
if ids, ok := ArgsIntSlice(args, "task_ids"); ok && len(ids) > 0 {
|
||||
return ids, nil
|
||||
}
|
||||
if id, ok := argsInt(args, "task_id"); ok {
|
||||
if id, ok := ArgsInt(args, "task_id"); ok {
|
||||
return []int{id}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("缺少必填参数 task_ids(兼容单值 task_id)")
|
||||
@@ -1,4 +1,4 @@
|
||||
package newagenttools
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -117,7 +117,7 @@ func QueueApplyHeadMove(state *ScheduleState, args map[string]any) string {
|
||||
}, "queue_apply_head_move")
|
||||
}
|
||||
|
||||
newDay, ok := argsInt(args, "new_day")
|
||||
newDay, ok := ArgsInt(args, "new_day")
|
||||
if !ok {
|
||||
return mustJSON(queueApplyHeadMoveResult{
|
||||
Tool: "queue_apply_head_move",
|
||||
@@ -130,7 +130,7 @@ func QueueApplyHeadMove(state *ScheduleState, args map[string]any) string {
|
||||
Result: "缺少必填参数 new_day。",
|
||||
}, "queue_apply_head_move")
|
||||
}
|
||||
newSlotStart, ok := argsInt(args, "new_slot_start")
|
||||
newSlotStart, ok := ArgsInt(args, "new_slot_start")
|
||||
if !ok {
|
||||
return mustJSON(queueApplyHeadMoveResult{
|
||||
Tool: "queue_apply_head_move",
|
||||
@@ -190,7 +190,7 @@ func QueueSkipHead(state *ScheduleState, args map[string]any) string {
|
||||
}
|
||||
|
||||
reason := ""
|
||||
if raw, ok := argsString(args, "reason"); ok {
|
||||
if raw, ok := ArgsString(args, "reason"); ok {
|
||||
reason = strings.TrimSpace(raw)
|
||||
}
|
||||
markCurrentTaskSkipped(state)
|
||||
@@ -1,4 +1,4 @@
|
||||
package newagenttools
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -850,7 +850,7 @@ func inferWeekBounds(state *ScheduleState) (int, int) {
|
||||
// readIntAny 按别名顺序读取 int 参数。
|
||||
func readIntAny(args map[string]any, keys ...string) (int, bool) {
|
||||
for _, key := range keys {
|
||||
value, ok := argsInt(args, key)
|
||||
value, ok := ArgsInt(args, key)
|
||||
if ok {
|
||||
return value, true
|
||||
}
|
||||
@@ -861,7 +861,7 @@ func readIntAny(args map[string]any, keys ...string) (int, bool) {
|
||||
// readStringAny 按别名顺序读取 string 参数。
|
||||
func readStringAny(args map[string]any, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value, ok := argsString(args, key); ok {
|
||||
if value, ok := ArgsString(args, key); ok {
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -894,7 +894,7 @@ func readBoolAnyWithDefault(args map[string]any, defaultValue bool, keys ...stri
|
||||
// readIntSliceAny 按别名顺序读取 int 列表参数。
|
||||
func readIntSliceAny(args map[string]any, keys ...string) []int {
|
||||
for _, key := range keys {
|
||||
if values, ok := argsIntSlice(args, key); ok {
|
||||
if values, ok := ArgsIntSlice(args, key); ok {
|
||||
return values
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package newagenttools
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package newagenttools
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package newagenttools
|
||||
package schedule
|
||||
|
||||
// TaskProcessingQueue 表示 execute 阶段的“逐项处理队列”运行态。
|
||||
//
|
||||
@@ -1,4 +1,4 @@
|
||||
package newagenttools
|
||||
package schedule
|
||||
|
||||
// DayMapping maps a day_index to a real (week, day_of_week) coordinate.
|
||||
type DayMapping struct {
|
||||
@@ -1,4 +1,4 @@
|
||||
package newagenttools
|
||||
package schedule
|
||||
|
||||
import "slices"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package newagenttools
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package newagenttools
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
175
backend/newAgent/tools/web/fetcher.go
Normal file
175
backend/newAgent/tools/web/fetcher.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Fetcher 抓取指定 URL 正文并做最小 HTML 清洗。
|
||||
//
|
||||
// 职责:
|
||||
// 1. 发起 HTTP GET 请求并读取响应体;
|
||||
// 2. 剥离 HTML 标签,保留纯文本内容;
|
||||
// 3. 按 MaxChars 截断,避免超长正文占用模型上下文。
|
||||
//
|
||||
// 不负责:
|
||||
// 1. 不负责 JS 渲染(无法处理 SPA 页面);
|
||||
// 2. 不负责反爬绕过(遇到 403 直接返回错误);
|
||||
// 3. 不负责正文提取算法优化(仅做粗粒度标签剥离)。
|
||||
type Fetcher struct {
|
||||
// Client 带超时的 HTTP 客户端,由调用方注入。
|
||||
Client *http.Client
|
||||
|
||||
// MaxChars 正文最大字符数。超出时截断并标记 truncated=true。0 使用默认值 4000。
|
||||
MaxChars int
|
||||
}
|
||||
|
||||
// NewFetcher 创建默认 Fetcher。
|
||||
//
|
||||
// 1. 超时默认 10 秒,足够覆盖大多数静态页面;
|
||||
// 2. MaxChars 默认 4000 字符,约占 1000~2000 token,不会挤占过多上下文。
|
||||
func NewFetcher() *Fetcher {
|
||||
return &Fetcher{
|
||||
Client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
MaxChars: 4000,
|
||||
}
|
||||
}
|
||||
|
||||
// FetchResult 抓取结果。
|
||||
type FetchResult struct {
|
||||
// Title 页面标题(从 <title> 标签提取)。
|
||||
Title string
|
||||
|
||||
// Content 清洗后的纯文本正文。
|
||||
Content string
|
||||
|
||||
// Truncated 正文是否被截断。
|
||||
Truncated bool
|
||||
}
|
||||
|
||||
// Fetch 抓取指定 URL 并返回清洗后的正文。
|
||||
//
|
||||
// 流程:
|
||||
// 1. 构建带超时的 HTTP GET 请求;
|
||||
// 2. 检查状态码,非 2xx 直接返回可读错误;
|
||||
// 3. 读取响应体,提取 <title>;
|
||||
// 4. 剥离 HTML 标签,按 MaxChars 截断;
|
||||
// 5. 所有失败场景返回 error,由工具层兜底组装 observation。
|
||||
func (f *Fetcher) Fetch(ctx context.Context, url string) (*FetchResult, error) {
|
||||
// 1. 构建请求,注入 ctx 用于超时与取消。
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("构建请求失败:%w", err)
|
||||
}
|
||||
|
||||
// 2. 模拟浏览器 User-Agent,避免部分站点直接拒绝。
|
||||
req.Header.Set("User-Agent", "SmartFlow-Agent/1.0 (compatible; web_fetch)")
|
||||
|
||||
resp, err := f.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败:%w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 3. 非 2xx 返回明确状态码,方便工具层区分 4xx/5xx。
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("HTTP %d:%s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
// 4. 限制读取量(最多 1MB),防止恶意超长响应撑爆内存。
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应体失败:%w", err)
|
||||
}
|
||||
|
||||
htmlStr := string(body)
|
||||
|
||||
// 5. 提取 <title> 内容。
|
||||
title := extractHTMLTitle(htmlStr)
|
||||
|
||||
// 6. 剥离 HTML 标签,得到纯文本。
|
||||
text := stripHTMLTags(htmlStr)
|
||||
|
||||
// 7. 清理多余空白(连续换行、行首行尾空格)。
|
||||
text = cleanWhitespace(text)
|
||||
|
||||
// 8. 按 MaxChars 截断。
|
||||
maxChars := f.MaxChars
|
||||
if maxChars <= 0 {
|
||||
maxChars = 4000
|
||||
}
|
||||
truncated := false
|
||||
runes := []rune(text)
|
||||
if len(runes) > maxChars {
|
||||
truncated = true
|
||||
runes = runes[:maxChars]
|
||||
}
|
||||
|
||||
return &FetchResult{
|
||||
Title: title,
|
||||
Content: string(runes),
|
||||
Truncated: truncated,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extractHTMLTitle 从 HTML 中提取 <title> 标签内容。
|
||||
//
|
||||
// 1. 使用正则匹配,不做 DOM 解析(兼顾性能与简单性);
|
||||
// 2. 找不到时返回空字符串,不报错。
|
||||
func extractHTMLTitle(htmlStr string) string {
|
||||
re := regexp.MustCompile("(?i)<title[^>]*>(.*?)</title>")
|
||||
matches := re.FindStringSubmatch(htmlStr)
|
||||
if len(matches) >= 2 {
|
||||
return strings.TrimSpace(matches[1])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// stripHTMLTags 剥离所有 HTML 标签,保留纯文本。
|
||||
//
|
||||
// 1. 先移除 <script> / <style> 块(避免 JS/CSS 内容污染正文);
|
||||
// 2. 再移除所有 HTML 标签;
|
||||
// 3. 解码常见 HTML 实体(& < > ")。
|
||||
func stripHTMLTags(htmlStr string) string {
|
||||
// 1. 移除 script/style 块
|
||||
re := regexp.MustCompile("(?is)<(script|style)[^>]*>.*?</\\1>")
|
||||
text := re.ReplaceAllString(htmlStr, " ")
|
||||
|
||||
// 2. 移除所有 HTML 标签
|
||||
reTag := regexp.MustCompile("<[^>]+>")
|
||||
text = reTag.ReplaceAllString(text, " ")
|
||||
|
||||
// 3. 解码常见 HTML 实体
|
||||
text = strings.ReplaceAll(text, "&", "&")
|
||||
text = strings.ReplaceAll(text, "<", "<")
|
||||
text = strings.ReplaceAll(text, ">", ">")
|
||||
text = strings.ReplaceAll(text, """, "\"")
|
||||
text = strings.ReplaceAll(text, "'", "'")
|
||||
text = strings.ReplaceAll(text, " ", " ")
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// cleanWhitespace 清理多余空白:连续空行合并为单个换行,去除行首行尾空格。
|
||||
func cleanWhitespace(text string) string {
|
||||
// 1. 连续换行压缩为最多两个换行(保留段落分隔感)。
|
||||
re := regexp.MustCompile("\\n{3,}")
|
||||
text = re.ReplaceAllString(text, "\n\n")
|
||||
|
||||
// 2. 按行去除首尾空白后重新拼装。
|
||||
lines := strings.Split(text, "\n")
|
||||
cleaned := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
cleaned = append(cleaned, trimmed)
|
||||
}
|
||||
|
||||
return strings.Join(cleaned, "\n")
|
||||
}
|
||||
82
backend/newAgent/tools/web/provider.go
Normal file
82
backend/newAgent/tools/web/provider.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SearchProvider 搜索供应商抽象接口。
|
||||
//
|
||||
// 职责:
|
||||
// 1. 接收检索查询与选项,返回结构化搜索结果;
|
||||
// 2. 实现方负责 HTTP 调用、错误重试、限流兜底;
|
||||
// 3. 调用方不感知底层是 Bocha / Mock 还是其他供应商。
|
||||
//
|
||||
// 不负责:
|
||||
// 1. 不负责 URL 正文抓取(由 Fetcher 承担);
|
||||
// 2. 不负责结果缓存(由上层工具决定)。
|
||||
type SearchProvider interface {
|
||||
// Name 返回供应商名称(如 "mock"、"bocha"),用于日志与降级标识。
|
||||
Name() string
|
||||
|
||||
// Search 执行一次检索。
|
||||
//
|
||||
// 1. ctx 用于超时控制与取消;
|
||||
// 2. opts.TopK 默认 5,上限 20,超出自动截断;
|
||||
// 3. 失败时返回 error,调用方负责兜底 observation 组装。
|
||||
Search(ctx context.Context, query string, opts SearchOptions) (*SearchResponse, error)
|
||||
}
|
||||
|
||||
// SearchOptions 搜索可选参数。
|
||||
type SearchOptions struct {
|
||||
// TopK 返回结果数上限。0 表示使用供应商默认值(通常为 5)。
|
||||
TopK int
|
||||
|
||||
// DomainAllow 仅返回指定域名下的结果。空表示不限。
|
||||
DomainAllow []string
|
||||
|
||||
// RecencyDays 仅返回最近 N 天内的结果。0 表示不限时间。
|
||||
RecencyDays int
|
||||
}
|
||||
|
||||
// SearchResponse 搜索结果集合。
|
||||
type SearchResponse struct {
|
||||
// Query 原始查询文本,用于日志追踪。
|
||||
Query string
|
||||
|
||||
// Items 搜索结果条目,按相关性降序排列。
|
||||
Items []SearchItem
|
||||
}
|
||||
|
||||
// SearchItem 单条搜索结果。
|
||||
type SearchItem struct {
|
||||
// Title 页面标题。
|
||||
Title string
|
||||
|
||||
// URL 页面链接。
|
||||
URL string
|
||||
|
||||
// Snippet 搜索引擎返回的摘要片段。
|
||||
Snippet string
|
||||
|
||||
// Domain 来源域名(如 "example.com"),由实现方从 URL 提取。
|
||||
Domain string
|
||||
|
||||
// PublishedAt 页面发布时间(若供应商可提供)。零值表示未知。
|
||||
PublishedAt time.Time
|
||||
|
||||
// Raw 供应商原始响应字段,供调试用,不传给模型。
|
||||
Raw map[string]any
|
||||
}
|
||||
|
||||
// normalizeTopK 将用户传入的 topK 归一化到 [1, max] 区间。
|
||||
// 默认值 5,上限 20,防止模型传入异常值导致 API 爆炸。
|
||||
func normalizeTopK(topK, defaultVal, maxVal int) int {
|
||||
if topK <= 0 {
|
||||
return defaultVal
|
||||
}
|
||||
if topK > maxVal {
|
||||
return maxVal
|
||||
}
|
||||
return topK
|
||||
}
|
||||
217
backend/newAgent/tools/web/provider_bocha.go
Normal file
217
backend/newAgent/tools/web/provider_bocha.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BochaProvider 博查(Bocha)搜索供应商实现。
|
||||
//
|
||||
// 职责:
|
||||
// 1. 将 SearchOptions 映射为博查 API 请求参数;
|
||||
// 2. 发起 HTTP POST 调用博查 web-search 端点;
|
||||
// 3. 将博查响应转换为统一的 SearchResponse 结构。
|
||||
//
|
||||
// 不负责:
|
||||
// 1. 不负责 API Key 管理(由调用方注入);
|
||||
// 2. 不负责重试(单次调用失败直接返回 error);
|
||||
// 3. 不负责 URL 正文抓取(由 Fetcher 承担)。
|
||||
//
|
||||
// 博查 API 文档:https://open.bochaai.com/
|
||||
type BochaProvider struct {
|
||||
// apiKey 博查 API Key,从配置注入。
|
||||
apiKey string
|
||||
|
||||
// httpClient 带超时的 HTTP 客户端。
|
||||
httpClient *http.Client
|
||||
|
||||
// baseURL 博查 API 基础地址,默认 https://api.bochaai.com/v1。
|
||||
baseURL string
|
||||
}
|
||||
|
||||
// NewBochaProvider 创建博查搜索供应商。
|
||||
//
|
||||
// 1. apiKey 必填,为空时 Search 会返回明确错误;
|
||||
// 2. 超时默认 10 秒,与工具层 ctx 超时对齐;
|
||||
// 3. baseURL 留空则使用默认地址。
|
||||
func NewBochaProvider(apiKey, baseURL string) *BochaProvider {
|
||||
if baseURL == "" {
|
||||
baseURL = "https://api.bochaai.com/v1"
|
||||
}
|
||||
return &BochaProvider{
|
||||
apiKey: apiKey,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
baseURL: baseURL,
|
||||
}
|
||||
}
|
||||
|
||||
// Name 返回供应商标识。
|
||||
func (b *BochaProvider) Name() string { return "bocha" }
|
||||
|
||||
// Search 调用博查 web-search API 执行检索。
|
||||
//
|
||||
// 流程:
|
||||
// 1. 参数校验(apiKey 非空、query 非空);
|
||||
// 2. 将 SearchOptions 映射为博查请求体(count / freshness / summary);
|
||||
// 3. 发起 HTTP POST,读取响应;
|
||||
// 4. 解析博查 JSON 响应,提取 webPages.value 数组;
|
||||
// 5. 转换为统一 SearchItem 结构返回。
|
||||
//
|
||||
// 错误处理:
|
||||
// - apiKey 为空 → 返回明确错误;
|
||||
// - HTTP 非 2xx → 返回带状态码的错误;
|
||||
// - 响应解析失败 → 返回原始响应片段供排查。
|
||||
func (b *BochaProvider) Search(ctx context.Context, query string, opts SearchOptions) (*SearchResponse, error) {
|
||||
// 1. 参数校验。
|
||||
if b.apiKey == "" {
|
||||
return nil, fmt.Errorf("博查 API Key 未配置")
|
||||
}
|
||||
query = strings.TrimSpace(query)
|
||||
if query == "" {
|
||||
return nil, fmt.Errorf("查询关键词为空")
|
||||
}
|
||||
|
||||
// 2. 组装请求体。
|
||||
// 2.1 count:博查支持 1~50,默认 10。
|
||||
count := normalizeTopK(opts.TopK, 10, 50)
|
||||
|
||||
// 2.2 freshness:将 RecencyDays 映射为博查的时间过滤枚举。
|
||||
freshness := mapRecencyDaysToFreshness(opts.RecencyDays)
|
||||
|
||||
reqBody := bochaSearchRequest{
|
||||
Query: query,
|
||||
Count: count,
|
||||
Freshness: freshness,
|
||||
Summary: true, // 开启 AI 摘要,提升结果信息密度
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化请求体失败:%w", err)
|
||||
}
|
||||
|
||||
// 3. 发起 HTTP POST。
|
||||
url := b.baseURL + "/web-search"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(bodyBytes)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("构建请求失败:%w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+b.apiKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := b.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求博查 API 失败:%w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 4. 读取响应体(限制 2MB,防止异常响应撑爆内存)。
|
||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取博查响应失败:%w", err)
|
||||
}
|
||||
|
||||
// 5. 检查 HTTP 状态码。
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
// 截取前 200 字符作为错误上下文,避免日志过长。
|
||||
snippet := string(respBody)
|
||||
if len(snippet) > 200 {
|
||||
snippet = snippet[:200]
|
||||
}
|
||||
return nil, fmt.Errorf("博查 API 返回 HTTP %d:%s", resp.StatusCode, snippet)
|
||||
}
|
||||
|
||||
// 6. 解析响应 JSON。
|
||||
var bochaResp bochaSearchAPIResponse
|
||||
if err := json.Unmarshal(respBody, &bochaResp); err != nil {
|
||||
return nil, fmt.Errorf("解析博查响应失败:%w", err)
|
||||
}
|
||||
|
||||
// 7. 提取搜索结果。
|
||||
items := make([]SearchItem, 0, len(bochaResp.Data.WebPages.Value))
|
||||
for _, v := range bochaResp.Data.WebPages.Value {
|
||||
item := SearchItem{
|
||||
Title: v.Name,
|
||||
URL: v.URL,
|
||||
Snippet: v.Summary, // 优先使用 AI 摘要;若为空则回退到 snippet
|
||||
Domain: v.SiteName,
|
||||
}
|
||||
if item.Snippet == "" {
|
||||
item.Snippet = v.Snippet
|
||||
}
|
||||
// 解析发布时间(博查格式:2024-07-22T00:00:00+08:00)。
|
||||
if v.DatePublished != "" {
|
||||
if t, err := time.Parse(time.RFC3339, v.DatePublished); err == nil {
|
||||
item.PublishedAt = t
|
||||
}
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
return &SearchResponse{
|
||||
Query: query,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// mapRecencyDaysToFreshness 将 RecencyDays 映射为博查 freshness 枚举值。
|
||||
//
|
||||
// 映射规则:
|
||||
// - 0 → noLimit(不限时间)
|
||||
// - 1 → oneDay
|
||||
// - 2~7 → oneWeek
|
||||
// - 8~30 → oneMonth
|
||||
// - 31~365 → oneYear
|
||||
// - >365 → noLimit
|
||||
func mapRecencyDaysToFreshness(days int) string {
|
||||
switch {
|
||||
case days <= 0:
|
||||
return "noLimit"
|
||||
case days <= 1:
|
||||
return "oneDay"
|
||||
case days <= 7:
|
||||
return "oneWeek"
|
||||
case days <= 30:
|
||||
return "oneMonth"
|
||||
case days <= 365:
|
||||
return "oneYear"
|
||||
default:
|
||||
return "noLimit"
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 博查 API 请求/响应结构体 ====================
|
||||
|
||||
// bochaSearchRequest 博查 web-search 请求体。
|
||||
type bochaSearchRequest struct {
|
||||
Query string `json:"query"`
|
||||
Count int `json:"count"`
|
||||
Freshness string `json:"freshness"`
|
||||
Summary bool `json:"summary"`
|
||||
}
|
||||
|
||||
// bochaSearchAPIResponse 博查 web-search 响应体(只提取需要的字段)。
|
||||
type bochaSearchAPIResponse struct {
|
||||
Data struct {
|
||||
WebPages struct {
|
||||
Value []bochaWebPageItem `json:"value"`
|
||||
} `json:"webPages"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// bochaWebPageItem 博查单条搜索结果。
|
||||
type bochaWebPageItem struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Snippet string `json:"snippet"`
|
||||
Summary string `json:"summary"`
|
||||
SiteName string `json:"siteName"`
|
||||
DatePublished string `json:"datePublished"`
|
||||
}
|
||||
58
backend/newAgent/tools/web/provider_mock.go
Normal file
58
backend/newAgent/tools/web/provider_mock.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MockProvider 空实现搜索供应商,返回硬编码结果。
|
||||
//
|
||||
// 用途:
|
||||
// 1. 在真实 API Key 到手前,先跑通工具注册→调用→observation 写回的完整链路;
|
||||
// 2. 后续替换为 Tavily/Brave 实现后,Mock 保留用于单元测试。
|
||||
//
|
||||
// 不负责:
|
||||
// 1. 不负责真实 HTTP 调用;
|
||||
// 2. 不负责网络错误模拟(如需测试超时可另行实现 TimeoutMockProvider)。
|
||||
type MockProvider struct{}
|
||||
|
||||
// Name 返回供应商标识。
|
||||
func (m *MockProvider) Name() string { return "mock" }
|
||||
|
||||
// Search 返回 2 条硬编码搜索结果,模拟正常检索链路。
|
||||
//
|
||||
// 1. 无论 query 内容如何,始终返回相同结果;
|
||||
// 2. ctx 仅做形式兼容,不检查超时;
|
||||
// 3. 永远不返回 error(Mock 不模拟失败场景)。
|
||||
func (m *MockProvider) Search(_ context.Context, query string, opts SearchOptions) (*SearchResponse, error) {
|
||||
topK := normalizeTopK(opts.TopK, 5, 20)
|
||||
|
||||
// 1. 准备 2 条模拟数据,覆盖核心字段(title/url/snippet/domain/published_at);
|
||||
// 2. 若调用方 topK=1 则只返回第一条。
|
||||
mockItems := []SearchItem{
|
||||
{
|
||||
Title: fmt.Sprintf("搜索结果示例 - %s", query),
|
||||
URL: "https://example.com/search-result-1",
|
||||
Snippet: "这是 MockProvider 返回的模拟搜索摘要,用于验证工具链路是否通畅。",
|
||||
Domain: "example.com",
|
||||
PublishedAt: time.Now().Add(-24 * time.Hour),
|
||||
},
|
||||
{
|
||||
Title: fmt.Sprintf("相关资料 - %s", query),
|
||||
URL: "https://example.com/related-resource-2",
|
||||
Snippet: "这是第二条 Mock 结果,模拟同主题下的补充信息来源。",
|
||||
Domain: "example.com",
|
||||
PublishedAt: time.Now().Add(-48 * time.Hour),
|
||||
},
|
||||
}
|
||||
|
||||
if topK < len(mockItems) {
|
||||
mockItems = mockItems[:topK]
|
||||
}
|
||||
|
||||
return &SearchResponse{
|
||||
Query: query,
|
||||
Items: mockItems,
|
||||
}, nil
|
||||
}
|
||||
227
backend/newAgent/tools/web/tools.go
Normal file
227
backend/newAgent/tools/web/tools.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SearchToolHandler web_search 工具 handler。
|
||||
//
|
||||
// 职责:
|
||||
// 1. 解析 args(query / top_k / domain_allow / recency_days);
|
||||
// 2. 调用 SearchProvider 执行检索;
|
||||
// 3. 组装结构化 JSON observation 返回给模型。
|
||||
//
|
||||
// 不负责:
|
||||
// 1. 不负责 provider 生命周期管理(由注册层注入);
|
||||
// 2. 不负责重试(provider 内部处理)。
|
||||
type SearchToolHandler struct {
|
||||
provider SearchProvider
|
||||
}
|
||||
|
||||
// NewSearchToolHandler 创建 web_search 工具 handler。
|
||||
//
|
||||
// 1. provider 为 nil 时,Handle 返回"搜索暂未启用"的 observation;
|
||||
// 2. 这样做的好处是:即使未配置 provider,也不会阻断主流程。
|
||||
func NewSearchToolHandler(provider SearchProvider) *SearchToolHandler {
|
||||
return &SearchToolHandler{provider: provider}
|
||||
}
|
||||
|
||||
// searchToolArgs web_search 工具的参数定义。
|
||||
type searchToolArgs struct {
|
||||
Query string `json:"query"`
|
||||
TopK int `json:"top_k"`
|
||||
DomainAllow []string `json:"domain_allow"`
|
||||
RecencyDays int `json:"recency_days"`
|
||||
}
|
||||
|
||||
// searchToolResult web_search 工具的输出结构。
|
||||
type searchToolResult struct {
|
||||
Tool string `json:"tool"`
|
||||
Query string `json:"query"`
|
||||
Count int `json:"count"`
|
||||
Items []searchItem `json:"items"`
|
||||
}
|
||||
|
||||
// searchItem 输出给模型的单条搜索结果。
|
||||
type searchItem struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Snippet string `json:"snippet"`
|
||||
Domain string `json:"domain"`
|
||||
PublishedAt string `json:"published_at,omitempty"`
|
||||
}
|
||||
|
||||
// Handle 执行 web_search 工具。
|
||||
//
|
||||
// 1. 解析参数,query 为必填,缺失时直接返回错误 observation;
|
||||
// 2. 调用 provider.Search,超时上限 10 秒;
|
||||
// 3. 失败时返回可恢复 observation(包含错误原因),不 panic、不阻断主流程。
|
||||
func (h *SearchToolHandler) Handle(args map[string]any) string {
|
||||
// 1. provider 为 nil 说明未启用,直接返回提示。
|
||||
if h.provider == nil {
|
||||
return `{"tool":"web_search","error":"搜索暂未启用,请跳过 web_search 继续执行其他操作。"}`
|
||||
}
|
||||
|
||||
// 2. 提取必填参数 query。
|
||||
query, _ := args["query"].(string)
|
||||
query = strings.TrimSpace(query)
|
||||
if query == "" {
|
||||
return `{"tool":"web_search","error":"参数错误:缺少必填参数 query。"}`
|
||||
}
|
||||
|
||||
// 3. 提取可选参数。
|
||||
topK, _ := args["top_k"].(float64)
|
||||
var domainAllow []string
|
||||
if raw, ok := args["domain_allow"].([]any); ok {
|
||||
for _, v := range raw {
|
||||
if s, ok := v.(string); ok {
|
||||
domainAllow = append(domainAllow, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
recencyDays, _ := args["recency_days"].(float64)
|
||||
|
||||
// 4. 构建带超时的 context,防止搜索请求卡死。
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 5. 调用 provider。
|
||||
start := time.Now()
|
||||
resp, err := h.provider.Search(ctx, query, SearchOptions{
|
||||
TopK: int(topK),
|
||||
DomainAllow: domainAllow,
|
||||
RecencyDays: int(recencyDays),
|
||||
})
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// 6. 记录日志,方便排查搜索耗时、结果数、失败原因。
|
||||
log.Printf("[web_search] provider=%s query=%q topK=%d elapsed=%s results=%d err=%v",
|
||||
h.provider.Name(), query, int(topK), elapsed, len(resp.Items), err)
|
||||
|
||||
if err != nil {
|
||||
// 7. 失败时返回可恢复 observation:模型看到后可选择换 query 或跳过。
|
||||
return fmt.Sprintf(`{"tool":"web_search","error":"搜索失败:%s","query":%q}`, err.Error(), query)
|
||||
}
|
||||
|
||||
// 8. 组装输出 JSON。
|
||||
items := make([]searchItem, 0, len(resp.Items))
|
||||
for _, item := range resp.Items {
|
||||
si := searchItem{
|
||||
Title: item.Title,
|
||||
URL: item.URL,
|
||||
Snippet: item.Snippet,
|
||||
Domain: item.Domain,
|
||||
}
|
||||
if !item.PublishedAt.IsZero() {
|
||||
si.PublishedAt = item.PublishedAt.Format("2006-01-02")
|
||||
}
|
||||
items = append(items, si)
|
||||
}
|
||||
|
||||
result := searchToolResult{
|
||||
Tool: "web_search",
|
||||
Query: query,
|
||||
Count: len(items),
|
||||
Items: items,
|
||||
}
|
||||
|
||||
out, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(`{"tool":"web_search","error":"序列化结果失败:%s"}`, err.Error())
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// FetchToolHandler web_fetch 工具 handler。
|
||||
//
|
||||
// 职责:
|
||||
// 1. 解析 args(url / max_chars);
|
||||
// 2. 调用 Fetcher 抓取并清洗正文;
|
||||
// 3. 组装 JSON observation 返回给模型。
|
||||
type FetchToolHandler struct {
|
||||
fetcher *Fetcher
|
||||
}
|
||||
|
||||
// NewFetchToolHandler 创建 web_fetch 工具 handler。
|
||||
func NewFetchToolHandler(fetcher *Fetcher) *FetchToolHandler {
|
||||
return &FetchToolHandler{fetcher: fetcher}
|
||||
}
|
||||
|
||||
// fetchToolResult web_fetch 工具的输出结构。
|
||||
type fetchToolResult struct {
|
||||
Tool string `json:"tool"`
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Truncated bool `json:"truncated"`
|
||||
}
|
||||
|
||||
// Handle 执行 web_fetch 工具。
|
||||
//
|
||||
// 1. 解析参数,url 为必填;
|
||||
// 2. max_chars 可选,为 0 时使用 Fetcher 默认值(4000);
|
||||
// 3. 所有失败场景返回结构化错误 observation,不 panic。
|
||||
func (h *FetchToolHandler) Handle(args map[string]any) string {
|
||||
// 1. fetcher 为 nil 说明未初始化。
|
||||
if h.fetcher == nil {
|
||||
return `{"tool":"web_fetch","error":"抓取服务暂未初始化,请跳过 web_fetch 继续执行。"}`
|
||||
}
|
||||
|
||||
// 2. 提取必填参数 url。
|
||||
url, _ := args["url"].(string)
|
||||
url = strings.TrimSpace(url)
|
||||
if url == "" {
|
||||
return `{"tool":"web_fetch","error":"参数错误:缺少必填参数 url。"}`
|
||||
}
|
||||
|
||||
// 3. 提取可选参数 max_chars,覆盖 Fetcher 默认值。
|
||||
maxChars := 0
|
||||
if v, ok := args["max_chars"].(float64); ok {
|
||||
maxChars = int(v)
|
||||
}
|
||||
|
||||
// 4. 若调用方指定 max_chars,临时覆盖 Fetcher 配置。
|
||||
savedMaxChars := h.fetcher.MaxChars
|
||||
if maxChars > 0 {
|
||||
h.fetcher.MaxChars = maxChars
|
||||
}
|
||||
defer func() {
|
||||
h.fetcher.MaxChars = savedMaxChars
|
||||
}()
|
||||
|
||||
// 5. 构建带超时的 context。
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 6. 调用 Fetcher。
|
||||
start := time.Now()
|
||||
result, err := h.fetcher.Fetch(ctx, url)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
log.Printf("[web_fetch] url=%q elapsed=%s truncated=%v err=%v", url, elapsed, result != nil && result.Truncated, err)
|
||||
|
||||
if err != nil {
|
||||
// 7. 失败时返回可恢复 observation。
|
||||
return fmt.Sprintf(`{"tool":"web_fetch","error":"抓取失败:%s","url":%q}`, err.Error(), url)
|
||||
}
|
||||
|
||||
// 8. 组装输出 JSON。
|
||||
out := fetchToolResult{
|
||||
Tool: "web_fetch",
|
||||
URL: url,
|
||||
Title: result.Title,
|
||||
Content: result.Content,
|
||||
Truncated: result.Truncated,
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(`{"tool":"web_fetch","error":"序列化结果失败:%s"}`, err.Error())
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream"
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
|
||||
agentchat "github.com/LoveLosita/smartflow/backend/agent/chat"
|
||||
@@ -223,8 +224,8 @@ func (s *AgentService) runNewAgentGraph(
|
||||
// 这些消息不会出现在 Redis LLM 历史缓存中;
|
||||
// 2. 恢复场景(confirm/ask_user)必须使用快照中的 ConversationContext,否则工具结果丢失,
|
||||
// 导致后续 LLM 调用收到非法的裸 Tool 消息,API 拒绝请求、连接断开。
|
||||
func (s *AgentService) loadOrCreateRuntimeState(ctx context.Context, chatID string, userID int) (*newagentmodel.AgentRuntimeState, *newagentmodel.ConversationContext, *newagenttools.ScheduleState, *newagenttools.ScheduleState) {
|
||||
newRT := func() (*newagentmodel.AgentRuntimeState, *newagentmodel.ConversationContext, *newagenttools.ScheduleState, *newagenttools.ScheduleState) {
|
||||
func (s *AgentService) loadOrCreateRuntimeState(ctx context.Context, chatID string, userID int) (*newagentmodel.AgentRuntimeState, *newagentmodel.ConversationContext, *schedule.ScheduleState, *schedule.ScheduleState) {
|
||||
newRT := func() (*newagentmodel.AgentRuntimeState, *newagentmodel.ConversationContext, *schedule.ScheduleState, *schedule.ScheduleState) {
|
||||
rt := newagentmodel.NewAgentRuntimeState(nil)
|
||||
cs := rt.EnsureCommonState()
|
||||
cs.UserID = userID
|
||||
@@ -528,7 +529,7 @@ func (s *AgentService) makeWriteSchedulePreviewFunc() newagentmodel.WriteSchedul
|
||||
if s.cacheDAO == nil {
|
||||
return nil
|
||||
}
|
||||
return func(ctx context.Context, state *newagenttools.ScheduleState, userID int, conversationID string, taskClassIDs []int) error {
|
||||
return func(ctx context.Context, state *schedule.ScheduleState, userID int, conversationID string, taskClassIDs []int) error {
|
||||
stateDigest := summarizeScheduleStateForPreviewDebug(state)
|
||||
preview := newagentconv.ScheduleStateToPreview(state, userID, conversationID, taskClassIDs, "")
|
||||
if preview == nil {
|
||||
@@ -549,7 +550,7 @@ func (s *AgentService) makeWriteSchedulePreviewFunc() newagentmodel.WriteSchedul
|
||||
}
|
||||
|
||||
// summarizeScheduleStateForPreviewDebug 统计 Deliver 写预览前的内存日程摘要。
|
||||
func summarizeScheduleStateForPreviewDebug(state *newagenttools.ScheduleState) string {
|
||||
func summarizeScheduleStateForPreviewDebug(state *schedule.ScheduleState) string {
|
||||
if state == nil {
|
||||
return "state=nil"
|
||||
}
|
||||
@@ -565,11 +566,11 @@ func summarizeScheduleStateForPreviewDebug(state *newagenttools.ScheduleState) s
|
||||
hasSlot := len(t.Slots) > 0
|
||||
|
||||
switch {
|
||||
case newagenttools.IsPendingTask(*t):
|
||||
case schedule.IsPendingTask(*t):
|
||||
pendingTotal++
|
||||
case newagenttools.IsSuggestedTask(*t):
|
||||
case schedule.IsSuggestedTask(*t):
|
||||
suggestedTotal++
|
||||
case newagenttools.IsExistingTask(*t):
|
||||
case schedule.IsExistingTask(*t):
|
||||
existingTotal++
|
||||
}
|
||||
if hasSlot {
|
||||
|
||||
Reference in New Issue
Block a user