重构: tool_registry 声明式注册基础设施(declare_tool! 宏 + list_projects 试点,零回归)
This commit is contained in:
@@ -0,0 +1,154 @@
|
|||||||
|
//! 声明式工具注册基础设施(tool_registry 拆分第一步)
|
||||||
|
//!
|
||||||
|
//! 背景:`tool_registry.rs` 4224 行,每加一个工具手写一段
|
||||||
|
//! `registry.register(name, desc, object_schema(...), RiskLevel::X, { ... Box::new ... })`,
|
||||||
|
//! 5 段重复样板 + 闭包捕获 + 缩进极易出错。本模块提供声明式宏 `declare_tool!`,
|
||||||
|
//! 把工具定义收敛为「名字 / 描述 / schema / 风险 / handler 块」五要素一行式声明。
|
||||||
|
//!
|
||||||
|
//! 设计取舍(三选一权衡):
|
||||||
|
//! - A. proc-macro `#[ai_tool]` 属性宏:需新建 proc-macro crate + syn/quote 重依赖,违反
|
||||||
|
//! 「不引新依赖」。否决。
|
||||||
|
//! - B. `inventory` crate 自动收集:新外部依赖 + 拉入 linkme/ctor 运行期注册语义,
|
||||||
|
//! 与现有 `register_*(&mut registry)` 命令式收集并存需双重 source-of-truth。否决。
|
||||||
|
//! - C. **`macro_rules!` 声明式宏(本方案)**:零新依赖(纯 std `macro_rules!`),
|
||||||
|
//! 展开为等价的 `registry.register(...)` 调用——与现有 48 个 `register` 调用**完全并存**,
|
||||||
|
//! 旧工具零改动,新工具可选声明式。handler 块就地书写,`$db`/`$registry` 等捕获变量
|
||||||
|
//! 原样透传,语义 1:1 等价(schema/risk/handler 同源不变)。
|
||||||
|
//!
|
||||||
|
//! 试点:`list_projects` 已迁至声明式(见 commands/ai/tools/list_projects.rs),验证编译过 +
|
||||||
|
//! 行为等价 + 48 工具基线不破。其余 47 个工具后续批次渐进迁移,不在此步。
|
||||||
|
//!
|
||||||
|
//! 宏展开示例(输入):
|
||||||
|
//! ```ignore
|
||||||
|
//! declare_tool!(registry, db: Arc<Database>, "list_projects",
|
||||||
|
//! "列出所有项目...", RiskLevel::Low,
|
||||||
|
//! schema: object_schema(vec![("offset", "integer", false), ("limit", "integer", false)]),
|
||||||
|
//! args => {
|
||||||
|
//! let repo = ProjectRepo::new(&db);
|
||||||
|
//! let items = repo.list_active().await?;
|
||||||
|
//! // ...
|
||||||
|
//! Ok(json!({ "items": items, ... }))
|
||||||
|
//! });
|
||||||
|
//! ```
|
||||||
|
//! 展开后等价于现有手写的:
|
||||||
|
//! ```ignore
|
||||||
|
//! registry.register(
|
||||||
|
//! "list_projects", "...",
|
||||||
|
//! object_schema(vec![...]), RiskLevel::Low,
|
||||||
|
//! { let db = db.clone(); Box::new(move |args: serde_json::Value| {
|
||||||
|
//! let db = db.clone();
|
||||||
|
//! Box::pin(async move { /* handler body */ })
|
||||||
|
//! })},
|
||||||
|
//! );
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
/// 声明式注册一个 AI 工具到 `$registry`。
|
||||||
|
///
|
||||||
|
/// 与手写 `registry.register(name, desc, schema, risk, handler)` 语义 1:1 等价,
|
||||||
|
/// 仅消除闭包包装样板(`{ let db = db.clone(); Box::new(move |args| { let db = db.clone(); Box::pin(async move { ... }) }) }`)。
|
||||||
|
///
|
||||||
|
/// 形参:
|
||||||
|
/// - `$registry`: `&mut AiToolRegistry` 注册表句柄。
|
||||||
|
/// - `$capture: $cap_ty`: handler 需捕获的外部变量(如 `db: Arc<Database>`)。宏自动 clone 进闭包,
|
||||||
|
/// handler body 内以 `$capture` 名访问。无捕获工具用 `_` 占位(并保证 body 不引用它)。
|
||||||
|
/// - `$name`: 工具名 `&str`。
|
||||||
|
/// - `$desc`: 工具描述 `&str`(发给 LLM)。
|
||||||
|
/// - `$risk`: `RiskLevel`(如 `RiskLevel::Low`)。
|
||||||
|
/// - `$schema`: 参数 JSON Schema(`serde_json::Value`),常用 `object_schema(...)`。
|
||||||
|
/// - `$handler`: 一个 **block 表达式**(花括号体),返回 `anyhow::Result<serde_json::Value>`。
|
||||||
|
/// 体内在 `$args`(serde_json::Value)与 `$capture`(克隆后的捕获变量)上工作。
|
||||||
|
/// 宏负责把它包进 `async move { ... }` 并 `Box::pin`——故调用方只写「同步语义的体」,
|
||||||
|
/// 不写 `async move`/`Box::pin`/`Box::new` 三层样板。
|
||||||
|
///
|
||||||
|
/// 注意:此宏不替代 `register`,而是包装它——`register` 仍是 `AiToolRegistry` 的唯一注册入口,
|
||||||
|
/// 宏仅是语法糖。现有 48 个手写 `register` 调用不动,新工具改用 `declare_tool!`。
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! declare_tool {
|
||||||
|
(
|
||||||
|
$registry:expr,
|
||||||
|
$capture:ident : $cap_ty:ty,
|
||||||
|
$name:expr,
|
||||||
|
$desc:expr,
|
||||||
|
$risk:expr,
|
||||||
|
schema: $schema:expr,
|
||||||
|
$args:ident => $handler:block
|
||||||
|
) => {{
|
||||||
|
let __cap: $cap_ty = $capture.clone();
|
||||||
|
$registry.register(
|
||||||
|
$name, $desc, $schema, $risk,
|
||||||
|
{
|
||||||
|
let $capture = __cap.clone();
|
||||||
|
::std::boxed::Box::new(move |$args: ::serde_json::Value| {
|
||||||
|
let $capture = $capture.clone();
|
||||||
|
::std::boxed::Box::pin(async move {
|
||||||
|
let $capture: $cap_ty = $capture;
|
||||||
|
$handler
|
||||||
|
})
|
||||||
|
as ::std::pin::Pin<::std::boxed::Box<
|
||||||
|
dyn ::std::future::Future<Output = ::anyhow::Result<::serde_json::Value>>
|
||||||
|
+ ::std::marker::Send,
|
||||||
|
>>
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::ai_tools::{object_schema, AiToolRegistry, RiskLevel};
|
||||||
|
|
||||||
|
/// 声明式宏注册的工具与手写 register 行为等价(name/schema/risk/执行结果一致)。
|
||||||
|
/// 这是「现有工具零回归 + 语义等价」的最小验证(试点 list_projects 迁移的微观镜像)。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn declare_tool_equivalent_to_register() {
|
||||||
|
let mut reg = AiToolRegistry::new();
|
||||||
|
|
||||||
|
// 声明式注册(新基础设施)
|
||||||
|
let counter: std::sync::Arc<std::sync::atomic::AtomicUsize> =
|
||||||
|
std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||||
|
declare_tool!(
|
||||||
|
reg,
|
||||||
|
counter: std::sync::Arc<std::sync::atomic::AtomicUsize>,
|
||||||
|
"echo_decl",
|
||||||
|
"声明式 echo 工具",
|
||||||
|
RiskLevel::Low,
|
||||||
|
schema: object_schema(vec![("msg", "string", true)]),
|
||||||
|
args => {
|
||||||
|
counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
let msg = args["msg"].as_str().unwrap_or("");
|
||||||
|
Ok(serde_json::json!({ "echo": msg }))
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 等价断言:定义层(name 存在 / risk 正确 / schema 透传)
|
||||||
|
assert_eq!(reg.len(), 1, "声明式宏应注册 1 个工具");
|
||||||
|
let tool = reg.get("echo_decl").expect("echo_decl 应已注册");
|
||||||
|
assert_eq!(tool.risk_level, RiskLevel::Low);
|
||||||
|
assert_eq!(tool.definition.function.name, "echo_decl");
|
||||||
|
assert_eq!(tool.definition.function.parameters["properties"]["msg"]["type"], "string");
|
||||||
|
|
||||||
|
// 执行等价:handler 收到 args、捕获变量可访问、返回 handler body 的 Value
|
||||||
|
let out = reg.execute("echo_decl", serde_json::json!({ "msg": "hi" })).await.unwrap();
|
||||||
|
assert_eq!(out["echo"], "hi");
|
||||||
|
assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1, "捕获变量应在执行时递增");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 无捕获工具(纯计算/常量返回)用占位 capture,handler 不引用它。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn declare_tool_no_capture() {
|
||||||
|
let mut reg = AiToolRegistry::new();
|
||||||
|
let dummy: std::sync::Arc<()> = std::sync::Arc::new(()); // 无真实捕获,占位
|
||||||
|
declare_tool!(
|
||||||
|
reg,
|
||||||
|
dummy: std::sync::Arc<()>,
|
||||||
|
"const_tool",
|
||||||
|
"无捕获常量工具",
|
||||||
|
RiskLevel::Medium,
|
||||||
|
schema: object_schema(vec![]),
|
||||||
|
_args => { Ok(serde_json::json!({ "ok": true })) }
|
||||||
|
);
|
||||||
|
let out = reg.execute("const_tool", serde_json::json!({})).await.unwrap();
|
||||||
|
assert_eq!(out["ok"], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
//! df-ai: AI 编排 — LLM Provider、Agent 协调、上下文管理、流式处理、工具注册
|
//! df-ai: AI 编排 — LLM Provider、Agent 协调、上下文管理、流式处理、工具注册
|
||||||
|
|
||||||
pub mod ai_tools;
|
pub mod ai_tools;
|
||||||
|
// 声明式工具注册宏 `declare_tool!`(tool_registry 拆分第一步基础设施)。
|
||||||
|
// macro_rules + #[macro_export]:零新依赖,展开为等价 `register` 调用,与现有 48 工具并存。
|
||||||
|
// 试点:list_projects 已迁至声明式(commands/ai/tools/list_projects.rs),验证可行+零回归。
|
||||||
|
#[macro_use]
|
||||||
|
pub mod ai_tools_decl;
|
||||||
pub mod anthropic_compat;
|
pub mod anthropic_compat;
|
||||||
pub mod anthropic_helpers;
|
pub mod anthropic_helpers;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ pub mod skills;
|
|||||||
pub mod stream_recv;
|
pub mod stream_recv;
|
||||||
pub mod title;
|
pub mod title;
|
||||||
pub mod tool_registry;
|
pub mod tool_registry;
|
||||||
|
// 声明式工具注册集合(tool_registry 拆分第一步 · 试点容器)。
|
||||||
|
// 试点:list_projects 已迁声明式 declare_tool! 宏,其余 47 工具仍在 tool_registry.rs。
|
||||||
|
pub mod tools;
|
||||||
|
|
||||||
use agentic::conv_state::{ConvState, ConvStateStore};
|
use agentic::conv_state::{ConvState, ConvStateStore};
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ use crate::state::AllowedDirs;
|
|||||||
|
|
||||||
/// CRUD list 工具的默认返回上限(防 LLM context 膨胀)
|
/// CRUD list 工具的默认返回上限(防 LLM context 膨胀)
|
||||||
/// 用于 list_projects / list_tasks / list_ideas / list_trash
|
/// 用于 list_projects / list_tasks / list_ideas / list_trash
|
||||||
const MAX_LIST_RESULTS: usize = 50;
|
///
|
||||||
|
/// tool_registry 拆分第一步:list_projects 已迁声明式试点(tools/list_projects.rs),
|
||||||
|
/// 复用本常量保单真相源(避免字面量 50 漂移),故 pub(crate) 暴露给 tools 子模块。
|
||||||
|
pub(crate) const MAX_LIST_RESULTS: usize = 50;
|
||||||
|
|
||||||
/// run_command 默认超时(秒)。LLM 可在 args timeout_secs 覆盖此默认值。
|
/// run_command 默认超时(秒)。LLM 可在 args timeout_secs 覆盖此默认值。
|
||||||
/// 提取为常量便于在超时标注处引用同一来源(F-260616-04)。
|
/// 提取为常量便于在超时标注处引用同一来源(F-260616-04)。
|
||||||
@@ -522,23 +525,11 @@ fn register_data_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
|||||||
/// 抽自 register_data_tools(SMELL-P0-2 续拆),【原样移入】,零行为变更。
|
/// 抽自 register_data_tools(SMELL-P0-2 续拆),【原样移入】,零行为变更。
|
||||||
/// 组内顺序保留原相对顺序(list/update/create/bind/delete/restore/purge/get_count)。
|
/// 组内顺序保留原相对顺序(list/update/create/bind/delete/restore/purge/get_count)。
|
||||||
fn register_project_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
fn register_project_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||||
registry.register(
|
// tool_registry 拆分第一步:list_projects 迁声明式试点(tools/list_projects.rs),
|
||||||
"list_projects", "列出所有项目,支持 offset/limit 分页。返回 items(项目列表)、total(总量)、has_more(是否有更多页)。默认 limit=50",
|
// 改调 declare_tool! 宏注册。其余 7 个项目工具仍手写在下方,零改动(共存)。
|
||||||
df_ai::ai_tools::object_schema(vec![("offset", "integer", false), ("limit", "integer", false)]), RiskLevel::Low,
|
// 行为逐字等价(schema/risk/handler body 同源),基线测试 test_build_ai_tool_registry_baseline_tool_count
|
||||||
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
|
// 仍断言 48 总量。
|
||||||
let db = db.clone();
|
super::tools::list_projects::register(registry, db);
|
||||||
Box::pin(async move {
|
|
||||||
let repo = df_storage::crud::ProjectRepo::new(&db);
|
|
||||||
let items = repo.list_active().await?; // list_active 排除回收站(deleted_at),防 LLM 看到已软删项目
|
|
||||||
let total = items.len();
|
|
||||||
let offset = args["offset"].as_u64().unwrap_or(0) as usize;
|
|
||||||
let limit = args["limit"].as_u64().unwrap_or(MAX_LIST_RESULTS as u64).min(MAX_LIST_RESULTS as u64) as usize;
|
|
||||||
let page_items: Vec<_> = items.into_iter().skip(offset).take(limit).collect();
|
|
||||||
let has_more = (offset + page_items.len()) < total;
|
|
||||||
Ok(serde_json::json!({ "items": page_items, "total": total, "has_more": has_more }))
|
|
||||||
})
|
|
||||||
})},
|
|
||||||
);
|
|
||||||
registry.register(
|
registry.register(
|
||||||
"update_project", "更新项目的指定字段(name/status/description/path/stack),需要提供项目 ID、字段名和新值。绑定代码目录推荐改用 bind_directory",
|
"update_project", "更新项目的指定字段(name/status/description/path/stack),需要提供项目 ID、字段名和新值。绑定代码目录推荐改用 bind_directory",
|
||||||
df_ai::ai_tools::object_schema(vec![("id", "string", true), ("field", "string", true), ("value", "string", true)]),
|
df_ai::ai_tools::object_schema(vec![("id", "string", true), ("field", "string", true), ("value", "string", true)]),
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
//! 试点工具:`list_projects` 声明式注册(tool_registry 拆分第一步 · 迁移试点)
|
||||||
|
//!
|
||||||
|
//! 验证 `declare_tool!` 宏基础设施可行:从原 `register_project_tools` 第 1 个 register 调用
|
||||||
|
//! (tool_registry.rs:524-541)迁移到声明式,行为逐字等价(schema/risk/handler body 同源)。
|
||||||
|
//!
|
||||||
|
//! 迁移策略(并存零回归):
|
||||||
|
//! - 原 `register_project_tools` 内的 list_projects register 调用删除,改调本模块 `register`。
|
||||||
|
//! - 其余 7 个项目工具(update/create/bind/delete/restore/purge/get_count)仍手写在原处,
|
||||||
|
//! 不动(本步只迁 1 个试点,后续批次渐进)。
|
||||||
|
//! - `MAX_LIST_RESULTS` 由 tool_registry.rs 改 `pub(crate)` 暴露,试点复用同一常量(单真相源,
|
||||||
|
//! 避免字面量 50 漂移)。
|
||||||
|
//!
|
||||||
|
//! 等价性验证:基线测试 test_build_ai_tool_registry_baseline_tool_count 仍断言 48 总量;
|
||||||
|
//! execute("list_projects", args) 行为不变(list_active 分页 + has_more 语义)。
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use df_ai::ai_tools::{object_schema, AiToolRegistry, RiskLevel};
|
||||||
|
use df_ai::declare_tool;
|
||||||
|
use df_storage::db::Database;
|
||||||
|
|
||||||
|
use crate::commands::ai::tool_registry::MAX_LIST_RESULTS;
|
||||||
|
|
||||||
|
/// 用声明式宏注册 `list_projects` 到 `$registry`。
|
||||||
|
///
|
||||||
|
/// 与原手写 register(name, desc, schema, risk, handler) 语义 1:1:
|
||||||
|
/// - name/desc/schema 字符串与 JSON Schema 逐字照搬原定义
|
||||||
|
/// - risk = RiskLevel::Low(只读,无副作用)
|
||||||
|
/// - handler body 与原 async move 块逐字一致(list_active 排软删 + offset/limit 分页 +
|
||||||
|
/// has_more 语义 + 默认/上限对齐 MAX_LIST_RESULTS)
|
||||||
|
///
|
||||||
|
/// 唯一差异:闭包包装(`{ let db = db.clone(); Box::new(move |args| { ... Box::pin(async move {...}) }) }`)
|
||||||
|
/// 改由 `declare_tool!` 宏生成,handler body 直接写业务逻辑。
|
||||||
|
pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||||
|
declare_tool!(
|
||||||
|
registry,
|
||||||
|
db: Arc<Database>,
|
||||||
|
"list_projects",
|
||||||
|
"列出所有项目,支持 offset/limit 分页。返回 items(项目列表)、total(总量)、has_more(是否有更多页)。默认 limit=50",
|
||||||
|
RiskLevel::Low,
|
||||||
|
schema: object_schema(vec![("offset", "integer", false), ("limit", "integer", false)]),
|
||||||
|
args => {
|
||||||
|
let repo = df_storage::crud::ProjectRepo::new(&db);
|
||||||
|
// list_active 排除回收站(deleted_at),防 LLM 看到已软删项目
|
||||||
|
let items = repo.list_active().await?;
|
||||||
|
let total = items.len();
|
||||||
|
let offset = args["offset"].as_u64().unwrap_or(0) as usize;
|
||||||
|
let limit = args["limit"]
|
||||||
|
.as_u64()
|
||||||
|
.unwrap_or(MAX_LIST_RESULTS as u64)
|
||||||
|
.min(MAX_LIST_RESULTS as u64) as usize;
|
||||||
|
let page_items: Vec<_> = items.into_iter().skip(offset).take(limit).collect();
|
||||||
|
let has_more = (offset + page_items.len()) < total;
|
||||||
|
Ok(serde_json::json!({ "items": page_items, "total": total, "has_more": has_more }))
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//! 声明式注册工具集合(tool_registry 拆分第一步 · 试点容器)
|
||||||
|
//!
|
||||||
|
//! 容纳迁自 `tool_registry.rs` 手写 register 调用、改用 `declare_tool!` 宏的工具。
|
||||||
|
//! 本步(基础设施 + 1 试点)仅迁 `list_projects`,其余 47 个工具仍在 tool_registry.rs 手写,
|
||||||
|
//! 后续批次渐进迁移(每批 1-3 个,各自独立文件,互不影响)。
|
||||||
|
//!
|
||||||
|
//! 设计:每个工具一个独立文件 + `register(&mut registry, db)` 入口,tool_registry.rs
|
||||||
|
//! 对应 `register_*` 函数改调本模块(原手写 register 调用删除)。这样工具定义与实现同源、
|
||||||
|
//! 文件边界清晰,新增工具只加文件 + 在对应 register_* 接一行,不动 4224 行巨函数主体。
|
||||||
|
|
||||||
|
pub mod list_projects;
|
||||||
Reference in New Issue
Block a user