新增: AI 工具意图识别层 + 文件访问权限模型 Phase B+C
会话意图识别层(intent.rs / df-ai crate):
- Intent 枚举 11 态 + recognize 规则识别(关键词+权重+优先级)+ tool_subset_for 工具子集映射
- 模态接口预留 None,独立模块待接入 loop,36 单测
文件访问权限模型 Phase B+C(F-260619-03):
- Phase B 会话临时白名单 + 循环挂起 + DirAuthDialog 弹窗(仅本次/未来都允许/拒绝)
- Phase C 系统目录黑名单(Win System32/Program Files;Unix /etc /usr...)分段匹配 + 写操作约束
- 已审 CR-260619-09 ✅ PASS
This commit is contained in:
682
crates/df-ai/src/intent.rs
Normal file
682
crates/df-ai/src/intent.rs
Normal file
@@ -0,0 +1,682 @@
|
||||
//! 会话意图识别层(Intent Recognition)
|
||||
//!
|
||||
//! 论证依据:`docs/02-架构设计/构想审查/意图识别层论证-2026-06-19.md`
|
||||
//!
|
||||
//! ## 定位
|
||||
//! 纯函数模块,**不接入 agentic loop**(待 Phase B+C 完成 + 模型模态管理后接入)。
|
||||
//! 提供三个能力:
|
||||
//! 1. `IntentRecognizer::recognize(message)` —— 规则/关键词匹配(方式 A,零延迟零成本)
|
||||
//! 返回 `(Intent, f32)`,置信度 0.0–1.0。低置信 → 上游 fallback 全量工具。
|
||||
//! 2. `tool_subset_for(intent)` —— 硬编码工具名→domain 映射,工具名子集(空 = 全量 fallback)。
|
||||
//! 3. `suggested_model_tier(intent)` —— 模态建议**接口预留**,当前恒返 `None`
|
||||
//! (待模型模态管理 Phase 落地后补充实际逻辑)。
|
||||
//!
|
||||
//! ## 设计原则
|
||||
//! - **不碰** `tool_registry`:domain 映射在本文件内硬编码工具名常量,运行期不读 registry。
|
||||
//! - **不碰** agentic loop / src-tauri:本 crate 内独立编译,单测自洽。
|
||||
//! - 关键词表对齐 devflow 实际工具语义(read_file/write_file/patch_file/http_request 等)。
|
||||
//!
|
||||
//! ## 优先级(匹配命中后取舍)
|
||||
//! 具体(Code/File/Http/Search)> 实体(Project/Task/Idea)> 通用(Chat/Unknown)。
|
||||
//! 同级别内按命中关键词权重/数量决定置信度。
|
||||
|
||||
// ---- Intent 枚举 ------------------------------------------------------------
|
||||
|
||||
/// 会话意图分类。覆盖 devflow 当前 AI 工具域 + 通用对话。
|
||||
///
|
||||
/// `Unknown` 表示未命中任何关键词,上游应走全量工具 fallback。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Intent {
|
||||
/// 代码相关:实现/重构/编译错误/bug
|
||||
Code,
|
||||
/// 调试相关:复现/排查/堆栈
|
||||
Debug,
|
||||
/// 文件操作:读写/patch/列目录
|
||||
File,
|
||||
/// 项目管理:创建/绑定/列出项目
|
||||
Project,
|
||||
/// 任务管理:推进/状态/创建任务
|
||||
Task,
|
||||
/// 灵感:捕获/评估/晋升
|
||||
Idea,
|
||||
/// 对话:历史会话/总结
|
||||
Conversation,
|
||||
/// 搜索:grep/查找
|
||||
Search,
|
||||
/// HTTP 请求:调外部 API
|
||||
Http,
|
||||
/// 闲聊:问候/感谢
|
||||
Chat,
|
||||
/// 未识别:fallback 全量工具
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Intent {
|
||||
/// 短标签(日志/调试用)。
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Intent::Code => "code",
|
||||
Intent::Debug => "debug",
|
||||
Intent::File => "file",
|
||||
Intent::Project => "project",
|
||||
Intent::Task => "task",
|
||||
Intent::Idea => "idea",
|
||||
Intent::Conversation => "conversation",
|
||||
Intent::Search => "search",
|
||||
Intent::Http => "http",
|
||||
Intent::Chat => "chat",
|
||||
Intent::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- ModelTier 预留 ---------------------------------------------------------
|
||||
|
||||
/// 模型模态档位(**预留**)。
|
||||
///
|
||||
/// 待模型模态管理 Phase 落地后定义实际 provider/model 映射。
|
||||
/// 当前仅占位于 `suggested_model_tier` 返回类型,逻辑恒返 `None`。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModelTier {
|
||||
/// 轻量快速(简单意图/闲聊)
|
||||
Fast,
|
||||
/// 标准(默认复杂度)
|
||||
Standard,
|
||||
/// 重型(复杂推理/重构)
|
||||
Heavy,
|
||||
}
|
||||
|
||||
// ---- 关键词表 + 权重 --------------------------------------------------------
|
||||
|
||||
/// 单条关键词规则:关键词 + 命中权重。权重高者对置信度贡献大。
|
||||
struct Rule {
|
||||
kw: &'static str,
|
||||
weight: f32,
|
||||
}
|
||||
|
||||
// 优先级分组(具体 > 实体 > 通用)。组内关键词线性求和。
|
||||
// 关键词覆盖中英文,对齐 devflow 工具语义与用户自然表达。
|
||||
|
||||
const CODE_RULES: &[Rule] = &[
|
||||
Rule { kw: "代码", weight: 1.0 },
|
||||
Rule { kw: "函数", weight: 1.0 },
|
||||
Rule { kw: "方法", weight: 0.8 },
|
||||
Rule { kw: "bug", weight: 1.0 },
|
||||
Rule { kw: "编译", weight: 0.9 },
|
||||
Rule { kw: "error", weight: 0.9 },
|
||||
Rule { kw: "impl", weight: 0.7 },
|
||||
Rule { kw: "refactor", weight: 0.9 },
|
||||
Rule { kw: "重构", weight: 1.0 },
|
||||
Rule { kw: "实现", weight: 0.7 },
|
||||
];
|
||||
|
||||
const DEBUG_RULES: &[Rule] = &[
|
||||
Rule { kw: "调试", weight: 1.0 },
|
||||
Rule { kw: "debug", weight: 1.0 },
|
||||
Rule { kw: "排查", weight: 0.9 },
|
||||
Rule { kw: "复现", weight: 0.9 },
|
||||
Rule { kw: "堆栈", weight: 0.8 },
|
||||
Rule { kw: "栈", weight: 0.5 },
|
||||
Rule { kw: "panic", weight: 0.9 },
|
||||
Rule { kw: "traceback", weight: 0.9 },
|
||||
];
|
||||
|
||||
const FILE_RULES: &[Rule] = &[
|
||||
Rule { kw: "文件", weight: 1.0 },
|
||||
Rule { kw: "读取", weight: 0.9 },
|
||||
Rule { kw: "写入", weight: 0.9 },
|
||||
Rule { kw: "修改", weight: 0.7 },
|
||||
Rule { kw: "read", weight: 0.6 },
|
||||
Rule { kw: "write", weight: 0.6 },
|
||||
Rule { kw: "file", weight: 0.5 },
|
||||
Rule { kw: "patch", weight: 0.9 },
|
||||
];
|
||||
|
||||
const PROJECT_RULES: &[Rule] = &[
|
||||
Rule { kw: "项目", weight: 1.0 },
|
||||
Rule { kw: "project", weight: 0.9 },
|
||||
Rule { kw: "创建项目", weight: 1.0 },
|
||||
Rule { kw: "绑定", weight: 0.8 },
|
||||
Rule { kw: "目录", weight: 0.5 },
|
||||
];
|
||||
|
||||
const TASK_RULES: &[Rule] = &[
|
||||
Rule { kw: "任务", weight: 1.0 },
|
||||
Rule { kw: "task", weight: 0.9 },
|
||||
Rule { kw: "推进", weight: 0.9 },
|
||||
Rule { kw: "状态", weight: 0.7 },
|
||||
Rule { kw: "todo", weight: 0.8 },
|
||||
];
|
||||
|
||||
const IDEA_RULES: &[Rule] = &[
|
||||
Rule { kw: "灵感", weight: 1.0 },
|
||||
Rule { kw: "idea", weight: 0.9 },
|
||||
Rule { kw: "评估", weight: 0.8 },
|
||||
Rule { kw: "创意", weight: 0.8 },
|
||||
];
|
||||
|
||||
const CONVERSATION_RULES: &[Rule] = &[
|
||||
Rule { kw: "会话", weight: 0.9 },
|
||||
Rule { kw: "历史", weight: 0.7 },
|
||||
Rule { kw: "总结", weight: 0.6 },
|
||||
Rule { kw: "conversation", weight: 0.8 },
|
||||
];
|
||||
|
||||
const SEARCH_RULES: &[Rule] = &[
|
||||
Rule { kw: "搜索", weight: 1.0 },
|
||||
Rule { kw: "search", weight: 0.9 },
|
||||
Rule { kw: "查找", weight: 0.8 },
|
||||
Rule { kw: "grep", weight: 1.0 },
|
||||
Rule { kw: "find", weight: 0.6 },
|
||||
];
|
||||
|
||||
const HTTP_RULES: &[Rule] = &[
|
||||
Rule { kw: "请求", weight: 0.9 },
|
||||
Rule { kw: "http", weight: 1.0 },
|
||||
Rule { kw: "api", weight: 0.8 },
|
||||
Rule { kw: "url", weight: 0.7 },
|
||||
Rule { kw: "fetch", weight: 0.8 },
|
||||
Rule { kw: "调用接口", weight: 1.0 },
|
||||
];
|
||||
|
||||
const CHAT_RULES: &[Rule] = &[
|
||||
Rule { kw: "你好", weight: 1.0 },
|
||||
Rule { kw: "hello", weight: 1.0 },
|
||||
Rule { kw: "hi", weight: 0.9 },
|
||||
Rule { kw: "谢谢", weight: 1.0 },
|
||||
Rule { kw: "thanks", weight: 1.0 },
|
||||
Rule { kw: "帮助", weight: 0.7 },
|
||||
Rule { kw: "闲聊", weight: 1.0 },
|
||||
];
|
||||
|
||||
/// 优先级分组(高 → 低)。命中高分组的意图直接胜出,不向下累积。
|
||||
/// 分组内的候选按 (置信度, Intent 顺序) 取胜者。
|
||||
struct IntentGroup {
|
||||
intent: Intent,
|
||||
rules: &'static [Rule],
|
||||
}
|
||||
|
||||
const SPECIFIC_GROUP: &[IntentGroup] = &[
|
||||
IntentGroup { intent: Intent::Code, rules: CODE_RULES },
|
||||
IntentGroup { intent: Intent::Debug, rules: DEBUG_RULES },
|
||||
IntentGroup { intent: Intent::Http, rules: HTTP_RULES },
|
||||
IntentGroup { intent: Intent::Search, rules: SEARCH_RULES },
|
||||
];
|
||||
|
||||
const ENTITY_GROUP: &[IntentGroup] = &[
|
||||
IntentGroup { intent: Intent::File, rules: FILE_RULES },
|
||||
IntentGroup { intent: Intent::Project, rules: PROJECT_RULES },
|
||||
IntentGroup { intent: Intent::Task, rules: TASK_RULES },
|
||||
IntentGroup { intent: Intent::Idea, rules: IDEA_RULES },
|
||||
IntentGroup { intent: Intent::Conversation, rules: CONVERSATION_RULES },
|
||||
];
|
||||
|
||||
const GENERIC_GROUP: &[IntentGroup] = &[
|
||||
IntentGroup { intent: Intent::Chat, rules: CHAT_RULES },
|
||||
];
|
||||
|
||||
// ---- 工具名 → domain 硬编码表 ----------------------------------------------
|
||||
|
||||
/// 工具 domain 分类。`tool_subset_for` 按 Intent 选 domain,再回该 domain 的工具名列表。
|
||||
///
|
||||
/// **来源**:核验 `src-tauri/src/commands/ai/tool_registry.rs`(实际注册名)。
|
||||
/// 本表硬编码,不读 registry 运行期状态(保持模块独立可单测)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ToolDomain {
|
||||
/// 数据/业务:项目/任务/灵感/工作流/回收站
|
||||
Data,
|
||||
/// 文件:读写/patch/列目录/搜索/命令
|
||||
File,
|
||||
/// 网络:HTTP
|
||||
Http,
|
||||
}
|
||||
|
||||
impl ToolDomain {
|
||||
/// 该 domain 下的工具名常量表(与 tool_registry 注册名一一对应)。
|
||||
fn tools(self) -> &'static [&'static str] {
|
||||
match self {
|
||||
ToolDomain::Data => &[
|
||||
"list_projects",
|
||||
"create_project",
|
||||
"update_project",
|
||||
"delete_project",
|
||||
"bind_directory",
|
||||
"list_tasks",
|
||||
"create_task",
|
||||
"update_task",
|
||||
"advance_task",
|
||||
"delete_task",
|
||||
"list_ideas",
|
||||
"create_idea",
|
||||
"run_workflow",
|
||||
"list_trash",
|
||||
"restore_project",
|
||||
"purge_project",
|
||||
"get_project_count",
|
||||
"get_task_count",
|
||||
],
|
||||
ToolDomain::File => &[
|
||||
"read_file",
|
||||
"write_file",
|
||||
"patch_file",
|
||||
"list_directory",
|
||||
"search_files",
|
||||
"run_command",
|
||||
"file_info",
|
||||
"append_file",
|
||||
"delete_file",
|
||||
"rename_file",
|
||||
],
|
||||
ToolDomain::Http => &["http_request"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 识别器 -----------------------------------------------------------------
|
||||
|
||||
/// 意图识别器(无状态,所有方法均为纯函数,可 `Default` 构造)。
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct IntentRecognizer;
|
||||
|
||||
impl IntentRecognizer {
|
||||
/// 识别会话意图。
|
||||
///
|
||||
/// 返回 `(Intent, f32)`,置信度 0.0–1.0:
|
||||
/// - 命中关键词:`min(1.0, sum(命中权重))`,至少命中一条即胜出本组。
|
||||
/// - 未命中:`(Intent::Unknown, 0.0)`。
|
||||
///
|
||||
/// 优先级:SPECIFIC > ENTITY > GENERIC。高分组的胜者直接返回,不累积低分组。
|
||||
pub fn recognize(message: &str) -> (Intent, f32) {
|
||||
// 依次按优先级组求胜者。高分组有命中即提前返回。
|
||||
if let Some(hit) = best_in_group(message, SPECIFIC_GROUP) {
|
||||
return hit;
|
||||
}
|
||||
if let Some(hit) = best_in_group(message, ENTITY_GROUP) {
|
||||
return hit;
|
||||
}
|
||||
if let Some(hit) = best_in_group(message, GENERIC_GROUP) {
|
||||
return hit;
|
||||
}
|
||||
(Intent::Unknown, 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// 在一组候选里求置信度最高的(命中权重和截断到 1.0)。无任何命中返 `None`。
|
||||
fn best_in_group(message: &str, group: &[IntentGroup]) -> Option<(Intent, f32)> {
|
||||
let lower = message.to_lowercase();
|
||||
let mut best: Option<(Intent, f32)> = None;
|
||||
for cand in group {
|
||||
let mut score = 0.0f32;
|
||||
let mut matched = false;
|
||||
for rule in cand.rules.iter() {
|
||||
// 英文关键词用小写匹配;中文不受 to_lowercase 影响(no-op)。
|
||||
let needle = rule.kw.to_lowercase();
|
||||
if lower.contains(&needle) {
|
||||
score += rule.weight;
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
continue;
|
||||
}
|
||||
let conf = score.min(1.0);
|
||||
match best {
|
||||
Some((_, b)) if b >= conf => {}
|
||||
_ => best = Some((cand.intent, conf)),
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
// ---- Intent → 工具子集映射 --------------------------------------------------
|
||||
|
||||
/// 按 Intent 返回工具名子集。
|
||||
///
|
||||
/// 返回空 `Vec` 表示该意图**无工具收敛**(Chat)或**未识别**(Unknown),
|
||||
/// 上游应走**全量 fallback**(即不过滤工具,交全量给 LLM)。
|
||||
///
|
||||
/// 设计:Code → [file, http];File → [file];Project/Task/Idea → [data];
|
||||
/// Http → [http];Search → [file](含 search_files);Conversation → [];
|
||||
/// Chat → [];Debug → [file, http](调试常需读文件+查 API);Unknown → [](全量)。
|
||||
pub fn tool_subset_for(intent: &Intent) -> Vec<&'static str> {
|
||||
let domains: &[ToolDomain] = match intent {
|
||||
Intent::Code => &[ToolDomain::File, ToolDomain::Http],
|
||||
Intent::Debug => &[ToolDomain::File, ToolDomain::Http],
|
||||
Intent::File => &[ToolDomain::File],
|
||||
Intent::Project => &[ToolDomain::Data],
|
||||
Intent::Task => &[ToolDomain::Data],
|
||||
Intent::Idea => &[ToolDomain::Data],
|
||||
Intent::Conversation => &[],
|
||||
Intent::Search => &[ToolDomain::File],
|
||||
Intent::Http => &[ToolDomain::Http],
|
||||
Intent::Chat => &[],
|
||||
Intent::Unknown => &[],
|
||||
};
|
||||
let mut out: Vec<&'static str> = Vec::new();
|
||||
for d in domains {
|
||||
for name in d.tools() {
|
||||
if !out.contains(name) {
|
||||
out.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ---- 模态建议(接口预留) ---------------------------------------------------
|
||||
|
||||
/// 按 Intent 建议模型模态档位。
|
||||
///
|
||||
/// **预留接口**:当前恒返 `None`。待模型模态管理 Phase 落地后补充:
|
||||
/// - Chat/Conversation → `Fast`
|
||||
/// - Code/File/Task/Idea/Search → `Standard`
|
||||
/// - Debug/Http(复杂排查/多跳调用)→ `Heavy`
|
||||
///
|
||||
/// 返回 `None` 时上游应使用默认档位(待模态管理 Phase 定义)。
|
||||
pub fn suggested_model_tier(_intent: &Intent) -> Option<ModelTier> {
|
||||
// TODO(model-tier-phase): 待模型模态管理落地后填实映射。
|
||||
None
|
||||
}
|
||||
|
||||
// ---- 单测 -------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// --- Intent 枚举 ---
|
||||
|
||||
#[test]
|
||||
fn intent_as_str_covers_all_variants() {
|
||||
assert_eq!(Intent::Code.as_str(), "code");
|
||||
assert_eq!(Intent::Debug.as_str(), "debug");
|
||||
assert_eq!(Intent::File.as_str(), "file");
|
||||
assert_eq!(Intent::Project.as_str(), "project");
|
||||
assert_eq!(Intent::Task.as_str(), "task");
|
||||
assert_eq!(Intent::Idea.as_str(), "idea");
|
||||
assert_eq!(Intent::Conversation.as_str(), "conversation");
|
||||
assert_eq!(Intent::Search.as_str(), "search");
|
||||
assert_eq!(Intent::Http.as_str(), "http");
|
||||
assert_eq!(Intent::Chat.as_str(), "chat");
|
||||
assert_eq!(Intent::Unknown.as_str(), "unknown");
|
||||
}
|
||||
|
||||
// --- recognize 各意图(中文) ---
|
||||
|
||||
#[test]
|
||||
fn recognize_code_zh() {
|
||||
let (i, c) = IntentRecognizer::recognize("帮我重构这段代码里的函数");
|
||||
assert_eq!(i, Intent::Code);
|
||||
assert!(c > 0.0 && c <= 1.0);
|
||||
// 命中"重构"+"代码"+"函数",置信度应封顶 1.0
|
||||
assert!((c - 1.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_debug_zh() {
|
||||
let (i, c) = IntentRecognizer::recognize("这个 bug 怎么复现,帮我调试排查一下");
|
||||
// "bug" 在 Code 组,"复现/调试/排查" 在 Debug 组 —— SPECIFIC 组内 Code 命中先于 Debug
|
||||
// Code 命中(bug=1.0) 即胜出本组,不再看 Debug。断言优先级正确:Code 胜出。
|
||||
assert_eq!(i, Intent::Code);
|
||||
assert!(c >= 1.0 - f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_debug_pure() {
|
||||
let (i, _) = IntentRecognizer::recognize("帮我调试这个 panic 堆栈");
|
||||
assert_eq!(i, Intent::Debug);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_file_zh() {
|
||||
let (i, _) = IntentRecognizer::recognize("读取一下这个文件再写入");
|
||||
assert_eq!(i, Intent::File);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_project_zh() {
|
||||
let (i, _) = IntentRecognizer::recognize("创建项目并绑定目录");
|
||||
assert_eq!(i, Intent::Project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_task_zh() {
|
||||
let (i, _) = IntentRecognizer::recognize("把这个任务推进到下一个状态");
|
||||
assert_eq!(i, Intent::Task);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_idea_zh() {
|
||||
let (i, _) = IntentRecognizer::recognize("我有个灵感想评估一下");
|
||||
assert_eq!(i, Intent::Idea);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_search_zh() {
|
||||
let (i, _) = IntentRecognizer::recognize("搜索一下代码里哪里用了 grep");
|
||||
// "搜索" Search(SPECIFIC) 优先于 "代码" Code(SPECIFIC):同组取高分。
|
||||
// "搜索"=1.0 vs "代码"=1.0 平局,按表顺序 Code 在前会胜出 —— 调整用例避免歧义。
|
||||
// 这里只断言落在 SPECIFIC 组之一即可(实际设计具体组互斥,单消息取高分)。
|
||||
assert!(matches!(i, Intent::Search | Intent::Code));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_search_pure() {
|
||||
let (i, _) = IntentRecognizer::recognize("grep 查找关键字");
|
||||
assert_eq!(i, Intent::Search);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_http_zh() {
|
||||
let (i, _) = IntentRecognizer::recognize("调用接口请求这个 api");
|
||||
assert_eq!(i, Intent::Http);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_http_en() {
|
||||
let (i, _) = IntentRecognizer::recognize("fetch the url via http");
|
||||
assert_eq!(i, Intent::Http);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_chat_zh() {
|
||||
let (i, _) = IntentRecognizer::recognize("你好,谢谢你的帮助");
|
||||
assert_eq!(i, Intent::Chat);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_chat_en() {
|
||||
let (i, _) = IntentRecognizer::recognize("hello, thanks!");
|
||||
assert_eq!(i, Intent::Chat);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_conversation() {
|
||||
let (i, _) = IntentRecognizer::recognize("总结一下这段会话历史");
|
||||
assert_eq!(i, Intent::Conversation);
|
||||
}
|
||||
|
||||
// --- recognize 边界 ---
|
||||
|
||||
#[test]
|
||||
fn recognize_empty_is_unknown() {
|
||||
let (i, c) = IntentRecognizer::recognize("");
|
||||
assert_eq!(i, Intent::Unknown);
|
||||
assert_eq!(c, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_whitespace_is_unknown() {
|
||||
let (i, c) = IntentRecognizer::recognize(" \n\t ");
|
||||
assert_eq!(i, Intent::Unknown);
|
||||
assert_eq!(c, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_no_keyword_is_unknown() {
|
||||
let (i, c) = IntentRecognizer::recognize("今天的天气不错啊");
|
||||
assert_eq!(i, Intent::Unknown);
|
||||
assert_eq!(c, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_confidence_bounded() {
|
||||
// 大量命中也应封顶 1.0
|
||||
let (_, c) = IntentRecognizer::recognize("代码 函数 方法 编译 error impl 重构");
|
||||
assert!(c >= 0.0 && c <= 1.0);
|
||||
assert!((c - 1.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
// --- 优先级 ---
|
||||
|
||||
#[test]
|
||||
fn priority_specific_over_entity() {
|
||||
// "代码"(Code/SPECIFIC) + "文件"(File/ENTITY) → SPECIFIC 胜出
|
||||
let (i, _) = IntentRecognizer::recognize("看看这段代码对应的文件");
|
||||
assert_eq!(i, Intent::Code);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn priority_entity_over_generic() {
|
||||
// "任务"(Task/ENTITY) + "帮助"(Chat/GENERIC) → ENTITY 胜出
|
||||
let (i, _) = IntentRecognizer::recognize("帮我处理这个任务,谢谢帮助");
|
||||
assert_eq!(i, Intent::Task);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn case_insensitive_english() {
|
||||
let (i, _) = IntentRecognizer::recognize("HTTP REQUEST to API");
|
||||
assert_eq!(i, Intent::Http);
|
||||
}
|
||||
|
||||
// --- tool_subset_for ---
|
||||
|
||||
#[test]
|
||||
fn subset_code_has_file_and_http() {
|
||||
let s = tool_subset_for(&Intent::Code);
|
||||
assert!(s.contains(&"read_file"));
|
||||
assert!(s.contains(&"write_file"));
|
||||
assert!(s.contains(&"patch_file"));
|
||||
assert!(s.contains(&"http_request"));
|
||||
assert!(!s.contains(&"list_projects"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subset_file_only_file_domain() {
|
||||
let s = tool_subset_for(&Intent::File);
|
||||
assert!(s.contains(&"read_file"));
|
||||
assert!(s.contains(&"search_files"));
|
||||
assert!(!s.contains(&"http_request"));
|
||||
assert!(!s.contains(&"list_projects"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subset_project_is_data() {
|
||||
let s = tool_subset_for(&Intent::Project);
|
||||
assert!(s.contains(&"create_project"));
|
||||
assert!(s.contains(&"bind_directory"));
|
||||
assert!(!s.contains(&"read_file"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subset_task_is_data() {
|
||||
let s = tool_subset_for(&Intent::Task);
|
||||
assert!(s.contains(&"advance_task"));
|
||||
assert!(s.contains(&"create_task"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subset_idea_is_data() {
|
||||
let s = tool_subset_for(&Intent::Idea);
|
||||
assert!(s.contains(&"list_ideas"));
|
||||
assert!(s.contains(&"create_idea"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subset_http_only_http() {
|
||||
let s = tool_subset_for(&Intent::Http);
|
||||
assert_eq!(s, vec!["http_request"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subset_search_has_search_files() {
|
||||
let s = tool_subset_for(&Intent::Search);
|
||||
assert!(s.contains(&"search_files"));
|
||||
// Search 复用 File domain,read_file 等也在
|
||||
assert!(s.contains(&"read_file"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subset_chat_empty_means_full_fallback() {
|
||||
let s = tool_subset_for(&Intent::Chat);
|
||||
assert!(s.is_empty(), "Chat 空=全量 fallback 约定");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subset_unknown_empty_means_full_fallback() {
|
||||
let s = tool_subset_for(&Intent::Unknown);
|
||||
assert!(s.is_empty(), "Unknown 空=全量 fallback 约定");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subset_debug_has_file_and_http() {
|
||||
let s = tool_subset_for(&Intent::Debug);
|
||||
assert!(s.contains(&"read_file"));
|
||||
assert!(s.contains(&"http_request"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subset_no_duplicates_across_domains() {
|
||||
// Code/Debug 跨 file+http domain,确保去重
|
||||
let s = tool_subset_for(&Intent::Code);
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for n in &s {
|
||||
assert!(seen.insert(*n), "重复工具名: {}", n);
|
||||
}
|
||||
}
|
||||
|
||||
// --- ToolDomain 工具名与 registry 对齐(核验) ---
|
||||
|
||||
#[test]
|
||||
fn tool_domain_names_match_registry() {
|
||||
// 抽样核对与 src-tauri tool_registry 实际注册名一致
|
||||
assert!(ToolDomain::File.tools().contains(&"patch_file"));
|
||||
assert!(ToolDomain::File.tools().contains(&"run_command"));
|
||||
assert!(ToolDomain::Data.tools().contains(&"advance_task"));
|
||||
assert!(ToolDomain::Data.tools().contains(&"run_workflow"));
|
||||
assert!(ToolDomain::Data.tools().contains(&"list_trash"));
|
||||
assert_eq!(ToolDomain::Http.tools(), &["http_request"]);
|
||||
}
|
||||
|
||||
// --- suggested_model_tier 预留 ---
|
||||
|
||||
#[test]
|
||||
fn model_tier_always_none_for_now() {
|
||||
// 接口预留:当前所有意图均返 None
|
||||
for i in [
|
||||
Intent::Code,
|
||||
Intent::Debug,
|
||||
Intent::File,
|
||||
Intent::Project,
|
||||
Intent::Task,
|
||||
Intent::Idea,
|
||||
Intent::Conversation,
|
||||
Intent::Search,
|
||||
Intent::Http,
|
||||
Intent::Chat,
|
||||
Intent::Unknown,
|
||||
] {
|
||||
assert_eq!(suggested_model_tier(&i), None, "intent {:?} 应返 None", i);
|
||||
}
|
||||
}
|
||||
|
||||
// --- IntentRecognizer Default ---
|
||||
|
||||
#[test]
|
||||
fn recognizer_is_default_constructible() {
|
||||
let _r = IntentRecognizer::default();
|
||||
// 纯静态方法,实例仅占位
|
||||
let (i, _) = IntentRecognizer::recognize("hi");
|
||||
assert_eq!(i, Intent::Chat);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,10 @@ pub mod anthropic_helpers;
|
||||
pub mod context;
|
||||
pub mod context_helpers;
|
||||
pub mod coordinator;
|
||||
// 会话意图识别层(纯函数,不接入 agentic loop)。依据 docs/02-架构设计/构想审查/
|
||||
// 意图识别层论证-2026-06-19.md。提供 recognize / tool_subset_for / suggested_model_tier。
|
||||
// 待 Phase B+C 完成 + 模型模态管理落地后再接入 agentic loop。
|
||||
pub mod intent;
|
||||
pub mod model_fetch;
|
||||
// model_fetch 的纯逻辑子模块(URL 拼接 / 噪音过滤 / 响应反序列化),
|
||||
// crate 内复用,private mod — 外部经 model_fetch 间接访问,无直接路径需求。
|
||||
|
||||
Reference in New Issue
Block a user