Files
smartmate/backend/agent2/model/schedule_refine.go
Losita a243154e23 Version: 0.7.9.dev.260326
后端:
1.把最后一块拼图:schedule_refine也搬迁到了agent2,此时agent已经完全解耦。但是它没融入新架构,Codex只尝试把它调整了一部分,回退了一些错误的更改,保持着现在的可运行状态。下次继续改。
2.agent目录先保留,直到refine彻底融入新架构。
3.改善Codex主导的新史山结构:node文件夹里面大量文件,转而改成了module.go+module_tool.go的双文件格局,极大提升架构整洁度和代码可读性。
前端:
1.新开了日历界面,正在保持往前推进。做了很多更改,感觉越来越好了。
2026-03-26 00:38:17 +08:00

345 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package agentmodel
import (
"sort"
"strings"
"time"
agentshared "github.com/LoveLosita/smartflow/backend/agent2/shared"
"github.com/LoveLosita/smartflow/backend/model"
)
const (
datetimeLayout = agentshared.MinuteLayout
ScheduleRefineDefaultPlanMax = 2
ScheduleRefineDefaultExecuteMax = 24
ScheduleRefineDefaultPerTaskBudget = 4
ScheduleRefineDefaultReplanMax = 2
ScheduleRefineDefaultCompositeRetry = 2
ScheduleRefineDefaultRepairReserve = 1
)
const (
defaultPlanMax = ScheduleRefineDefaultPlanMax
defaultExecuteMax = ScheduleRefineDefaultExecuteMax
defaultPerTaskBudget = ScheduleRefineDefaultPerTaskBudget
defaultReplanMax = ScheduleRefineDefaultReplanMax
defaultCompositeRetry = ScheduleRefineDefaultCompositeRetry
defaultRepairReserve = ScheduleRefineDefaultRepairReserve
)
// RefineContract 琛ㄧず鏈疆寰皟鎰忓浘濂戠害銆?
type RefineContract struct {
Intent string `json:"intent"`
Strategy string `json:"strategy"`
HardRequirements []string `json:"hard_requirements"`
HardAssertions []RefineAssertion `json:"hard_assertions,omitempty"`
KeepRelativeOrder bool `json:"keep_relative_order"`
OrderScope string `json:"order_scope"`
}
// RefineAssertion 琛ㄧず鍙敱鍚庣鐩存帴鍒ゅ畾鐨勭粨鏋勫寲纭柇瑷€銆?
//
// 瀛楁璇存槑锛?
// 1. Metric锛氭柇瑷€鎸囨爣鍚嶏紝渚嬪 source_move_ratio_percent锛?
// 2. Operator锛氭瘮杈冩搷浣滅锛屾敮鎸?== / <= / >= / between锛?
// 3. Value/Min/Max锛氶槇鍊硷紱
// 4. Week/TargetWeek锛氬彲閫夊懆娆笂涓嬫枃銆?
type RefineAssertion struct {
Metric string `json:"metric"`
Operator string `json:"operator"`
Value int `json:"value,omitempty"`
Min int `json:"min,omitempty"`
Max int `json:"max,omitempty"`
Week int `json:"week,omitempty"`
TargetWeek int `json:"target_week,omitempty"`
}
// HardCheckReport 琛ㄧず缁堝纭牎楠岀粨鏋溿€?
type HardCheckReport struct {
PhysicsPassed bool `json:"physics_passed"`
PhysicsIssues []string `json:"physics_issues,omitempty"`
IntentPassed bool `json:"intent_passed"`
IntentReason string `json:"intent_reason,omitempty"`
IntentUnmet []string `json:"intent_unmet,omitempty"`
OrderPassed bool `json:"order_passed"`
OrderIssues []string `json:"order_issues,omitempty"`
RepairTried bool `json:"repair_tried"`
}
// ReactRoundObservation 璁板綍姣忚疆 ReAct 鐨勫叧閿瀵熴€?
type ReactRoundObservation struct {
Round int `json:"round"`
GoalCheck string `json:"goal_check,omitempty"`
Decision string `json:"decision,omitempty"`
ToolName string `json:"tool_name,omitempty"`
ToolParams map[string]any `json:"tool_params,omitempty"`
ToolSuccess bool `json:"tool_success"`
ToolErrorCode string `json:"tool_error_code,omitempty"`
ToolResult string `json:"tool_result,omitempty"`
Reflect string `json:"reflect,omitempty"`
}
// PlannerPlan 琛ㄧず Planner 鐢熸垚鐨勯樁娈垫墽琛岃鍒掋€?
type PlannerPlan struct {
Summary string `json:"summary"`
Steps []string `json:"steps,omitempty"`
}
// RefineSlicePlan 琛ㄧず鍒囩墖鑺傜偣杈撳嚭銆?
type RefineSlicePlan struct {
WeekFilter []int `json:"week_filter,omitempty"`
SourceDays []int `json:"source_days,omitempty"`
TargetDays []int `json:"target_days,omitempty"`
ExcludeSections []int `json:"exclude_sections,omitempty"`
Reason string `json:"reason,omitempty"`
}
// RefineObjective 琛ㄧず鈥滃彲鎵ц涓斿彲鏍¢獙鈥濈殑鐩爣绾︽潫銆?
//
// 璁捐璇存槑锛?
// 1. 鐢?contract/slice 浠庤嚜鐒惰瑷€缂栬瘧寰楀埌锛?
// 2. 鎵ц闃舵锛坉one 鏀跺彛锛変笌缁堝闃舵锛坔ard_check锛夊叡鐢ㄥ悓涓€浠界害鏉燂紱
// 3. 閬垮厤鈥滄墽琛岄€昏緫涓庣粓瀹¢€昏緫鍚勮鍚勮瘽鈥濄€?
type RefineObjective struct {
Mode string `json:"mode,omitempty"` // none | move_all | move_ratio
SourceWeeks []int `json:"source_weeks,omitempty"`
TargetWeeks []int `json:"target_weeks,omitempty"`
SourceDays []int `json:"source_days,omitempty"`
TargetDays []int `json:"target_days,omitempty"`
ExcludeSections []int `json:"exclude_sections,omitempty"`
BaselineSourceTaskCount int `json:"baseline_source_task_count,omitempty"`
RequiredMoveMin int `json:"required_move_min,omitempty"`
RequiredMoveMax int `json:"required_move_max,omitempty"`
Reason string `json:"reason,omitempty"`
}
// ScheduleRefineState 鏄繛缁井璋冨浘鐨勭粺涓€鐘舵€併€?
type ScheduleRefineState struct {
// 1) 璇锋眰涓婁笅鏂?
TraceID string
UserID int
ConversationID string
UserMessage string
RequestNow time.Time
RequestNowText string
// 2) 缁ф壙鑷瑙堝揩鐓х殑鏁版嵁
TaskClassIDs []int
Constraints []string
// InitialHybridEntries 淇濆瓨鏈疆寰皟寮€濮嬪墠鐨勫熀绾匡紝鐢ㄤ簬缁堝鍋氣€滃墠鍚庡姣斺€濄€?
// 璇存槑锛?
// 1. 鍙璇箟锛屼笉鍙備笌鎵ц鏈熸敼鍐欙紱
// 2. 缁堝鍙熀浜庡畠鍒ゆ柇鈥滄潵婧愪换鍔℃槸鍚︾湡姝h縼绉诲埌鐩爣鍖哄煙鈥濄€?
InitialHybridEntries []model.HybridScheduleEntry
HybridEntries []model.HybridScheduleEntry
AllocatedItems []model.TaskClassItem
CandidatePlans []model.UserWeekSchedule
// 3) 鏈疆鎵ц鐘舵€?
UserIntent string
Contract RefineContract
PlanMax int
PerTaskBudget int
ExecuteMax int
ReplanMax int
// CompositeRetryMax 琛ㄧず澶嶅悎璺敱澶辫触鍚庣殑鏈€澶ч噸璇曟鏁帮紙涓嶅惈棣栨灏濊瘯锛夈€?
CompositeRetryMax int
PlanUsed int
ReplanUsed int
MaxRounds int
RepairReserve int
RoundUsed int
ActionLogs []string
ConsecutiveFailures int
ThinkingBoostArmed bool
ObservationHistory []ReactRoundObservation
CurrentPlan PlannerPlan
BatchMoveAllowed bool
// DisableCompositeTools=true 琛ㄧず宸茶繘鍏?ReAct 鍏滃簳锛岀姝㈠啀璋冪敤澶嶅悎宸ュ叿銆?
DisableCompositeTools bool
// CompositeRouteTried 鏍囪鏄惁灏濊瘯杩団€滃鍚堟壒澶勭悊璺敱鈥濄€?
CompositeRouteTried bool
// CompositeRouteSucceeded 鏍囪澶嶅悎鎵瑰鐞嗚矾鐢辨槸鍚﹀凡瀹屾垚鈥滃鍚堝垎鏀嚭绔欌€濄€?
//
// 璇存槑锛?
// 1. true 琛ㄧず褰撳墠閾捐矾鍙互璺宠繃 ReAct 鍏滃簳锛岀洿鎺ヨ繘鍏?hard_check锛?
// 2. 瀹冧笉绛変环浜庘€滅粓瀹″凡閫氳繃鈥濓紝缁堝鏄惁閫氳繃浠嶄互鍚庣画 HardCheck 缁撴灉涓哄噯锛?
// 3. 杩欐牱鍖哄垎鏄负浜嗛伩鍏嶁€滃鍚堝伐鍏峰凡鎴愬姛鎵ц锛屼絾涓氬姟鐩爣瑕佺瓑缁堝瑁佸喅鈥濇椂琚鍒や负澶辫触銆?
CompositeRouteSucceeded bool
TaskActionUsed map[int]int
EntriesVersion int
SeenSlotQueries map[string]struct{}
// RequiredCompositeTool 琛ㄧず鏈疆绛栫暐瑕佹眰鈥滃繀椤昏嚦灏戞垚鍔熶竴娆♀€濈殑澶嶅悎宸ュ叿銆?
// 鍙栧€肩害瀹氾細"" | "SpreadEven" | "MinContextSwitch"銆?
RequiredCompositeTool string
// CompositeToolCalled 璁板綍澶嶅悎宸ュ叿鏄惁鑷冲皯璋冪敤杩囦竴娆★紙涓嶅尯鍒嗘垚鍔熷け璐ワ級銆?
CompositeToolCalled map[string]bool
// CompositeToolSuccess 璁板綍澶嶅悎宸ュ叿鏄惁鑷冲皯鎴愬姛杩囦竴娆°€?
CompositeToolSuccess map[string]bool
SlicePlan RefineSlicePlan
Objective RefineObjective
WorksetTaskIDs []int
WorksetCursor int
CurrentTaskID int
CurrentTaskAttempt int
LastFailedCallSignature string
OriginOrderMap map[int]int
// 4) 缁堝鐘舵€?
HardCheck HardCheckReport
// 5) 鏈€缁堣緭鍑?
FinalSummary string
Completed bool
}
// NewScheduleRefineState 鍩轰簬涓婁竴鐗堥瑙堝揩鐓у垵濮嬪寲鐘舵€併€?
//
// 鑱岃矗杈圭晫锛?
// 1. 璐熻矗鍒濆鍖栭绠椼€佷笂涓嬫枃瀛楁涓庡彲鍙樼姸鎬佸鍣紱
// 2. 璐熻矗鎷疯礉 preview 鏁版嵁锛岄伩鍏嶈法璇锋眰寮曠敤姹℃煋锛?
// 3. 涓嶈礋璐e仛浠讳綍璋冨害鍔ㄤ綔銆?
func NewScheduleRefineState(traceID string, userID int, conversationID string, userMessage string, preview *model.SchedulePlanPreviewCache) *ScheduleRefineState {
now := nowToMinute()
st := &ScheduleRefineState{
TraceID: strings.TrimSpace(traceID),
UserID: userID,
ConversationID: strings.TrimSpace(conversationID),
UserMessage: strings.TrimSpace(userMessage),
RequestNow: now,
RequestNowText: now.In(loadLocation()).Format(datetimeLayout),
PlanMax: defaultPlanMax,
PerTaskBudget: defaultPerTaskBudget,
ExecuteMax: defaultExecuteMax,
ReplanMax: defaultReplanMax,
CompositeRetryMax: defaultCompositeRetry,
RepairReserve: defaultRepairReserve,
MaxRounds: defaultExecuteMax + defaultRepairReserve,
ActionLogs: make([]string, 0, 32),
ObservationHistory: make([]ReactRoundObservation, 0, 24),
TaskActionUsed: make(map[int]int),
SeenSlotQueries: make(map[string]struct{}),
OriginOrderMap: make(map[int]int),
CompositeToolCalled: map[string]bool{
"SpreadEven": false,
"MinContextSwitch": false,
},
CompositeToolSuccess: map[string]bool{
"SpreadEven": false,
"MinContextSwitch": false,
},
CurrentPlan: PlannerPlan{
Summary: "initialized, waiting for planner output",
},
SlicePlan: RefineSlicePlan{
Reason: "灏氭湭鍒囩墖",
},
}
if preview == nil {
return st
}
st.TaskClassIDs = append([]int(nil), preview.TaskClassIDs...)
st.InitialHybridEntries = cloneHybridEntries(preview.HybridEntries)
st.HybridEntries = cloneHybridEntries(preview.HybridEntries)
st.AllocatedItems = cloneTaskClassItems(preview.AllocatedItems)
st.CandidatePlans = cloneWeekSchedules(preview.CandidatePlans)
st.OriginOrderMap = buildOriginOrderMap(st.HybridEntries)
return st
}
func loadLocation() *time.Location {
return agentshared.ShanghaiLocation()
}
func nowToMinute() time.Time {
return agentshared.NowToMinute()
}
func cloneHybridEntries(src []model.HybridScheduleEntry) []model.HybridScheduleEntry {
return agentshared.CloneHybridEntries(src)
}
func cloneTaskClassItems(src []model.TaskClassItem) []model.TaskClassItem {
return agentshared.CloneTaskClassItems(src)
}
func cloneWeekSchedules(src []model.UserWeekSchedule) []model.UserWeekSchedule {
return agentshared.CloneWeekSchedules(src)
}
// buildOriginOrderMap 鏋勫缓 suggested 浠诲姟鐨勫垵濮嬮『搴忓熀绾匡紙task_item_id -> rank锛夈€?
func buildOriginOrderMap(entries []model.HybridScheduleEntry) map[int]int {
orderMap := make(map[int]int)
if len(entries) == 0 {
return orderMap
}
suggested := make([]model.HybridScheduleEntry, 0, len(entries))
for _, entry := range entries {
if isMovableSuggestedTask(entry) {
suggested = append(suggested, entry)
}
}
sort.SliceStable(suggested, func(i, j int) bool {
left := suggested[i]
right := suggested[j]
if left.Week != right.Week {
return left.Week < right.Week
}
if left.DayOfWeek != right.DayOfWeek {
return left.DayOfWeek < right.DayOfWeek
}
if left.SectionFrom != right.SectionFrom {
return left.SectionFrom < right.SectionFrom
}
if left.SectionTo != right.SectionTo {
return left.SectionTo < right.SectionTo
}
return left.TaskItemID < right.TaskItemID
})
for i, entry := range suggested {
orderMap[entry.TaskItemID] = i + 1
}
return orderMap
}
// FinalHardCheckPassed 鍒ゆ柇鈥滄渶缁堢粓瀹♀€濇槸鍚︽暣浣撻€氳繃銆?
//
// 鑱岃矗杈圭晫锛?
// 1. 璐熻矗鑱氬悎 physics/order/intent 涓夌被纭牎楠岀粨鏋滐紝缁欐湇鍔″眰涓庢€荤粨闃舵缁熶竴澶嶇敤锛?
// 2. 涓嶈礋璐hЕ鍙戠粓瀹★紝涔熶笉璐熻矗鎺ㄥ淇鍔ㄤ綔锛?
// 3. nil state 瑙嗕负鏈€氳繃锛岄伩鍏嶄笂灞傛妸缂哄け缁撴灉璇垽涓烘垚鍔熴€?
func FinalHardCheckPassed(st *ScheduleRefineState) bool {
if st == nil {
return false
}
return st.HardCheck.PhysicsPassed && st.HardCheck.OrderPassed && st.HardCheck.IntentPassed
}
func isMovableSuggestedTask(entry model.HybridScheduleEntry) bool {
if strings.TrimSpace(entry.Status) != "suggested" || entry.TaskItemID <= 0 {
return false
}
if strings.EqualFold(strings.TrimSpace(entry.Type), "course") {
return false
}
return true
}