Version: 0.9.41.dev.260424

后端:
1. 随口记从 Execute 工具链路迁移到独立 QuickTask 轻量节点——单轮流式提取意图直接调 service,绕过 ReAct 循环
- 新增 QuickTask graph 节点 + Chat→QuickTask→END 分支
- Chat 路由提示词新增 quick_task 路由判别规则,execute 路由收窄为日程类
- Execute 提示词(有 plan / ReAct 两套)移除 quick_note_create / query_tasks 指令
- ToolRegistry 注销 quick_note_create / query_tasks,移除相关依赖与注册
- 依赖注入从 ToolRegistry 改为 Service 层直接注入 QuickTaskDeps

2. urgency_threshold_at 代码兜底 + API 返回补全
- priorityGroup=2 且有 deadline 但 LLM 未填时,自动设为 deadline-24h
- 任务查询接口返回结构补充 UrgencyThresholdAt 字段与转换映射

3. 记忆召回条数 5→10
This commit is contained in:
LoveLosita
2026-04-24 14:02:27 +08:00
parent c258602a2b
commit 8daae62812
19 changed files with 606 additions and 136 deletions

View File

@@ -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 层初始化。

View File

@@ -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,
}
}

View File

@@ -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 是"更新任务属性"接口的请求体。

View File

@@ -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

View File

@@ -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 不能为空")

View File

@@ -13,6 +13,7 @@ const (
PhasePlanning Phase = "planning"
PhaseWaitingConfirm Phase = "waiting_confirm"
PhaseExecuting Phase = "executing"
PhaseQuickTask Phase = "quick_task"
PhaseDone Phase = "done"
)

View File

@@ -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 层共享) ---

View File

@@ -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 {

View File

@@ -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

View File

@@ -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": "移动任务",

View File

@@ -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
}

View File

@@ -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 = `
输出格式(严格两段式):
第一段(控制码,用户不可见,后端会截取):
<SMARTFLOW_ROUTE nonce="给定nonce" route="direct_reply|execute|deep_answer|plan" rough_build="false" refine="false" reorder="false" thinking="false"/>
<SMARTFLOW_ROUTE nonce="给定nonce" route="direct_reply|execute|deep_answer|plan|quick_task" rough_build="false" refine="false" reorder="false" thinking="false"/>
第二段(紧接控制码之后,用户可见):
根据路由输出对应内容。
@@ -59,6 +67,8 @@ const chatRoutingSystemPrompt = `
<SMARTFLOW_ROUTE nonce="给定nonce" route="direct_reply"/>
当然可以,我先直接回答你这个问题。
<SMARTFLOW_ROUTE nonce="给定nonce" route="quick_task"/>
<SMARTFLOW_ROUTE nonce="给定nonce" route="execute"/>
好的,我来帮你看看今天的安排。

View File

@@ -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. 输出格式:先输出一行 <SMARTFLOW_DECISION>{JSON 决策}</SMARTFLOW_DECISION>然后换行输出给用户看的自然语言正文。JSON 中不要包含 speak 字段——用户可见的话放在标签之后。
2. 读操作action=continue + tool_call。
3. 写操作(日程变更,如 place/move/swap/batch_move/unplace/spread_even/min_context_switchaction=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. 你可以在需要日程写操作时提出 confirmmove/swap/unplace/batch_move/spread_evenquick_note_create 不需要确认,用 action=continue若信息足够必须显式填写 priority_group若信息不足则先 ask_user。
4. 你可以在需要日程写操作时提出 confirmmove/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. 输出格式:先输出一行 <SMARTFLOW_DECISION>{JSON 决策}</SMARTFLOW_DECISION>然后换行输出给用户看的自然语言正文。JSON 中不要包含 speak 字段——用户可见的话放在标签之后。
2. 读操作action=continue + tool_call。
3. 写操作(日程变更,如 place/move/swap/batch_move/unplace/spread_even/min_context_switchaction=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 {

View File

@@ -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标签之后换行输出给用户看的自然语言正文。
决策标签格式:<SMARTFLOW_DECISION>{JSON}</SMARTFLOW_DECISION>
JSON 字段说明:
- action只能是 create / query / ask
- create 时title 必填deadline_at 必填priority_group 必填,范围 1-4urgency_threshold_at 满足条件时填写,条件在下面
- query 时quadrant 可选 1-4keyword 可选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 字段,给用户看的话放在 </SMARTFLOW_DECISION> 标签之后
6. 紧急分界时间,即任务从"重要不紧急"自动轮换到"重要且紧急"的时间点;格式同 deadline_at ——当 priority_group=2 时必填,你必须根据 deadline 自动推算一个合理的紧急分界时间(通常为 deadline 前 24-48 小时不要等用户提供priority_group 为 1、3、4 或无截止时间时不要输出此字段
示例:
<SMARTFLOW_DECISION>{"action":"create","title":"明天开会","deadline_at":"明天下午3点"}</SMARTFLOW_DECISION>
好的,我来帮你记一下。
<SMARTFLOW_DECISION>{"action":"create","title":"下周交报告","deadline_at":"下周五 18:00","priority_group":2,"urgency_threshold_at":"下周四 09:00"}</SMARTFLOW_DECISION>
好的,我也帮你记一下。
<SMARTFLOW_DECISION>{"action":"query","limit":5}</SMARTFLOW_DECISION>
我帮你查一下当前的任务。
<SMARTFLOW_DECISION>{"action":"ask","question":"你想记录什么呢?告诉我具体内容吧。"}</SMARTFLOW_DECISION>
你想记录什么呢?告诉我具体内容吧。`
// 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()
}

View File

@@ -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)["']?)?` +

View File

@@ -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 模式。

View File

@@ -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

View File

@@ -12,7 +12,7 @@ import (
)
const (
newAgentMemoryRetrieveLimit = 5
newAgentMemoryRetrieveLimit = 10
newAgentMemoryIntroLine = "以下是与当前对话相关的用户记忆,仅在自然且确实有帮助时参考,不要生硬复述。"
)

View File

@@ -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
}