✨ feat(agent): 通用分流接入随口问图编排,修复任务查询条数与重复输出问题 - ♻️ 将 Agent 路由升级为通用 `action` 分流机制,统一支持 `chat` / `quick_note_create` / `task_query` - 🧩 新增 `taskquery` 子模块并落地图编排链路:`plan -> quadrant -> time_anchor -> tool_query -> reflect` - 🔧 在图内接入 `query_tasks` 工具调用,支持自动放宽检索条件与反思重试,最多重试 2 次 - 🚪 保持 `/agent/chat` 作为多合一入口,不额外新增任务查询 HTTP 接口 - 🪄 修复“随口问”场景下的双重列表输出问题:LLM 仅保留简短前缀,任务列表统一由后端进行确定性渲染 - 🎯 修复显式数量约束失效问题:支持提取“来一个”“前 3 个”“top5”等数量表达,并将其锁定为 `limit` - 🛡️ 防止在重试或放宽检索阶段改写用户显式指定的数量约束 - ✅ 补充并更新测试,覆盖路由解析、数量提取、`limit` 生效及重复输出等关键场景 📝 docs: 更新随口问链路文档与决策记录 - 📚 更新 README 5.4,新增/修订随口问链路 Mermaid 图 - 🧭 新增随口问功能决策记录 FDR
94 lines
3.6 KiB
Go
94 lines
3.6 KiB
Go
package agentsvc
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/LoveLosita/smartflow/backend/agent/quicknote"
|
||
"github.com/LoveLosita/smartflow/backend/agent/route"
|
||
)
|
||
|
||
// TestParseQuickNoteRouteControlTag_QuickNote
|
||
// 目的:验证模型控制码在 action=quick_note 时可被稳定解析,
|
||
// 并且会校验 nonce,避免历史脏内容或伪造片段误命中。
|
||
func TestParseQuickNoteRouteControlTag_QuickNote(t *testing.T) {
|
||
nonce := "abc123nonce"
|
||
raw := `<SMARTFLOW_ROUTE nonce="abc123nonce" action="quick_note"></SMARTFLOW_ROUTE>
|
||
<SMARTFLOW_REASON>用户明确在请求未来提醒</SMARTFLOW_REASON>`
|
||
|
||
decision, err := route.ParseQuickNoteRouteControlTag(raw, nonce)
|
||
if err != nil {
|
||
t.Fatalf("解析失败: %v", err)
|
||
}
|
||
if decision == nil {
|
||
t.Fatalf("decision 不应为空")
|
||
}
|
||
// 兼容逻辑:历史 quick_note 会被统一映射到 quick_note_create。
|
||
if decision.Action != route.ActionQuickNoteCreate {
|
||
t.Fatalf("action 解析错误,期望=%s 实际=%s", route.ActionQuickNoteCreate, decision.Action)
|
||
}
|
||
if strings.TrimSpace(decision.Reason) == "" {
|
||
t.Fatalf("reason 不应为空")
|
||
}
|
||
}
|
||
|
||
// TestParseRouteControlTag_TaskQuery
|
||
// 目的:验证通用分流中 action=task_query 的控制码可稳定解析。
|
||
func TestParseRouteControlTag_TaskQuery(t *testing.T) {
|
||
nonce := "taskquerynonce"
|
||
raw := `<SMARTFLOW_ROUTE nonce="taskquerynonce" action="task_query"></SMARTFLOW_ROUTE>
|
||
<SMARTFLOW_REASON>用户在查最紧急任务</SMARTFLOW_REASON>`
|
||
|
||
decision, err := route.ParseRouteControlTag(raw, nonce)
|
||
if err != nil {
|
||
t.Fatalf("解析失败: %v", err)
|
||
}
|
||
if decision == nil {
|
||
t.Fatalf("decision 不应为空")
|
||
}
|
||
if decision.Action != route.ActionTaskQuery {
|
||
t.Fatalf("action 解析错误,期望=%s 实际=%s", route.ActionTaskQuery, decision.Action)
|
||
}
|
||
}
|
||
|
||
// TestParseQuickNoteRouteControlTag_NonceMismatch
|
||
// 目的:确保 nonce 不匹配时直接报错,避免把非本次请求的控制码当作有效路由。
|
||
func TestParseQuickNoteRouteControlTag_NonceMismatch(t *testing.T) {
|
||
raw := `<SMARTFLOW_ROUTE nonce="wrongnonce" action="chat"></SMARTFLOW_ROUTE>`
|
||
if _, err := route.ParseQuickNoteRouteControlTag(raw, "expectednonce"); err == nil {
|
||
t.Fatalf("期望 nonce 不匹配时报错,但未报错")
|
||
}
|
||
}
|
||
|
||
// TestBuildQuickNoteFinalReply_NoFalseSuccessWithoutTaskID
|
||
// 目的:即使 state.Persisted 被错误置为 true,只要 task_id 无效,也不能返回“安排成功”文案。
|
||
func TestBuildQuickNoteFinalReply_NoFalseSuccessWithoutTaskID(t *testing.T) {
|
||
state := &quicknote.QuickNoteState{
|
||
Persisted: true,
|
||
PersistedTaskID: 0,
|
||
ExtractedTitle: "去下馆子",
|
||
}
|
||
|
||
reply := buildQuickNoteFinalReply(nil, nil, "我今天晚上6点要去下馆子,记得喊我", state)
|
||
if strings.Contains(reply, "给你安排上了") || strings.Contains(reply, "已安排") {
|
||
t.Fatalf("不应返回成功文案,实际回复=%s", reply)
|
||
}
|
||
}
|
||
|
||
// TestBuildQuickNoteFinalReply_UseExtractedBanter
|
||
// 目的:当聚合规划阶段已经产出 banter 时,最终回复应直接复用,避免再次调用润色模型。
|
||
func TestBuildQuickNoteFinalReply_UseExtractedBanter(t *testing.T) {
|
||
state := &quicknote.QuickNoteState{
|
||
Persisted: true,
|
||
PersistedTaskID: 12,
|
||
ExtractedTitle: "明天去取快递",
|
||
ExtractedPriority: 2,
|
||
ExtractedBanter: "取件路上注意保暖,别被风吹懵了。",
|
||
}
|
||
|
||
reply := buildQuickNoteFinalReply(nil, nil, "明天上午12点我要去取快递,到时候记得q我", state)
|
||
if !strings.Contains(reply, "取件路上注意保暖") {
|
||
t.Fatalf("期望复用 ExtractedBanter,实际回复=%s", reply)
|
||
}
|
||
}
|