- AppState 拆分为 LlmState/DataSourceState/SessionState 三个子结构,消除 50+ 平铺字段 - 新增 ServiceError 结构化错误类型替代 handler 中的 msg.contains() 字符串匹配 - 移除 api::handlers 兼容命名空间,路由直接引用 agent/auth/papers 等模块 - login_rate_limiter/upload_rate_limiter 从 Mutex\<HashMap\> 迁移为 DashMap(无锁) - 新增 session_last_active: DashMap\<String, AtomicU64\>,auth 中间件快速路径免写锁 - 会话过期清理改为按间隔触发(300s),避免每次请求全表扫描 - MAX_SESSIONS 1000→10000,SSE 广播通道 256→1024 - AgentTool trait 新增 is_internal()/display_name(),SSE 事件携带工具元数据 - 新增 GET /chat/tools 端点暴露注册工具列表 - pending_questions/pending_permissions 增加 created_at 时间戳,自动清理过期条目(10min TTL) - Agent 超时现在正确 abort 后台任务并设置取消令牌 - FITS 解析: APOGEE/DESI 改用 read_image+切片替代 read_rows(修复 fitsio panic) - ZTF: CIRCLE 参数分隔符 +→空格(修复 IRSA 400),半径自动裁剪至硬上限 - MAST TESS: parse_tic_json 兼容数组/对象两种 API 响应格式 - 统一检索: per_target_limit 默认 50→1,sources 支持 per-source release/version - Gaia 测光从 VizieR 镜像切换至官方 TAP 服务 - 向量化降级从串行改为并发 5 条/批(buffer_unordered) - 混合检索 RRF 合并从借用改为 owned RetrievalResult - PDF 中间件: URL 解码 %2F/%2E 后判扩展名;文件名过滤非 ASCII + 禁 \ 防头注入 - chat_agent 日志截断问题内容至 50 字符;list_sessions 强制 limit clamp - 设计令牌三层架构: primitive→semantic→component,浅色暖纸张学术/暗色 Night Indigo - useTheme hook + ThemeToggle 侧边栏组件 + main.tsx 防 FOUC 初始化 - 全组件从硬编码 slate 色迁移至语义令牌(bg-surface/text-content/border-subtle 等) - 新增 ToastContainer 非阻塞通知系统 - deploy.sh 引入 SSH ControlMaster 单次密码复用
206 lines
6.9 KiB
Rust
206 lines
6.9 KiB
Rust
// src/agent/tools/astro/research/paper_reader_tools.rs
|
||
//
|
||
// 文献内容读取与大纲提取的智能体工具定义。
|
||
|
||
use async_trait::async_trait;
|
||
use serde_json::json;
|
||
use tracing::info;
|
||
|
||
use crate::agent::tools::{AgentTool, ToolContext, ToolOutput};
|
||
|
||
// ==========================================
|
||
// 1. GetPaperOutlineTool (获取文献大纲目录)
|
||
// ==========================================
|
||
|
||
#[derive(Default)]
|
||
pub struct GetPaperOutlineTool;
|
||
|
||
#[async_trait]
|
||
impl AgentTool for GetPaperOutlineTool {
|
||
fn name(&self) -> &'static str {
|
||
"get_paper_outline"
|
||
}
|
||
|
||
fn display_name(&self) -> &str {
|
||
"文献提纲提取"
|
||
}
|
||
|
||
fn description(&self) -> &'static str {
|
||
"获取文献的章节大纲目录,列出所有章节的序号和标题。\n\
|
||
在读取文献内容前,强烈建议先使用此工具了解章节结构,以便按需读取,节约 token。"
|
||
}
|
||
|
||
fn parameters(&self) -> serde_json::Value {
|
||
json!({
|
||
"type": "object",
|
||
"properties": {
|
||
"bibcode": {
|
||
"type": "string",
|
||
"description": "文献的唯一标识符(bibcode,如 2006BaltA..15...69W)"
|
||
}
|
||
},
|
||
"required": ["bibcode"]
|
||
})
|
||
}
|
||
|
||
async fn execute(&self, args: serde_json::Value, ctx: &ToolContext) -> ToolOutput {
|
||
let bibcode = match args.get("bibcode").and_then(|b| b.as_str()) {
|
||
Some(b) => b.to_string(),
|
||
None => return ToolOutput::error("缺少必需参数 'bibcode'"),
|
||
};
|
||
|
||
info!("[GetPaperOutline] 提取大纲: {}", bibcode);
|
||
let state = &ctx.app_state;
|
||
|
||
let mode = crate::services::paper::ReadMode::Outline { max_level: Some(3) };
|
||
|
||
match crate::services::paper::read_paper_content(
|
||
&state.db,
|
||
&state.config.storage.library_dir,
|
||
&bibcode,
|
||
mode,
|
||
)
|
||
.await
|
||
{
|
||
Ok(res) => {
|
||
let sections = res.outline.unwrap_or_default();
|
||
if sections.is_empty() {
|
||
return ToolOutput::success(
|
||
"该文献未检测到章节标题(## 或 ### 格式)。请直接读取全文。",
|
||
json!({ "bibcode": bibcode, "sections": [] }),
|
||
);
|
||
}
|
||
|
||
let display_sections: Vec<serde_json::Value> = sections
|
||
.iter()
|
||
.map(|s| {
|
||
json!({
|
||
"index": s.index,
|
||
"level": s.level,
|
||
"heading": s.heading.clone(),
|
||
})
|
||
})
|
||
.collect();
|
||
|
||
let header = format!("文献 {} 章节大纲(共 {} 节):\n", bibcode, sections.len());
|
||
let footer = "\n\n使用 get_paper_content 并传入 section_index 或 section_name 读取指定章节内容。";
|
||
|
||
ToolOutput::success(
|
||
header + &res.content + footer,
|
||
json!({
|
||
"bibcode": bibcode,
|
||
"section_count": sections.len(),
|
||
"sections": display_sections
|
||
}),
|
||
)
|
||
}
|
||
Err(e) => ToolOutput::error(e.to_string()),
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// 2. GetPaperContentTool (获取文献具体内容)
|
||
// ==========================================
|
||
|
||
#[derive(Default)]
|
||
pub struct GetPaperContentTool;
|
||
|
||
#[async_trait]
|
||
impl AgentTool for GetPaperContentTool {
|
||
fn name(&self) -> &'static str {
|
||
"get_paper_content"
|
||
}
|
||
|
||
fn display_name(&self) -> &str {
|
||
"文献内容读取"
|
||
}
|
||
|
||
fn description(&self) -> &'static str {
|
||
"获取文献的具体内容(全文或指定章节内容)。\n\
|
||
可传入 section_index 或 section_name 指定仅读取某一章节。\n\
|
||
先使用 get_paper_outline 获取章节结构后按需读取,可大幅减少 token 消耗。"
|
||
}
|
||
|
||
fn parameters(&self) -> serde_json::Value {
|
||
json!({
|
||
"type": "object",
|
||
"properties": {
|
||
"bibcode": {
|
||
"type": "string",
|
||
"description": "文献的唯一标识符(bibcode,如 2006BaltA..15...69W)"
|
||
},
|
||
"section_index": {
|
||
"type": "integer",
|
||
"description": "章节序号(0-based)。从 get_paper_outline 返回的 sections[index] 获取。与 section_name 二选一"
|
||
},
|
||
"section_name": {
|
||
"type": "string",
|
||
"description": "章节标题文本(支持模糊匹配,如 'introduction')。与 section_index 二选一"
|
||
}
|
||
},
|
||
"required": ["bibcode"]
|
||
})
|
||
}
|
||
|
||
async fn execute(&self, args: serde_json::Value, ctx: &ToolContext) -> ToolOutput {
|
||
let bibcode = match args.get("bibcode").and_then(|b| b.as_str()) {
|
||
Some(b) => b.to_string(),
|
||
None => return ToolOutput::error("缺少必需参数 'bibcode'"),
|
||
};
|
||
|
||
let section_index = args
|
||
.get("section_index")
|
||
.and_then(|s| s.as_u64())
|
||
.map(|n| n as usize);
|
||
let section_name = args
|
||
.get("section_name")
|
||
.and_then(|s| s.as_str())
|
||
.map(String::from);
|
||
|
||
info!(
|
||
"[GetPaperContent] bibcode={}, section_index={:?}, section_name={:?}",
|
||
bibcode, section_index, section_name
|
||
);
|
||
|
||
let state = &ctx.app_state;
|
||
|
||
let mode = if let Some(idx) = section_index {
|
||
crate::services::paper::ReadMode::SectionIndex(idx)
|
||
} else if let Some(name) = section_name {
|
||
crate::services::paper::ReadMode::SectionName(name)
|
||
} else {
|
||
crate::services::paper::ReadMode::Full {
|
||
include_translation: false,
|
||
}
|
||
};
|
||
|
||
let label = match &mode {
|
||
crate::services::paper::ReadMode::SectionIndex(idx) => Some(format!("#{}", idx)),
|
||
crate::services::paper::ReadMode::SectionName(name) => Some(name.clone()),
|
||
_ => None,
|
||
};
|
||
let is_full = matches!(mode, crate::services::paper::ReadMode::Full { .. });
|
||
|
||
match crate::services::paper::read_paper_content(
|
||
&state.db,
|
||
&state.config.storage.library_dir,
|
||
&bibcode,
|
||
mode,
|
||
)
|
||
.await
|
||
{
|
||
Ok(res) => ToolOutput::success(
|
||
res.content.clone(),
|
||
json!({
|
||
"bibcode": bibcode,
|
||
"chars": res.content.len(),
|
||
"section": label,
|
||
"full_text": is_full
|
||
}),
|
||
),
|
||
Err(e) => ToolOutput::error(e.to_string()),
|
||
}
|
||
}
|
||
}
|