// src/agent/tools/search_history.rs // // search_history 工具:搜索跨会话历史记录(P3 FTS5 集成)。 // Agent 可用此工具查找之前研究过的主题、已发现的结论。 use async_trait::async_trait; use serde_json::json; use super::{AgentTool, ToolContext, ToolOutput}; pub struct SearchHistoryTool; #[async_trait] impl AgentTool for SearchHistoryTool { fn name(&self) -> &str { "search_history" } fn display_name(&self) -> &str { "检索历史" } fn is_internal(&self) -> bool { true } fn description(&self) -> &str { "搜索之前会话的历史记录,查找已研究过的主题、已发现的结论、已下载的文献。\ 使用此工具可以避免重复研究已经完成的工作。" } fn parameters(&self) -> serde_json::Value { json!({ "type": "object", "properties": { "query": { "type": "string", "description": "搜索关键词(支持 FTS5 查询语法,多个词用空格分隔)" }, "scope": { "type": "string", "enum": ["sessions", "messages", "all"], "description": "搜索范围:sessions=会话标题/摘要, messages=消息内容, all=全部", "default": "all" } }, "required": ["query"] }) } fn is_concurrency_safe(&self, _args: &serde_json::Value) -> bool { true } fn is_readonly(&self) -> bool { true } fn defer_loading(&self) -> bool { true } async fn execute(&self, args: serde_json::Value, ctx: &ToolContext) -> ToolOutput { let raw_query = match args.get("query").and_then(|v| v.as_str()) { Some(q) if !q.is_empty() => q, _ => return ToolOutput::error("缺少 query 参数"), }; let query = crate::services::search::sanitize_fts5_query(raw_query); let scope = args.get("scope").and_then(|v| v.as_str()).unwrap_or("all"); let db = &ctx.app_state.db; let mut results = Vec::new(); if scope == "all" || scope == "sessions" { match sqlx::query_as::<_, (String, String, String)>( "SELECT s.session_id, s.title, \ snippet(agent_sessions_fts, 1, '', '', '...', 40) \ FROM agent_sessions_fts fts \ JOIN agent_sessions s ON s.session_id = fts.session_id \ WHERE agent_sessions_fts MATCH $1 ORDER BY rank LIMIT 10", ) .bind(&query) .fetch_all(db) .await { Ok(rows) => { for (sid, title, snippet) in rows { results.push(json!({ "type": "session", "session_id": sid, "title": title, "snippet": snippet, })); } } Err(e) => { tracing::warn!("[SearchHistory] 搜索会话失败: {}", e); } } } if scope == "all" || scope == "messages" { match sqlx::query_as::<_, (String, String, String, String)>( "SELECT fts.session_id, s.title, \ snippet(agent_messages_fts, 2, '', '', '...', 80), fts.role \ FROM agent_messages_fts fts \ JOIN agent_sessions s ON s.session_id = fts.session_id \ JOIN agent_messages m ON m.rowid = fts.rowid \ WHERE agent_messages_fts MATCH $1 AND m.active = 1 \ ORDER BY rank LIMIT 10", ) .bind(&query) .fetch_all(db) .await { Ok(rows) => { for (sid, title, snippet, role) in rows { results.push(json!({ "type": format!("message/{}", role), "session_id": sid, "title": title, "snippet": snippet, })); } } Err(e) => { tracing::warn!("[SearchHistory] 搜索消息失败: {}", e); } } } if results.is_empty() { ToolOutput::success( format!("未找到与 \"{}\" 相关的历史记录。", query), json!({"query": query, "results": [], "count": 0}), ) } else { let count = results.len(); ToolOutput::success( format!("找到 {} 条与 \"{}\" 相关的历史记录", count, query), json!({"query": query, "results": results, "count": count}), ) } } }