数据分析层(新增 services/{spectrum,timeseries,analysis}):
- 光谱参数提取 parameters.rs:LAMOST/SDSS/APOGEE/DESI FITS header 跨源归一化读取
Teff/logg/[Fe/H]/RV 及 ASPCAP 20+ 元素丰度,rayon 并发批量提取
- 谱线测量 lines.rs:内置真空/空气波长谱线表,窗口内极值搜索 + 梯形法积分 EW + FWHM,支持自定义谱线
- 交叉相关测速 cross_correlate.rs:对数波长重采样对齐,内置 Pickles 模板按光谱型插值,
CCF 峰值位置提取 RV 及不确定度
- 周期搜索 periodicity.rs:Lomb-Scargle 周期图(含 FAP 误报概率)+ BLS 凌星检测 + 相位折叠
- 变星分类 classification.rs:振幅/偏度/峰度/过零率/eta 等统计特征 + 规则分类(RR Lyrae/Cepheid/食双星/AGN 等)
- SED 拟合 sed.rs:多波段测光黑体模型拟合,输出 T_eff/半径/消光 A_V/光度及不确定度
- 运动学 kinematics.rs:视差+自行+RV → 银河系 UVW 空间速度,含移动星群成员概率(Banyan Σ 简化版)
- 化学丰度 chemistry.rs:[α/Fe] vs [Fe/H] 计算,厚盘/薄盘/晕星族判别
- 观测规划 observability.rs:目标升落时间/airmass/月相影响/曝光时间估算
- 赫罗图 hr_diagram.rs:Gaia TAP CMD 查询,新增 GET /api/analysis/hr-diagram 端点
数据获取层:
- JWST:clients/mast/jwst.rs 封装 MAST Portal 锥形检索 + JwstSpectrumFetcher(NIRSpec/MIRI 光谱)
- X 射线:clients/heasarc 封装 HEASARC TAP(ADQL)+ XMM-Newton/Chandra 光谱 fetcher
- 图像 cutout:SDSS SkyServer/STScI DSS/Pan-STARRS 三源 cutout + 发现图(Finding Chart)生成
- Source 枚举新增 Jwst/Xmm/Chandra 并注册 ObservationRegistry,前端 SOURCE_THEME 与筛选器同步三源
Agent 工具集(24→35):
- 新增 9 个分析工具:get_spectrum_parameters / measure_spectral_lines / measure_radial_velocity /
find_period / classify_variable_star / fit_sed / analyze_kinematics / analyze_abundance_pattern / plan_observation
- batch_process:批量样本"查询→下载→分析→报告"流水线,并发控制防数据源速率限制
- literature_monitor:按 ADS 查询式/时间窗/最低引用数检查最新文献
定时文献同步:
- sync_queries 表新增 is_scheduled 列(migration 20260713)
- 新增 POST /sync/queries/:id/schedule 端点
- 服务启动时拉起每小时调度器,对 is_scheduled=1 的检索配置静默执行 ADS(entdate 增量)/arXiv 增量同步
- search_history 工具收敛至 services/search::search_agent_history,消除 FTS 查询逻辑重复
其他:
- plotting skill 由占位填充为完整科研绘图规范:光谱/光变/折叠曲线/CMD/SED/[α/Fe]/周期图/Mollweide/发现图 9 类 matplotlib 模板
- 删除死代码 streaming_executor.rs(929 行,仅剩 mod 声明引用,无调用方)
- 新增 docs/roadmap-research-features.md 科研功能路线图及实现状态
96 lines
3.5 KiB
Rust
96 lines
3.5 KiB
Rust
// src/agent/runtime/duplicate_detector.rs
|
||
//
|
||
// 同质调用检测器:检测重复的工具调用,防止死循环。
|
||
//
|
||
// 历史实现是单槽(只记住上一次调用),A/B 交替死循环
|
||
// (call A → call B → call A → call B ...)检测不到。
|
||
// 现改为"连续计数 + 滑动窗口计数"双通道:
|
||
// - 连续通道:连续相同调用 ≥ threshold(原语义,立即触发)
|
||
// - 窗口通道:最近 threshold*2 次调用中同一调用出现 ≥ threshold+2 次
|
||
// (捕获 A/B 交替;门槛略高以放过合理的重复只读调用)
|
||
|
||
use std::collections::VecDeque;
|
||
|
||
/// 同质调用检测器
|
||
#[derive(Debug, Default)]
|
||
pub struct DuplicateDetector {
|
||
/// 最近调用的滑动窗口(name, arguments)
|
||
recent: VecDeque<(String, String)>,
|
||
}
|
||
|
||
impl DuplicateDetector {
|
||
/// 记录一次调用,返回是否检测到死循环
|
||
pub fn record(&mut self, tool_name: &str, arguments: &str, threshold: usize) -> bool {
|
||
let key = (tool_name.to_string(), arguments.to_string());
|
||
let window = (threshold * 2).max(4);
|
||
|
||
self.recent.push_back(key.clone());
|
||
while self.recent.len() > window {
|
||
self.recent.pop_front();
|
||
}
|
||
|
||
// 连续通道:尾部连续出现次数
|
||
let consecutive = self.recent.iter().rev().take_while(|k| *k == &key).count();
|
||
if threshold >= 2 && consecutive >= threshold {
|
||
return true;
|
||
}
|
||
|
||
// 窗口通道:同一调用在窗口内出现次数(捕获 A/B 交替)。
|
||
// 门槛与 threshold 相同:纯交替下窗口内单键最多出现 window/2 =
|
||
// threshold 次,恰好可达;合理的间隔重复(≤ threshold-1 次)不受影响。
|
||
let occurrences = self.recent.iter().filter(|k| **k == key).count();
|
||
threshold >= 2 && occurrences >= threshold
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_consecutive_duplicates_detected() {
|
||
let mut det = DuplicateDetector::default();
|
||
assert!(!det.record("search_papers", "q=1", 3));
|
||
assert!(!det.record("search_papers", "q=1", 3));
|
||
assert!(det.record("search_papers", "q=1", 3));
|
||
}
|
||
|
||
#[test]
|
||
fn test_alternating_loop_detected() {
|
||
// A/B 交替:单槽实现检测不到,窗口通道应捕获
|
||
let mut det = DuplicateDetector::default();
|
||
let mut tripped = false;
|
||
for i in 0..6 {
|
||
let a = det.record("tool_a", "{}", 3);
|
||
let b = det.record("tool_b", "{}", 3);
|
||
if a || b {
|
||
// 交替 5 次出现(threshold+2 = 5)后触发
|
||
assert!(i >= 1, "不应过早触发 (round {})", i);
|
||
tripped = true;
|
||
break;
|
||
}
|
||
}
|
||
assert!(tripped, "A/B 交替死循环应被检测到");
|
||
}
|
||
|
||
#[test]
|
||
fn test_spaced_repeats_not_detected() {
|
||
// 合理的重复只读调用(间隔其他调用)不应触发
|
||
let mut det = DuplicateDetector::default();
|
||
det.record("read_file", "a.rs", 3);
|
||
det.record("grep_files", "pat", 3);
|
||
det.record("read_file", "b.rs", 3);
|
||
det.record("glob_files", "*.rs", 3);
|
||
let tripped = det.record("read_file", "a.rs", 3);
|
||
assert!(!tripped);
|
||
}
|
||
|
||
#[test]
|
||
fn test_different_args_not_duplicates() {
|
||
let mut det = DuplicateDetector::default();
|
||
for q in ["q=1", "q=2", "q=3", "q=4", "q=5", "q=6", "q=7", "q=8"] {
|
||
assert!(!det.record("search_papers", q, 3));
|
||
}
|
||
}
|
||
}
|