diff --git a/backend/cmd/start.go b/backend/cmd/start.go index 024c138..32aed24 100644 --- a/backend/cmd/start.go +++ b/backend/cmd/start.go @@ -182,60 +182,57 @@ func Start() { agentService.SetToolRegistry(newagenttools.NewDefaultRegistryWithDeps(newagenttools.DefaultRegistryDeps{ RAGRuntime: ragRuntime, WebSearchProvider: webSearchProvider, - QuickNote: newagenttools.QuickNoteDeps{ - CreateTask: func(userID int, title string, priorityGroup int, deadlineAt *time.Time) (int, error) { - // 调用目的:随口记工具通过此闭包写库,捕获 start 层 taskRepo 实例。 - created, err := taskRepo.AddTask(&model.Task{ - UserID: userID, - Title: title, - Priority: priorityGroup, - IsCompleted: false, - DeadlineAt: deadlineAt, - }) - if err != nil { - return 0, err - } - return created.ID, nil - }, - }, - TaskQuery: newagenttools.TaskQueryDeps{ - // 调用目的:桥接新工具参数到旧 service 层查询能力,复用已有的过滤/排序/紧急度提升逻辑。 - QueryTasks: func(ctx context.Context, userID int, params newagenttools.TaskQueryParams) ([]newagenttools.TaskQueryResult, error) { - req := newagentmodel.TaskQueryRequest{ - UserID: userID, - Quadrant: params.Quadrant, - SortBy: params.SortBy, - Order: params.Order, - Limit: params.Limit, - IncludeCompleted: params.IncludeCompleted, - Keyword: params.Keyword, - DeadlineBefore: params.DeadlineBefore, - DeadlineAfter: params.DeadlineAfter, - } - records, err := agentService.QueryTasksForTool(ctx, req) - if err != nil { - return nil, err - } - results := make([]newagenttools.TaskQueryResult, 0, len(records)) - for _, r := range records { - deadlineStr := "" - if r.DeadlineAt != nil { - deadlineStr = r.DeadlineAt.In(time.Local).Format("2006-01-02 15:04") - } - results = append(results, newagenttools.TaskQueryResult{ - ID: r.ID, - Title: r.Title, - PriorityGroup: r.PriorityGroup, - IsCompleted: r.IsCompleted, - DeadlineAt: deadlineStr, - }) - } - return results, nil - }, - }, })) agentService.SetScheduleProvider(newagentconv.NewScheduleProvider(scheduleRepo, taskClassRepo)) agentService.SetCompactionStore(agentRepo) + agentService.SetQuickTaskDeps(newagentmodel.QuickTaskDeps{ + CreateTask: func(userID int, title string, priorityGroup int, deadlineAt *time.Time, urgencyThresholdAt *time.Time) (int, error) { + created, err := taskRepo.AddTask(&model.Task{ + UserID: userID, + Title: title, + Priority: priorityGroup, + IsCompleted: false, + DeadlineAt: deadlineAt, + UrgencyThresholdAt: urgencyThresholdAt, + }) + if err != nil { + return 0, err + } + return created.ID, nil + }, + QueryTasks: func(ctx context.Context, userID int, params newagenttools.TaskQueryParams) ([]newagenttools.TaskQueryResult, error) { + req := newagentmodel.TaskQueryRequest{ + UserID: userID, + Quadrant: params.Quadrant, + SortBy: params.SortBy, + Order: params.Order, + Limit: params.Limit, + IncludeCompleted: params.IncludeCompleted, + Keyword: params.Keyword, + DeadlineBefore: params.DeadlineBefore, + DeadlineAfter: params.DeadlineAfter, + } + records, err := agentService.QueryTasksForTool(ctx, req) + if err != nil { + return nil, err + } + results := make([]newagenttools.TaskQueryResult, 0, len(records)) + for _, r := range records { + deadlineStr := "" + if r.DeadlineAt != nil { + deadlineStr = r.DeadlineAt.In(time.Local).Format("2006-01-02 15:04") + } + results = append(results, newagenttools.TaskQueryResult{ + ID: r.ID, + Title: r.Title, + PriorityGroup: r.PriorityGroup, + IsCompleted: r.IsCompleted, + DeadlineAt: deadlineStr, + }) + } + return results, nil + }, + }) agentService.SetMemoryReader(memoryModule, memoryCfg) // API 层初始化。 diff --git a/backend/conv/task.go b/backend/conv/task.go index 5ba5c4f..e9705c0 100644 --- a/backend/conv/task.go +++ b/backend/conv/task.go @@ -43,14 +43,20 @@ func ModelToGetUserTasksResp(tasks []model.Task) []model.GetUserTaskResp { deadline = task.DeadlineAt.Format("2006-01-02 15:04:05") } + urgencyThreshold := "" + if task.UrgencyThresholdAt != nil { + urgencyThreshold = task.UrgencyThresholdAt.Format("2006-01-02 15:04:05") + } + resp = append(resp, model.GetUserTaskResp{ - ID: task.ID, - UserID: task.UserID, - Title: task.Title, - PriorityGroup: task.Priority, - Status: status, - Deadline: deadline, - IsCompleted: task.IsCompleted, + ID: task.ID, + UserID: task.UserID, + Title: task.Title, + PriorityGroup: task.Priority, + Status: status, + Deadline: deadline, + IsCompleted: task.IsCompleted, + UrgencyThresholdAt: urgencyThreshold, }) } return resp @@ -66,13 +72,18 @@ func ModelToGetUserTaskResp(task *model.Task) model.GetUserTaskResp { if task.DeadlineAt != nil { deadline = task.DeadlineAt.Format("2006-01-02 15:04:05") } + urgencyThreshold := "" + if task.UrgencyThresholdAt != nil { + urgencyThreshold = task.UrgencyThresholdAt.Format("2006-01-02 15:04:05") + } return model.GetUserTaskResp{ - ID: task.ID, - UserID: task.UserID, - Title: task.Title, - PriorityGroup: task.Priority, - Status: status, - Deadline: deadline, - IsCompleted: task.IsCompleted, + ID: task.ID, + UserID: task.UserID, + Title: task.Title, + PriorityGroup: task.Priority, + Status: status, + Deadline: deadline, + IsCompleted: task.IsCompleted, + UrgencyThresholdAt: urgencyThreshold, } } diff --git a/backend/model/task.go b/backend/model/task.go index 31b116b..51efd48 100644 --- a/backend/model/task.go +++ b/backend/model/task.go @@ -103,13 +103,14 @@ type UserUndoCompleteTaskResponse struct { } type GetUserTaskResp struct { - ID int `json:"id"` - UserID int `json:"user_id"` - Title string `json:"title"` - PriorityGroup int `json:"priority_group"` - Status string `json:"status"` - Deadline string `json:"deadline"` - IsCompleted bool `json:"is_completed"` + ID int `json:"id"` + UserID int `json:"user_id"` + Title string `json:"title"` + PriorityGroup int `json:"priority_group"` + Status string `json:"status"` + Deadline string `json:"deadline"` + IsCompleted bool `json:"is_completed"` + UrgencyThresholdAt string `json:"urgency_threshold_at,omitempty"` } // UserUpdateTaskRequest 是"更新任务属性"接口的请求体。 diff --git a/backend/newAgent/graph/common_graph.go b/backend/newAgent/graph/common_graph.go index be8c064..3b3705f 100644 --- a/backend/newAgent/graph/common_graph.go +++ b/backend/newAgent/graph/common_graph.go @@ -20,6 +20,7 @@ const ( NodeOrderGuard = "order_guard" NodeInterrupt = "interrupt" NodeDeliver = "deliver" + NodeQuickTask = "quick_task" ) func RunAgentGraph(ctx context.Context, input newagentmodel.AgentGraphRunInput) (*newagentmodel.AgentGraphState, error) { @@ -55,6 +56,9 @@ func RunAgentGraph(ctx context.Context, input newagentmodel.AgentGraphRunInput) if err := g.AddLambdaNode(NodeOrderGuard, compose.InvokableLambda(nodes.OrderGuard)); err != nil { return nil, err } + if err := g.AddLambdaNode(NodeQuickTask, compose.InvokableLambda(nodes.QuickTask)); err != nil { + return nil, err + } if err := g.AddLambdaNode(NodeInterrupt, compose.InvokableLambda(nodes.Interrupt)); err != nil { return nil, err } @@ -68,7 +72,7 @@ func RunAgentGraph(ctx context.Context, input newagentmodel.AgentGraphRunInput) if err := g.AddEdge(compose.START, NodeChat); err != nil { return nil, err } - // Chat -> END / Plan / Confirm / RoughBuild / Execute / Deliver / Interrupt + // Chat -> END / Plan / Confirm / RoughBuild / Execute / QuickTask / Deliver / Interrupt if err := g.AddBranch(NodeChat, compose.NewGraphBranch( branchAfterChat, map[string]bool{ @@ -76,6 +80,7 @@ func RunAgentGraph(ctx context.Context, input newagentmodel.AgentGraphRunInput) NodeConfirm: true, NodeRoughBuild: true, NodeExecute: true, + NodeQuickTask: true, NodeDeliver: true, NodeInterrupt: true, compose.END: true, @@ -150,6 +155,10 @@ func RunAgentGraph(ctx context.Context, input newagentmodel.AgentGraphRunInput) if err := g.AddEdge(NodeDeliver, compose.END); err != nil { return nil, err } + // QuickTask -> END:轻量路径,直接返回结果。 + if err := g.AddEdge(NodeQuickTask, compose.END); err != nil { + return nil, err + } // --- 编译运行 --- maxSteps := flowState.MaxRounds + 10 @@ -186,6 +195,8 @@ func branchAfterChat(_ context.Context, st *newagentmodel.AgentGraphState) (stri return NodePlan, nil case newagentmodel.PhaseWaitingConfirm: return NodeConfirm, nil + case newagentmodel.PhaseQuickTask: + return NodeQuickTask, nil case newagentmodel.PhaseExecuting: if flowState.NeedsRoughBuild && st.Deps.RoughBuildFunc != nil { return NodeRoughBuild, nil diff --git a/backend/newAgent/model/chat_contract.go b/backend/newAgent/model/chat_contract.go index 00ff2b8..07fe5f6 100644 --- a/backend/newAgent/model/chat_contract.go +++ b/backend/newAgent/model/chat_contract.go @@ -20,6 +20,9 @@ const ( // ChatRoutePlan 复杂规划:需要先制定计划,进 Plan 节点。 ChatRoutePlan ChatRoute = "plan" + + // ChatRouteQuickTask 快捷任务:随口记增查改删等轻量任务操作,走 QuickTask 轻量路径。 + ChatRouteQuickTask ChatRoute = "quick_task" ) // ChatRoutingDecision 是 Chat 节点单次路由决策的结构化输出。 @@ -59,7 +62,7 @@ func (d *ChatRoutingDecision) Validate() error { d.Normalize() switch d.Route { - case ChatRouteDirectReply, ChatRouteExecute, ChatRouteDeepAnswer, ChatRoutePlan: + case ChatRouteDirectReply, ChatRouteExecute, ChatRouteDeepAnswer, ChatRoutePlan, ChatRouteQuickTask: // ok case "": return fmt.Errorf("chat routing decision.route 不能为空") diff --git a/backend/newAgent/model/common_state.go b/backend/newAgent/model/common_state.go index c7241fd..3099ef2 100644 --- a/backend/newAgent/model/common_state.go +++ b/backend/newAgent/model/common_state.go @@ -13,6 +13,7 @@ const ( PhasePlanning Phase = "planning" PhaseWaitingConfirm Phase = "waiting_confirm" PhaseExecuting Phase = "executing" + PhaseQuickTask Phase = "quick_task" PhaseDone Phase = "done" ) diff --git a/backend/newAgent/model/graph_run_state.go b/backend/newAgent/model/graph_run_state.go index b0dc374..cf82c04 100644 --- a/backend/newAgent/model/graph_run_state.go +++ b/backend/newAgent/model/graph_run_state.go @@ -96,6 +96,21 @@ type AgentGraphDeps struct { // PersistVisibleMessage 按 Service 注入,newAgent 每个节点产出的可见 speak // 都会在 AppendHistory 之后立刻调用这个回调,把消息同步落到 Redis + MySQL。 PersistVisibleMessage PersistVisibleMessageFunc + + // QuickTaskDeps 快捷任务节点的直接依赖,绕过 ToolRegistry 走轻量路径。 + QuickTaskDeps QuickTaskDeps +} + +// QuickTaskDeps 描述快捷任务节点所需的服务层依赖。 +// +// 职责边界: +// 1. QuickTask 节点直接调这些函数,不经过 ToolRegistry,不走 ReAct 循环; +// 2. CreateTask 和 QueryTasks 的签名与 tools 包的 QuickNoteDeps / TaskQueryDeps 一致。 +type QuickTaskDeps struct { + // CreateTask 创建一条四象限任务,返回 task_id。 + CreateTask func(userID int, title string, priorityGroup int, deadlineAt *time.Time, urgencyThresholdAt *time.Time) (taskID int, err error) + // QueryTasks 按条件查询用户任务列表。 + QueryTasks func(ctx context.Context, userID int, params newagenttools.TaskQueryParams) ([]newagenttools.TaskQueryResult, error) } // --- 记忆 pinned block 常量(供 agentsvc 和 node 层共享) --- diff --git a/backend/newAgent/node/agent_nodes.go b/backend/newAgent/node/agent_nodes.go index 919f0f8..97889f4 100644 --- a/backend/newAgent/node/agent_nodes.go +++ b/backend/newAgent/node/agent_nodes.go @@ -198,6 +198,31 @@ func (n *AgentNodes) OrderGuard(ctx context.Context, st *newagentmodel.AgentGrap return st, nil } +// QuickTask 负责把 graph 的 quick_task 节点请求转给 RunQuickTaskNode。 +func (n *AgentNodes) QuickTask(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) { + if st == nil { + return nil, errors.New("quick_task node: state is nil") + } + + // QuickTask 不需要工具目录,直接复用 ChatClient。 + st.EnsureConversationContext().SetToolSchemas(nil) + + if err := RunQuickTaskNode(ctx, QuickTaskNodeInput{ + RuntimeState: st.EnsureRuntimeState(), + ConversationContext: st.EnsureConversationContext(), + UserInput: st.Request.UserInput, + Client: st.Deps.ResolveChatClient(), + ChunkEmitter: st.EnsureChunkEmitter(), + QuickTaskDeps: st.Deps.QuickTaskDeps, + PersistVisibleMessage: st.Deps.PersistVisibleMessage, + }); err != nil { + return nil, err + } + + saveAgentState(ctx, st) + return st, nil +} + // Deliver 负责把 graph 的 deliver 节点请求转给 RunDeliverNode。 func (n *AgentNodes) Deliver(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) { if st == nil { diff --git a/backend/newAgent/node/chat.go b/backend/newAgent/node/chat.go index 9cdc373..60b6e7f 100644 --- a/backend/newAgent/node/chat.go +++ b/backend/newAgent/node/chat.go @@ -244,6 +244,12 @@ func streamAndDispatch( case newagentmodel.ChatRoutePlan: return handleRoutePlanStream(reader, emitter, flowState, effectiveThinking, visible) + case newagentmodel.ChatRouteQuickTask: + // 关闭路由流,后续由 QuickTask 节点自行处理。 + _ = reader.Close() + flowState.Phase = newagentmodel.PhaseQuickTask + return nil + default: flowState.Phase = newagentmodel.PhasePlanning return nil diff --git a/backend/newAgent/node/execute.go b/backend/newAgent/node/execute.go index 14ae02c..ed41541 100644 --- a/backend/newAgent/node/execute.go +++ b/backend/newAgent/node/execute.go @@ -389,15 +389,6 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error { decision.Action = newagentmodel.ExecuteActionContinue } - // 随口记工具 speak 清空: - // 1. quick_note_create 是轻量记录操作,不需要 execute 阶段向用户输出任何文案; - // 2. 收口统一由 deliver 阶段完成,避免 execute + deliver 重复输出导致废话; - // 3. 后端强制清空兜底,即使 LLM 误填了 speak 也不会推流到前端。 - if decision.ToolCall != nil && strings.EqualFold(decision.ToolCall.Name, "quick_note_create") { - decision.Speak = "" - flowState.UsedQuickNote = true - } - // 自省校验:next_plan / done 必须附带 goal_check,否则不推进,追加修正让 LLM 重试。 if decision.Action == newagentmodel.ExecuteActionNextPlan || decision.Action == newagentmodel.ExecuteActionDone { @@ -2026,8 +2017,6 @@ func resolveToolDisplayNameCN(toolName string) string { "query_target_tasks": "查询目标任务", "query_available_slots": "查询可用时间段", "get_task_info": "查看任务详情", - "quick_note_create": "创建提醒任务", - "query_tasks": "查询任务列表", "web_search": "网页搜索", "web_fetch": "网页抓取", "move": "移动任务", diff --git a/backend/newAgent/node/quick_task.go b/backend/newAgent/node/quick_task.go new file mode 100644 index 0000000..fb93518 --- /dev/null +++ b/backend/newAgent/node/quick_task.go @@ -0,0 +1,328 @@ +package newagentnode + +import ( + "context" + "fmt" + "io" + "log" + "strings" + "time" + + infrallm "github.com/LoveLosita/smartflow/backend/infra/llm" + newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model" + newagentprompt "github.com/LoveLosita/smartflow/backend/newAgent/prompt" + newagentrouter "github.com/LoveLosita/smartflow/backend/newAgent/router" + newagentshared "github.com/LoveLosita/smartflow/backend/newAgent/shared" + newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream" + newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools" + "github.com/cloudwego/eino/schema" +) + +const ( + quickTaskStageName = "quick_task" + quickTaskBlockID = "qt_main" +) + +// QuickTaskNodeInput 描述快捷任务节点的输入。 +type QuickTaskNodeInput struct { + RuntimeState *newagentmodel.AgentRuntimeState + ConversationContext *newagentmodel.ConversationContext + UserInput string + Client *infrallm.Client + ChunkEmitter *newagentstream.ChunkEmitter + QuickTaskDeps newagentmodel.QuickTaskDeps + PersistVisibleMessage newagentmodel.PersistVisibleMessageFunc +} + +// quickTaskDecision 是从 LLM 输出中解析的结构化意图。 +type quickTaskDecision struct { + Action string `json:"action"` + Title string `json:"title,omitempty"` + DeadlineAt string `json:"deadline_at,omitempty"` + PriorityGroup *int `json:"priority_group,omitempty"` + UrgencyThresholdAt string `json:"urgency_threshold_at,omitempty"` + TaskID *int `json:"task_id,omitempty"` + + // query 参数 + Quadrant *int `json:"quadrant,omitempty"` + Keyword string `json:"keyword,omitempty"` + Limit *int `json:"limit,omitempty"` + + // ask 参数 + Question string `json:"question,omitempty"` +} + +// RunQuickTaskNode 执行快捷任务节点:流式 LLM 提取意图 → 直接调 service → 追加结果。 +func RunQuickTaskNode(ctx context.Context, input QuickTaskNodeInput) error { + flowState := input.RuntimeState.EnsureCommonState() + emitter := input.ChunkEmitter + + // 1. 构造 messages。 + messages := newagentprompt.BuildQuickTaskMessagesSimple(input.UserInput) + + // 2. 真流式调用 LLM。 + reader, err := input.Client.Stream(ctx, messages, infrallm.GenerateOptions{ + Temperature: 0.3, + MaxTokens: 512, + }) + if err != nil { + log.Printf("[WARN] quick_task: Stream 调用失败 chat=%s err=%v", flowState.ConversationID, err) + _ = emitter.EmitAssistantText(quickTaskBlockID, quickTaskStageName, "抱歉,处理任务时出了点问题,请重试。", true) + flowState.Phase = newagentmodel.PhaseDone + return nil + } + + // 3. 两阶段流式解析。 + parser := newagentrouter.NewStreamDecisionParser() + firstChunk := true + var decision *quickTaskDecision + var fullText strings.Builder + + // 阶段一:解析决策标签。 + for { + chunk, recvErr := reader.Recv() + if recvErr == io.EOF { + break + } + if recvErr != nil { + log.Printf("[WARN] quick_task stream recv error chat=%s err=%v", flowState.ConversationID, recvErr) + break + } + + content := "" + if chunk != nil { + content = chunk.Content + } + + visible, ready, _ := parser.Feed(content) + if !ready { + continue + } + + result := parser.Result() + + // Fallback / 解析失败:把原始文本当作纯回复推送。 + if result.Fallback || result.ParseFailed { + log.Printf("[DEBUG] quick_task: 标签解析失败 chat=%s raw=%s", flowState.ConversationID, result.RawBuffer) + if result.RawBuffer != "" { + _ = emitter.EmitAssistantText(quickTaskBlockID, quickTaskStageName, result.RawBuffer, firstChunk) + fullText.WriteString(result.RawBuffer) + } + break + } + + // 解析 JSON。 + log.Printf("[DEBUG] quick_task: LLM 原始决策 JSON chat=%s json=%s", flowState.ConversationID, result.DecisionJSON) + var parseErr error + decision, parseErr = infrallm.ParseJSONObject[quickTaskDecision](result.DecisionJSON) + if parseErr != nil { + log.Printf("[DEBUG] quick_task: JSON 解析失败 chat=%s json=%s", flowState.ConversationID, result.DecisionJSON) + if result.RawBuffer != "" { + _ = emitter.EmitAssistantText(quickTaskBlockID, quickTaskStageName, result.RawBuffer, firstChunk) + fullText.WriteString(result.RawBuffer) + } + break + } + log.Printf("[DEBUG] quick_task: 解析结果 chat=%s action=%s title=%s deadline_at=%s priority_group=%v urgency_threshold_at=%q", + flowState.ConversationID, decision.Action, decision.Title, decision.DeadlineAt, decision.PriorityGroup, decision.UrgencyThresholdAt) + + // 阶段二:流式推送标签后正文。 + if visible != "" { + if emitErr := emitter.EmitAssistantText(quickTaskBlockID, quickTaskStageName, visible, firstChunk); emitErr != nil { + log.Printf("[WARN] quick_task emit error chat=%s err=%v", flowState.ConversationID, emitErr) + } + fullText.WriteString(visible) + firstChunk = false + } + for { + chunk2, recvErr2 := reader.Recv() + if recvErr2 == io.EOF { + break + } + if recvErr2 != nil { + log.Printf("[WARN] quick_task stream error chat=%s err=%v", flowState.ConversationID, recvErr2) + break + } + if chunk2 == nil || chunk2.Content == "" { + continue + } + if emitErr := emitter.EmitAssistantText(quickTaskBlockID, quickTaskStageName, chunk2.Content, firstChunk); emitErr != nil { + log.Printf("[WARN] quick_task emit error chat=%s err=%v", flowState.ConversationID, emitErr) + } + fullText.WriteString(chunk2.Content) + firstChunk = false + } + break + } + + // 4. 流结束但未解析到决策 → 降级为纯文本回复。 + if decision == nil { + finalText := fullText.String() + if strings.TrimSpace(finalText) == "" { + _ = emitter.EmitAssistantText(quickTaskBlockID, quickTaskStageName, "抱歉,处理任务时出了点问题,请重试。", true) + } + msg := schema.AssistantMessage(finalText, nil) + input.ConversationContext.AppendHistory(msg) + persistVisibleAssistantMessage(ctx, input.PersistVisibleMessage, flowState, msg) + flowState.Phase = newagentmodel.PhaseDone + return nil + } + + log.Printf("[DEBUG] quick_task: chat=%s action=%s raw_title=%s", flowState.ConversationID, decision.Action, decision.Title) + + // 5. 根据意图执行操作。 + var resultText string + switch decision.Action { + case "create": + resultText = handleQuickTaskCreate(ctx, input, decision, flowState) + case "query": + resultText = handleQuickTaskQuery(ctx, input, decision, flowState) + case "ask": + resultText = decision.Question + if resultText == "" { + resultText = "你想记录什么呢?告诉我具体内容吧。" + } + default: + resultText = "抱歉,我没有理解你的意思。你可以试试说「记一下明天开会」或「看看我的任务」。" + } + + // 6. 追加操作结果文本。 + if resultText != "" { + _ = emitter.EmitAssistantText(quickTaskBlockID, quickTaskStageName, resultText, false) + fullText.WriteString(resultText) + } + + // 7. 写入对话历史。 + finalText := fullText.String() + msg := schema.AssistantMessage(finalText, nil) + input.ConversationContext.AppendHistory(msg) + persistVisibleAssistantMessage(ctx, input.PersistVisibleMessage, flowState, msg) + + flowState.Phase = newagentmodel.PhaseDone + return nil +} + +// handleQuickTaskCreate 处理任务创建。 +func handleQuickTaskCreate( + ctx context.Context, + input QuickTaskNodeInput, + decision *quickTaskDecision, + flowState *newagentmodel.CommonState, +) string { + title := strings.TrimSpace(decision.Title) + if title == "" { + return "你想记录什么呢?告诉我具体内容吧。" + } + + var deadline *time.Time + if raw := strings.TrimSpace(decision.DeadlineAt); raw != "" { + parsed, err := newagentshared.ParseOptionalDeadline(raw) + if err != nil { + return fmt.Sprintf("截止时间格式不太对(%s),不过我先把任务记下来啦。", err) + } + deadline = parsed + } + + priorityGroup := 0 + if decision.PriorityGroup != nil && newagentshared.IsValidTaskPriority(*decision.PriorityGroup) { + priorityGroup = *decision.PriorityGroup + } + if priorityGroup == 0 { + priorityGroup = quickNoteFallbackPriority(deadline) + } + + var urgencyThreshold *time.Time + if raw := strings.TrimSpace(decision.UrgencyThresholdAt); raw != "" { + parsed, err := newagentshared.ParseOptionalDeadline(raw) + if err == nil { + urgencyThreshold = parsed + } + } + // LLM 经常省略 urgency_threshold_at,代码兜底:priorityGroup=2 且有 deadline 时自动推算。 + if urgencyThreshold == nil && priorityGroup == 2 && deadline != nil { + fallback := deadline.Add(-24 * time.Hour) + urgencyThreshold = &fallback + } + + log.Printf("[DEBUG] quick_task: CreateTask 参数 chat=%s title=%s priorityGroup=%d deadline=%v urgencyThreshold=%v urgency_raw=%q", + flowState.ConversationID, title, priorityGroup, deadline, urgencyThreshold, decision.UrgencyThresholdAt) + _, err := input.QuickTaskDeps.CreateTask(flowState.UserID, title, priorityGroup, deadline, urgencyThreshold) + if err != nil { + return fmt.Sprintf("记录失败了(%s),稍后再试试?", err) + } + + flowState.UsedQuickNote = true + + priorityLabel := newagentshared.PriorityLabelCN(priorityGroup) + deadlineStr := "" + if deadline != nil { + deadlineStr = deadline.In(newagentshared.ShanghaiLocation()).Format("2006-01-02 15:04") + } + + if deadlineStr != "" { + return fmt.Sprintf("已记录:%s(%s,截止 %s)", title, priorityLabel, deadlineStr) + } + return fmt.Sprintf("已记录:%s(%s)", title, priorityLabel) +} + +// handleQuickTaskQuery 处理任务查询。 +func handleQuickTaskQuery( + ctx context.Context, + input QuickTaskNodeInput, + decision *quickTaskDecision, + flowState *newagentmodel.CommonState, +) string { + params := newagenttools.TaskQueryParams{ + SortBy: "deadline", + Order: "asc", + Limit: 5, + IncludeCompleted: false, + } + + if decision.Quadrant != nil && *decision.Quadrant >= 1 && *decision.Quadrant <= 4 { + params.Quadrant = decision.Quadrant + } + if kw := strings.TrimSpace(decision.Keyword); kw != "" { + params.Keyword = kw + } + if decision.Limit != nil && *decision.Limit > 0 && *decision.Limit <= 20 { + params.Limit = *decision.Limit + } + + results, err := input.QuickTaskDeps.QueryTasks(ctx, flowState.UserID, params) + if err != nil { + return fmt.Sprintf("查询失败了(%s),稍后再试试?", err) + } + + if len(results) == 0 { + return "当前没有匹配的任务。" + } + + var sb strings.Builder + sb.WriteString(fmt.Sprintf("找到 %d 条任务:\n", len(results))) + for _, t := range results { + label := newagentshared.PriorityLabelCN(t.PriorityGroup) + line := fmt.Sprintf("- %s(%s", t.Title, label) + if t.DeadlineAt != "" { + line += fmt.Sprintf(",截止 %s", t.DeadlineAt) + } + line += ")" + if t.IsCompleted { + line += " ✅" + } + sb.WriteString(line + "\n") + } + + return sb.String() +} + +// quickNoteFallbackPriority 根据截止时间推断默认优先级,与 tools/quicknote.go 保持一致。 +func quickNoteFallbackPriority(deadline *time.Time) int { + if deadline != nil { + if time.Until(*deadline) <= 48*time.Hour { + return newagentshared.QuickNotePriorityImportantUrgent + } + return newagentshared.QuickNotePriorityImportantNotUrgent + } + return newagentshared.QuickNotePrioritySimpleNotImportant +} diff --git a/backend/newAgent/prompt/chat.go b/backend/newAgent/prompt/chat.go index 0355cb1..116cd90 100644 --- a/backend/newAgent/prompt/chat.go +++ b/backend/newAgent/prompt/chat.go @@ -14,10 +14,18 @@ const chatRoutingSystemPrompt = ` 路由规则: - direct_reply:纯闲聊、简单问答、轻量生活建议、打招呼、感谢等不需要工具、也不需要长链路思考的请求。控制码后直接输出完整回复。 -- execute:需要用工具处理的请求(记录任务/提醒、查询日程、移动课程、排课等),但不需要先制定计划。控制码后输出简短确认。 +- quick_task:用户明确想记录/添加/修改/删除一个待办或提醒(如"记一下""提醒我""帮我记"),或查看/筛选任务列表(如"我有什么任务""待办清单""最近急事")。该路由走轻量快捷路径,延迟低、废话少。控制码后不要输出任何内容。 +- execute:需要用工具处理的日程类请求(查询日程、移动课程、排课等),但不需要先制定计划。控制码后输出简短确认。 - deep_answer:复杂问题但不需要工具(如分析建议、知识解释、方案比较、深度讨论等),需要深度思考后回答。控制码后不要输出任何占位过渡语,后端会直接进入第二次正式回答。 - plan:用户明确要求先制定计划,或涉及多阶段复杂规划。控制码后输出简短确认。 +quick_task 判别要点: +- 用户明确要"记/添加/提醒"一个待办 → quick_task +- 用户要查看/筛选/列出任务清单 → quick_task +- 用户要修改/删除某个任务 → quick_task +- 但如果用户同时提了日程排布(如"把明天的课调一下,再记一下周五开会"),混合操作走 execute +- 如果信息不足(如"帮我记一下"但没说记什么),走 direct_reply 追问 + 通用回答约束: - 非日程、非任务类问题,只要不需要工具,也应当正常回答。 - 不要因为用户的问题不涉及排程,就说自己“只能处理日程/任务安排”。 @@ -44,7 +52,7 @@ const chatRoutingSystemPrompt = ` 输出格式(严格两段式): 第一段(控制码,用户不可见,后端会截取): - + 第二段(紧接控制码之后,用户可见): 根据路由输出对应内容。 @@ -59,6 +67,8 @@ const chatRoutingSystemPrompt = ` 当然可以,我先直接回答你这个问题。 + + 好的,我来帮你看看今天的安排。 diff --git a/backend/newAgent/prompt/execute.go b/backend/newAgent/prompt/execute.go index 8b6bcf2..82f55f2 100644 --- a/backend/newAgent/prompt/execute.go +++ b/backend/newAgent/prompt/execute.go @@ -14,7 +14,7 @@ const executeSystemPromptWithPlan = ` 你可以做什么: 1. 只围绕当前步骤推进,先读后写,逐步完成当前步骤。 2. 可调用读工具补充事实,再决定下一步。 -3. 日程写操作时输出 action=confirm 并附带 tool_call,等待用户确认。quick_note_create 不需要确认,用 action=continue;若信息足够,必须显式填写 priority_group,若信息不足则先 ask_user,不要盲猜。 +3. 日程写操作时输出 action=confirm 并附带 tool_call,等待用户确认。 4. 若用户给出了"二次微调方向"(如负载均衡、某天减负、某类任务后移),优先围绕该方向推进,并在 goal_check 说明满足情况。 5. 只有在用户明确允许打乱顺序时,才可使用 min_context_switch 做重排。 6. 多任务微调时默认走队列链路:query_target_tasks(enqueue=true) → queue_pop_head → query_available_slots → queue_apply_head_move / queue_skip_head。 @@ -39,12 +39,10 @@ const executeSystemPromptWithPlan = ` 1. 输出格式:先输出一行 {JSON 决策},然后换行输出给用户看的自然语言正文。JSON 中不要包含 speak 字段——用户可见的话放在标签之后。 2. 读操作:action=continue + tool_call。 3. 写操作(日程变更,如 place/move/swap/batch_move/unplace/spread_even/min_context_switch):action=confirm + tool_call。 -4. quick_note_create(记录任务/提醒):若信息足够,action=continue + tool_call,并显式填写 priority_group;若信息不足且无法可靠推断,action=ask_user 先追问。quick_note_create 调用时和调用后 speak 必须留空,收口由 deliver 阶段统一完成;调用成功后可继续(done/next_plan/continue)处理其他任务,但不要为 quick_note_create 本身补充说明。 -5. query_tasks(查看/筛选任务列表):读操作,action=continue + tool_call。用于回答"我有什么任务""最近有什么急事"等问题,支持按象限、关键词、截止时间范围筛选和排序。 -6. 缺关键上下文且无法通过工具补齐:action=ask_user。 -7. 仅当当前步骤完成时输出 action=next_plan,并在 goal_check 对照 done_when 给出证据。 -8. 仅当整体任务完成时输出 action=done,并在 goal_check 总结完成证据。 -9. 流程应正式终止时输出 action=abort。` +4. 缺关键上下文且无法通过工具补齐:action=ask_user。 +5. 仅当当前步骤完成时输出 action=next_plan,并在 goal_check 对照 done_when 给出证据。 +6. 仅当整体任务完成时输出 action=done,并在 goal_check 总结完成证据。 +7. 流程应正式终止时输出 action=abort。` const executeSystemPromptReAct = ` 你是 SmartMate 的执行器,当前处于自由执行模式(无预定义 plan 步骤)。 @@ -59,7 +57,7 @@ const executeSystemPromptReAct = ` 1. 你可以基于用户给定的二次微调方向,对 suggested 做定向微调。 2. existing 属于已安排事实层,可用于冲突判断和参考,不作为 move/batch_move/spread_even 的目标。 3. 你可以先调用读工具补充必要事实(例如 get_overview/query_target_tasks/query_available_slots/get_task_info)。 -4. 你可以在需要日程写操作时提出 confirm(move/swap/unplace/batch_move/spread_even)。quick_note_create 不需要确认,用 action=continue;若信息足够,必须显式填写 priority_group,若信息不足则先 ask_user。 +4. 你可以在需要日程写操作时提出 confirm(move/swap/unplace/batch_move/spread_even)。 5. 只有用户明确允许打乱顺序时,才可使用 min_context_switch。 6. 多任务处理默认使用队列链路:先 query_target_tasks(enqueue=true) 入队,再 queue_pop_head 逐项处理。 @@ -82,11 +80,9 @@ const executeSystemPromptReAct = ` 1. 输出格式:先输出一行 {JSON 决策},然后换行输出给用户看的自然语言正文。JSON 中不要包含 speak 字段——用户可见的话放在标签之后。 2. 读操作:action=continue + tool_call。 3. 写操作(日程变更,如 place/move/swap/batch_move/unplace/spread_even/min_context_switch):action=confirm + tool_call。 -4. quick_note_create(记录任务/提醒):若信息足够,action=continue + tool_call,并显式填写 priority_group;若信息不足且无法可靠推断,action=ask_user 先追问。quick_note_create 调用时和调用后 speak 必须留空,收口由 deliver 阶段统一完成;调用成功后可继续(done/next_plan/continue)处理其他任务,但不要为 quick_note_create 本身补充说明。 -5. query_tasks(查看/筛选任务列表):读操作,action=continue + tool_call。用于回答"我有什么任务""最近有什么急事"等问题,支持按象限、关键词、截止时间范围筛选和排序。 -6. 缺关键上下文且无法通过工具补齐:action=ask_user。 -7. 任务完成:action=done,并在 goal_check 总结完成证据。 -8. 流程应正式终止:action=abort。` +4. 缺关键上下文且无法通过工具补齐:action=ask_user。 +5. 任务完成:action=done,并在 goal_check 总结完成证据。 +6. 流程应正式终止:action=abort。` // BuildExecuteSystemPrompt 返回执行阶段系统提示词(有 plan 模式)。 func BuildExecuteSystemPrompt() string { diff --git a/backend/newAgent/prompt/quick_task.go b/backend/newAgent/prompt/quick_task.go new file mode 100644 index 0000000..b8fb341 --- /dev/null +++ b/backend/newAgent/prompt/quick_task.go @@ -0,0 +1,102 @@ +package newagentprompt + +import ( + "fmt" + "strings" + "time" + + newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model" + "github.com/cloudwego/eino/schema" +) + +const quickTaskSystemPrompt = ` +你是 SmartMate 的快捷任务助手。用户想记录或查看待办任务。你需要从用户消息中提取操作意图和参数。 + +你能做的操作: +- create:记录一条新任务/提醒 +- query:查看/筛选任务列表 + +输出格式(两阶段): +先输出一行决策标签,标签内是 JSON;标签之后换行输出给用户看的自然语言正文。 +决策标签格式:{JSON} + +JSON 字段说明: +- action:只能是 create / query / ask +- create 时:title 必填,deadline_at 必填,priority_group 必填,范围 1-4;urgency_threshold_at 满足条件时填写,条件在下面 +- query 时:quadrant 可选 1-4,keyword 可选,limit 可选 +- ask 时:question 必填 + +规则: +1. 优先级:1=重要且紧急,2=重要不紧急,3=简单不重要,4=复杂不重要;紧急判定:截止时间距今不超过 48 小时为紧急(1),超过 48 小时为不紧急(2),无截止时间默认 3 +2. 信息不足时(如缺少 title),输出 {"action":"ask","question":"追问内容"} +3. deadline_at 支持「明天下午3点」「下周一」「2026-04-20 18:00」等格式 +4. 未提供的可选字段直接省略,不要填 null 或空字符串 +5. JSON 中不要包含 speak 字段,给用户看的话放在 标签之后 +6. 紧急分界时间,即任务从"重要不紧急"自动轮换到"重要且紧急"的时间点;格式同 deadline_at ——当 priority_group=2 时必填,你必须根据 deadline 自动推算一个合理的紧急分界时间(通常为 deadline 前 24-48 小时),不要等用户提供;priority_group 为 1、3、4 或无截止时间时不要输出此字段 + +示例: + +{"action":"create","title":"明天开会","deadline_at":"明天下午3点"} +好的,我来帮你记一下。 +{"action":"create","title":"下周交报告","deadline_at":"下周五 18:00","priority_group":2,"urgency_threshold_at":"下周四 09:00"} +好的,我也帮你记一下。 + +{"action":"query","limit":5} +我帮你查一下当前的任务。 + +{"action":"ask","question":"你想记录什么呢?告诉我具体内容吧。"} +你想记录什么呢?告诉我具体内容吧。` + +// BuildQuickTaskSystemPrompt 返回快捷任务阶段的系统提示词。 +func BuildQuickTaskSystemPrompt() string { + return strings.TrimSpace(quickTaskSystemPrompt) +} + +// BuildQuickTaskMessagesSimple 组装快捷任务阶段的最简 messages(无对话历史)。 +func BuildQuickTaskMessagesSimple(userInput string) []*schema.Message { + systemMsg := schema.SystemMessage(BuildQuickTaskSystemPrompt()) + userMsg := schema.UserMessage(buildQuickTaskUserPrompt(userInput)) + return []*schema.Message{systemMsg, userMsg} +} + +func buildQuickTaskUserPrompt(userInput string) string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("当前时间=%s\n", time.Now().In(time.Local).Format("2006-01-02 15:04"))) + sb.WriteString("\n请从用户消息中提取操作意图和参数,严格按 SMARTFLOW_DECISION 标签格式输出。\n\n") + + trimmedInput := strings.TrimSpace(userInput) + if trimmedInput != "" { + sb.WriteString("用户输入:\n") + sb.WriteString(trimmedInput) + sb.WriteString("\n") + } + + return sb.String() +} + +// BuildQuickTaskMessages 组装快捷任务阶段的完整 messages(含对话历史)。 +func BuildQuickTaskMessages( + ctx *newagentmodel.ConversationContext, + userInput string, + toolSchemas []newagentmodel.ToolSchemaContext, +) []*schema.Message { + return buildUnifiedStageMessages( + ctx, + StageMessagesConfig{ + SystemPrompt: BuildQuickTaskSystemPrompt(), + Msg1Content: buildChatConversationMessage(ctx), + Msg2Content: buildQuickTaskWorkspace(toolSchemas), + Msg3Suffix: buildQuickTaskUserPrompt(userInput), + Msg3Role: schema.User, + }, + ) +} + +func buildQuickTaskWorkspace(toolSchemas []newagentmodel.ToolSchemaContext) string { + var sb strings.Builder + sb.WriteString("可用工具:\n") + for _, ts := range toolSchemas { + sb.WriteString(fmt.Sprintf("- %s: %s\n 参数: %s\n", ts.Name, ts.Desc, ts.SchemaText)) + } + return sb.String() +} diff --git a/backend/newAgent/router/chat_route.go b/backend/newAgent/router/chat_route.go index 265eb5b..2dafd65 100644 --- a/backend/newAgent/router/chat_route.go +++ b/backend/newAgent/router/chat_route.go @@ -24,7 +24,7 @@ var ( chatRouteHeaderRegex = regexp.MustCompile( `(?is)<\s*SMARTFLOW_ROUTE\b` + `[^>]*\bnonce\s*=\s*["']?([a-zA-Z0-9\-]+)["']?` + - `[^>]*\broute\s*=\s*["']?(direct_reply|execute|deep_answer|plan)["']?` + + `[^>]*\broute\s*=\s*["']?(direct_reply|execute|deep_answer|plan|quick_task)["']?` + `(?:[^>]*\brough_build\s*=\s*["']?(true|false)["']?)?` + `(?:[^>]*\brefine\s*=\s*["']?(true|false)["']?)?` + `(?:[^>]*\breorder\s*=\s*["']?(true|false)["']?)?` + diff --git a/backend/newAgent/tools/registry.go b/backend/newAgent/tools/registry.go index 315e4e8..06ef2e3 100644 --- a/backend/newAgent/tools/registry.go +++ b/backend/newAgent/tools/registry.go @@ -31,12 +31,6 @@ type DefaultRegistryDeps struct { // WebSearchProvider Web 搜索供应商。为 nil 时 web_search / web_fetch 返回"暂未启用",不阻断主流程。 WebSearchProvider web.SearchProvider - - // QuickNote 随口记工具依赖。CreateTask 为 nil 时 quick_note_create 返回错误提示,不阻断主流程。 - QuickNote QuickNoteDeps - - // TaskQuery 任务查询工具依赖。QueryTasks 为 nil 时 query_tasks 不注册,不影响其他工具。 - TaskQuery TaskQueryDeps } // ToolRegistry 管理工具注册、查找与执行。 @@ -129,10 +123,8 @@ var writeTools = map[string]bool{ // 调用目的:这些工具不需要日程状态即可执行,execute 节点在 ScheduleState 为 nil 时允许调用。 var scheduleFreeTools = map[string]bool{ - "quick_note_create": true, - "query_tasks": true, - "web_search": true, - "web_fetch": true, + "web_search": true, + "web_fetch": true, } // ==================== 默认注册表 ==================== @@ -332,30 +324,6 @@ func NewDefaultRegistryWithDeps(deps DefaultRegistryDeps) *ToolRegistry { }, ) - // --- 随口记工具 --- - // 调用目的:将"帮我记一下明天开会"等随口任务请求直接写入数据库,无需 ScheduleState。 - // 不加入 writeTools:随口记是用户明确指令,不需要 confirm 节点二次确认。 - if deps.QuickNote.CreateTask != nil { - quickNoteHandler := NewQuickNoteToolHandler(deps.QuickNote) - r.Register("quick_note_create", - "记录一条任务/提醒/待办事项到用户的任务列表。支持中文相对时间(如“明天下午3点”、“下周一”)。title 必填。记录成功后,回复时应包含一句与任务内容相关的轻松跟进话术(不超过30字),类似朋友间的友好调侃。", - `{"name":"quick_note_create","parameters":{"title":{"type":"string","required":true,"description":"任务标题,简洁明确"},"deadline_at":{"type":"string","description":"可选截止时间,支持 yyyy-MM-dd HH:mm 或中文相对时间(明天/下周一/后天等)"},"priority_group":{"type":"int","description":"优先级(1重要且紧急,2重要不紧急,3简单不重要,4复杂不重要);信息足够时请显式填写,不确定时可不填,由工具层自动推断"}}}`, - quickNoteHandler, - ) - } - - // --- 任务查询读工具 --- - // 调用目的:将"帮我看看有什么任务""最近有什么急事"等查询请求直接查库返回结构化结果,无需 ScheduleState。 - // 不加入 writeTools:查询是只读操作,不需要 confirm 节点二次确认。 - if deps.TaskQuery.QueryTasks != nil { - taskQueryHandler := NewTaskQueryToolHandler(deps.TaskQuery) - r.Register("query_tasks", - "按象限、关键词、截止时间筛选并排序任务列表,返回结构化结果。所有参数均为可选。", - `{"name":"query_tasks","parameters":{"quadrant":{"type":"int","description":"可选象限筛选(1~4)"},"keyword":{"type":"string","description":"可选标题关键词,模糊匹配"},"deadline_before":{"type":"string","description":"可选截止时间上界,支持 yyyy-MM-dd HH:mm 或 yyyy-MM-dd"},"deadline_after":{"type":"string","description":"可选截止时间下界,支持 yyyy-MM-dd HH:mm 或 yyyy-MM-dd"},"sort_by":{"type":"string","description":"排序字段(deadline|priority|id),默认deadline"},"order":{"type":"string","description":"排序方向(asc|desc),默认asc"},"limit":{"type":"int","description":"返回条数,默认5,上限20"},"include_completed":{"type":"bool","description":"是否包含已完成任务,默认false"}}}`, - taskQueryHandler, - ) - } - // --- Web 搜索读工具 --- // 1. provider 为 nil 时 handler 返回"暂未启用"的 observation,不会阻断主流程; // 2. 两个工具均为读操作,走 action=continue + tool_call 模式。 diff --git a/backend/service/agentsvc/agent.go b/backend/service/agentsvc/agent.go index 29ead6d..7c7605c 100644 --- a/backend/service/agentsvc/agent.go +++ b/backend/service/agentsvc/agent.go @@ -61,6 +61,7 @@ type AgentService struct { scheduleProvider newagentmodel.ScheduleStateProvider agentStateStore newagentmodel.AgentStateStore compactionStore newagentmodel.CompactionStore + quickTaskDeps newagentmodel.QuickTaskDeps memoryReader MemoryReader memoryCfg memorymodel.Config memoryObserver memoryobserve.Observer diff --git a/backend/service/agentsvc/agent_memory.go b/backend/service/agentsvc/agent_memory.go index f34b6dc..5df3f6b 100644 --- a/backend/service/agentsvc/agent_memory.go +++ b/backend/service/agentsvc/agent_memory.go @@ -12,7 +12,7 @@ import ( ) const ( - newAgentMemoryRetrieveLimit = 5 + newAgentMemoryRetrieveLimit = 10 newAgentMemoryIntroLine = "以下是与当前对话相关的用户记忆,仅在自然且确实有帮助时参考,不要生硬复述。" ) diff --git a/backend/service/agentsvc/agent_newagent.go b/backend/service/agentsvc/agent_newagent.go index f65482e..4f1af72 100644 --- a/backend/service/agentsvc/agent_newagent.go +++ b/backend/service/agentsvc/agent_newagent.go @@ -209,6 +209,7 @@ func (s *AgentService) runNewAgentGraph( ThinkingExecute: viper.GetBool("agent.thinking.execute"), ThinkingDeliver: viper.GetBool("agent.thinking.deliver"), PersistVisibleMessage: persistVisibleMessage, + QuickTaskDeps: s.quickTaskDeps, } // 10. 构造 AgentGraphRunInput 并运行 graph。 @@ -704,3 +705,8 @@ func (s *AgentService) SetAgentStateStore(store newagentmodel.AgentStateStore) { func (s *AgentService) SetCompactionStore(store newagentmodel.CompactionStore) { s.compactionStore = store } + +// quickTaskDeps 由 cmd/start.go 注入 +func (s *AgentService) SetQuickTaskDeps(deps newagentmodel.QuickTaskDeps) { + s.quickTaskDeps = deps +}