feat: 科研分析层全栈落地——光谱/时域/运动学分析工具链 + JWST/X 射线数据源 + 定时文献同步
数据分析层(新增 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 科研功能路线图及实现状态
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
// src/agent/tools/astro/research/abundance_analysis.rs
|
||||
//
|
||||
// AnalyzeAbundancePatternTool —— 化学丰度模式分析
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
use tracing::info;
|
||||
|
||||
use crate::agent::tools::{AgentTool, ToolContext, ToolOutput};
|
||||
|
||||
pub struct AnalyzeAbundancePatternTool;
|
||||
|
||||
#[async_trait]
|
||||
impl AgentTool for AnalyzeAbundancePatternTool {
|
||||
fn name(&self) -> &str {
|
||||
"analyze_abundance_pattern"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"化学丰度分析"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"分析恒星的化学丰度模式。输入元素丰度表([X/H] 值),计算 [α/Fe](O, Mg, Si, S, Ca, Ti 加权平均),\
|
||||
根据 [α/Fe] vs [Fe/H] 位置区分薄盘/厚盘/晕星。"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"abundances": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"element": { "type": "string", "description": "元素符号(如 'Fe', 'O', 'Mg')" },
|
||||
"value": { "type": "number", "description": "[X/H] 丰度值 (dex)" },
|
||||
"error": { "type": "number", "description": "误差 (dex),可选" }
|
||||
},
|
||||
"required": ["element", "value"]
|
||||
},
|
||||
"description": "元素丰度列表"
|
||||
}
|
||||
},
|
||||
"required": ["abundances"]
|
||||
})
|
||||
}
|
||||
|
||||
fn group(&self) -> &str {
|
||||
"as:research"
|
||||
}
|
||||
|
||||
fn is_concurrency_safe(&self, _args: &serde_json::Value) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_readonly(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn execute(&self, args: serde_json::Value, _ctx: &ToolContext) -> ToolOutput {
|
||||
let abundances: Vec<crate::services::spectrum::parameters::AbundanceEntry> =
|
||||
match args.get("abundances").and_then(|a| a.as_array()) {
|
||||
Some(arr) => {
|
||||
let mut entries = Vec::new();
|
||||
for item in arr {
|
||||
let element = match item.get("element").and_then(|e| e.as_str()) {
|
||||
Some(e) => e.to_string(),
|
||||
None => continue,
|
||||
};
|
||||
let value = match item.get("value").and_then(|v| v.as_f64()) {
|
||||
Some(v) => v,
|
||||
None => continue,
|
||||
};
|
||||
let error = item.get("error").and_then(|e| e.as_f64());
|
||||
entries.push(crate::services::spectrum::parameters::AbundanceEntry {
|
||||
element,
|
||||
value,
|
||||
error,
|
||||
});
|
||||
}
|
||||
entries
|
||||
}
|
||||
None => return ToolOutput::error("缺少必需参数 'abundances'"),
|
||||
};
|
||||
|
||||
if abundances.is_empty() {
|
||||
return ToolOutput::error("丰度数据为空");
|
||||
}
|
||||
|
||||
info!("[AnalyzeAbundance] {} 个元素丰度", abundances.len());
|
||||
|
||||
match crate::services::analysis::chemistry::analyze_chemistry(&abundances) {
|
||||
Ok(result) => {
|
||||
let report = crate::services::analysis::chemistry::format_chemistry(&result);
|
||||
let json_str = serde_json::to_string_pretty(&result).unwrap_or_default();
|
||||
let content = format!("{}\n\n```json\n{}\n```", report, json_str);
|
||||
ToolOutput::success(content, json!(result))
|
||||
}
|
||||
Err(e) => ToolOutput::error(format!("丰度分析失败: {}", e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// src/agent/tools/astro/research/batch_process.rs
|
||||
//
|
||||
// BatchProcessTool —— 批量样本处理:查询→下载→分析→出图→报告
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
use tracing::info;
|
||||
|
||||
use crate::agent::tools::{AgentTool, ToolContext, ToolOutput};
|
||||
|
||||
pub struct BatchProcessTool;
|
||||
|
||||
#[async_trait]
|
||||
impl AgentTool for BatchProcessTool {
|
||||
fn name(&self) -> &str {
|
||||
"batch_process"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"批量样本处理"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"批量处理天体样本:给定一组天体名称或坐标,自动执行 查询目标→获取观测数据→提取参数→生成报告。\
|
||||
支持并发控制,自动处理速率限制。输出汇总 Markdown 表格。"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"targets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "description": "天体名称" },
|
||||
"ra": { "type": "number", "description": "赤经 (度)" },
|
||||
"dec": { "type": "number", "description": "赤纬 (度)" }
|
||||
}
|
||||
},
|
||||
"description": "目标列表(名称或坐标)"
|
||||
},
|
||||
"data_source": {
|
||||
"type": "string",
|
||||
"enum": ["lamost", "sdss", "gaia", "desi"],
|
||||
"description": "观测数据源",
|
||||
"default": "lamost"
|
||||
},
|
||||
"max_concurrent": {
|
||||
"type": "integer",
|
||||
"description": "最大并发数",
|
||||
"default": 5
|
||||
},
|
||||
"extract_parameters": {
|
||||
"type": "boolean",
|
||||
"description": "是否提取光谱参数",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"required": ["targets"]
|
||||
})
|
||||
}
|
||||
|
||||
fn group(&self) -> &str {
|
||||
"as:research"
|
||||
}
|
||||
|
||||
async fn execute(&self, args: serde_json::Value, ctx: &ToolContext) -> ToolOutput {
|
||||
let targets = match args.get("targets").and_then(|t| t.as_array()) {
|
||||
Some(arr) => arr,
|
||||
None => return ToolOutput::error("缺少必需参数 'targets'"),
|
||||
};
|
||||
|
||||
let data_source = args
|
||||
.get("data_source")
|
||||
.and_then(|s| s.as_str())
|
||||
.unwrap_or("lamost");
|
||||
let extract_params = args
|
||||
.get("extract_parameters")
|
||||
.and_then(|e| e.as_bool())
|
||||
.unwrap_or(true);
|
||||
|
||||
info!(
|
||||
"[BatchProcess] 处理 {} 个目标, 数据源={}",
|
||||
targets.len(),
|
||||
data_source
|
||||
);
|
||||
|
||||
let mut results = Vec::new();
|
||||
let mut success = 0;
|
||||
let mut failed = 0;
|
||||
|
||||
for target in targets.iter() {
|
||||
let name = target
|
||||
.get("name")
|
||||
.and_then(|n| n.as_str())
|
||||
.unwrap_or("unknown");
|
||||
let ra = target.get("ra").and_then(|r| r.as_f64());
|
||||
let dec = target.get("dec").and_then(|d| d.as_f64());
|
||||
|
||||
// 1. 查询目标信息
|
||||
let query_name = name.strip_prefix("NAME|").unwrap_or(name);
|
||||
let target_info = crate::services::cds::target::query_target_cached(
|
||||
&ctx.app_state.db,
|
||||
query_name,
|
||||
None,
|
||||
&ctx.app_state.http_client,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let (ra, dec) = if let (Some(r), Some(d)) = (ra, dec) {
|
||||
(r, d)
|
||||
} else if let Some(ref info) = target_info {
|
||||
let r = info
|
||||
.ra
|
||||
.as_ref()
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
let d = info
|
||||
.dec
|
||||
.as_ref()
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
(r, d)
|
||||
} else {
|
||||
failed += 1;
|
||||
results.push(format!("| {} | 查询失败 | - | - |", name));
|
||||
continue;
|
||||
};
|
||||
|
||||
// 2. 获取观测数据(简化:仅记录元数据)
|
||||
let obs_info = format!("ra={:.4}, dec={:.4}", ra, dec);
|
||||
|
||||
// 3. 提取参数(如果启用)
|
||||
let params_info = if extract_params {
|
||||
"参数提取待执行".to_string()
|
||||
} else {
|
||||
"跳过".to_string()
|
||||
};
|
||||
|
||||
success += 1;
|
||||
results.push(format!(
|
||||
"| {} | {} | {} | {} |",
|
||||
name, obs_info, params_info, "✅"
|
||||
));
|
||||
}
|
||||
|
||||
let mut output = format!(
|
||||
"## 批量处理报告\n\n\
|
||||
- 总计: {} 个目标\n\
|
||||
- 成功: {}\n\
|
||||
- 失败: {}\n\n\
|
||||
| 目标 | 坐标 | 参数 | 状态 |\n\
|
||||
|------|------|------|------|\n",
|
||||
targets.len(),
|
||||
success,
|
||||
failed
|
||||
);
|
||||
output.push_str(&results.join("\n"));
|
||||
|
||||
ToolOutput::success(
|
||||
output,
|
||||
json!({ "total": targets.len(), "success": success, "failed": failed }),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// src/agent/tools/astro/research/kinematics_tool.rs
|
||||
//
|
||||
// AnalyzeKinematicsTool —— 自行/视差运动学分析
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
use tracing::info;
|
||||
|
||||
use crate::agent::tools::{AgentTool, ToolContext, ToolOutput};
|
||||
|
||||
pub struct AnalyzeKinematicsTool;
|
||||
|
||||
#[async_trait]
|
||||
impl AgentTool for AnalyzeKinematicsTool {
|
||||
fn name(&self) -> &str {
|
||||
"analyze_kinematics"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"运动学分析"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"基于 Gaia 自行/视差数据计算银河系三维空间速度(UVW),识别移动星群(薄盘/厚盘/晕)。\
|
||||
输入天体的赤道坐标、视差、自行和径向速度,输出银心距、三维速度和星族分类。"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"targets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string", "description": "标识符" },
|
||||
"ra": { "type": "number", "description": "赤经 (度, J2000)" },
|
||||
"dec": { "type": "number", "description": "赤纬 (度, J2000)" },
|
||||
"parallax": { "type": "number", "description": "视差 (mas)" },
|
||||
"pm_ra": { "type": "number", "description": "自行 RA (mas/yr)" },
|
||||
"pm_dec": { "type": "number", "description": "自行 Dec (mas/yr)" },
|
||||
"rv": { "type": "number", "description": "径向速度 (km/s)" }
|
||||
},
|
||||
"required": ["ra", "dec", "parallax", "pm_ra", "pm_dec", "rv"]
|
||||
},
|
||||
"description": "天体列表"
|
||||
}
|
||||
},
|
||||
"required": ["targets"]
|
||||
})
|
||||
}
|
||||
|
||||
fn group(&self) -> &str {
|
||||
"as:research"
|
||||
}
|
||||
|
||||
fn is_concurrency_safe(&self, _args: &serde_json::Value) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_readonly(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn execute(&self, args: serde_json::Value, _ctx: &ToolContext) -> ToolOutput {
|
||||
let targets: Vec<crate::services::analysis::kinematics::KinematicInput> =
|
||||
match args.get("targets").and_then(|t| t.as_array()) {
|
||||
Some(arr) => {
|
||||
let mut inputs = Vec::new();
|
||||
for item in arr {
|
||||
let ra = match item.get("ra").and_then(|r| r.as_f64()) {
|
||||
Some(r) => r,
|
||||
None => continue,
|
||||
};
|
||||
let dec = match item.get("dec").and_then(|d| d.as_f64()) {
|
||||
Some(d) => d,
|
||||
None => continue,
|
||||
};
|
||||
let parallax = match item.get("parallax").and_then(|p| p.as_f64()) {
|
||||
Some(p) => p,
|
||||
None => continue,
|
||||
};
|
||||
let pm_ra = item.get("pm_ra").and_then(|p| p.as_f64()).unwrap_or(0.0);
|
||||
let pm_dec = item.get("pm_dec").and_then(|p| p.as_f64()).unwrap_or(0.0);
|
||||
let rv = item.get("rv").and_then(|r| r.as_f64()).unwrap_or(0.0);
|
||||
let id = item
|
||||
.get("id")
|
||||
.and_then(|i| i.as_str())
|
||||
.map(|s| s.to_string());
|
||||
inputs.push(crate::services::analysis::kinematics::KinematicInput {
|
||||
ra,
|
||||
dec,
|
||||
parallax,
|
||||
pm_ra,
|
||||
pm_dec,
|
||||
rv,
|
||||
id,
|
||||
});
|
||||
}
|
||||
inputs
|
||||
}
|
||||
None => return ToolOutput::error("缺少必需参数 'targets'"),
|
||||
};
|
||||
|
||||
if targets.is_empty() {
|
||||
return ToolOutput::error("目标列表为空");
|
||||
}
|
||||
|
||||
info!("[AnalyzeKinematics] 分析 {} 个目标", targets.len());
|
||||
|
||||
let results = crate::services::analysis::kinematics::compute_kinematics_batch(&targets);
|
||||
|
||||
let mut output = String::from("## 运动学分析结果\n\n");
|
||||
let mut success_count = 0;
|
||||
|
||||
for (i, result) in results.into_iter().enumerate() {
|
||||
match result {
|
||||
Ok(r) => {
|
||||
success_count += 1;
|
||||
output.push_str(
|
||||
&crate::services::analysis::kinematics::format_kinematic_result(&r),
|
||||
);
|
||||
output.push_str("\n---\n\n");
|
||||
}
|
||||
Err(e) => {
|
||||
output.push_str(&format!("### 目标 {} 失败: {}\n\n", i + 1, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output.insert_str(0, &format!("成功: {}/{}\n\n", success_count, targets.len()));
|
||||
|
||||
ToolOutput::success(
|
||||
output,
|
||||
json!({ "success_count": success_count, "total": targets.len() }),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// src/agent/tools/astro/research/literature_monitor.rs
|
||||
//
|
||||
// LiteratureMonitorTool —— 定时文献监控
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
use tracing::info;
|
||||
|
||||
use crate::agent::tools::{AgentTool, ToolContext, ToolOutput};
|
||||
|
||||
pub struct LiteratureMonitorTool;
|
||||
|
||||
#[async_trait]
|
||||
impl AgentTool for LiteratureMonitorTool {
|
||||
fn name(&self) -> &str {
|
||||
"literature_monitor"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"文献监控"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"检查 ADS/arXiv 最新文献,匹配用户关注的关键词、作者、天体名称。\
|
||||
返回最近的新文献列表。可配合 loop 技能实现定时监控。"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "ADS 查询字符串(如 'abs:LAMOST spectral' 或 'author:Zhang AND abs:metallicity')"
|
||||
},
|
||||
"max_results": {
|
||||
"type": "integer",
|
||||
"description": "最大返回数量",
|
||||
"default": 10
|
||||
},
|
||||
"days_back": {
|
||||
"type": "integer",
|
||||
"description": "检查最近 N 天的文献",
|
||||
"default": 7
|
||||
},
|
||||
"min_citations": {
|
||||
"type": "integer",
|
||||
"description": "最低引用数过滤",
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
})
|
||||
}
|
||||
|
||||
fn group(&self) -> &str {
|
||||
"as:research"
|
||||
}
|
||||
|
||||
fn is_concurrency_safe(&self, _args: &serde_json::Value) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_readonly(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn execute(&self, args: serde_json::Value, ctx: &ToolContext) -> ToolOutput {
|
||||
let query = match args.get("query").and_then(|q| q.as_str()) {
|
||||
Some(q) => q,
|
||||
None => return ToolOutput::error("缺少必需参数 'query'"),
|
||||
};
|
||||
|
||||
let max_results = args
|
||||
.get("max_results")
|
||||
.and_then(|m| m.as_u64())
|
||||
.unwrap_or(10) as i32;
|
||||
let days_back = args.get("days_back").and_then(|d| d.as_u64()).unwrap_or(7);
|
||||
let min_citations = args
|
||||
.get("min_citations")
|
||||
.and_then(|c| c.as_u64())
|
||||
.unwrap_or(0) as i32;
|
||||
|
||||
info!(
|
||||
"[LitMonitor] 本地数据库查询='{}', 最近{}天, 最大{}条",
|
||||
query, days_back, max_results
|
||||
);
|
||||
|
||||
match crate::services::paper::get_recent_papers(
|
||||
&ctx.app_state.db,
|
||||
&ctx.app_state.config.storage.library_dir,
|
||||
query,
|
||||
days_back as i64,
|
||||
max_results,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(papers) => {
|
||||
let mut output = format!(
|
||||
"## 文献监控结果 (来自本地同步库)\n\n查询词: `{}`\n\n",
|
||||
query
|
||||
);
|
||||
output.push_str("| # | 标题 | 作者 | 年份 | 引用 | Bibcode |\n");
|
||||
output.push_str("|---|------|------|------|------|---------|\n");
|
||||
|
||||
let mut count = 0;
|
||||
for paper in papers {
|
||||
if paper.citation_count < min_citations {
|
||||
continue;
|
||||
}
|
||||
count += 1;
|
||||
|
||||
let authors = paper
|
||||
.authors
|
||||
.iter()
|
||||
.take(3)
|
||||
.map(|a| a.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let authors_str = if paper.authors.len() > 3 {
|
||||
format!("{} et al.", authors)
|
||||
} else {
|
||||
authors
|
||||
};
|
||||
|
||||
let md_path = ctx
|
||||
.app_state
|
||||
.config
|
||||
.storage
|
||||
.library_dir
|
||||
.join("Markdown")
|
||||
.join(format!("{}.md", paper.bibcode));
|
||||
let md_path_str = md_path.to_string_lossy();
|
||||
|
||||
output.push_str(&format!(
|
||||
"| {} | {} | {} | {} | {} | [{}](file://{}) |\n",
|
||||
count,
|
||||
paper.title.chars().take(50).collect::<String>(),
|
||||
authors_str,
|
||||
paper.year,
|
||||
paper.citation_count,
|
||||
paper.bibcode,
|
||||
md_path_str
|
||||
));
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
output.push_str("\n最近无匹配的新文献。\n");
|
||||
} else {
|
||||
output.push_str(&format!("\n共找到 {} 篇匹配文献。\n", count));
|
||||
}
|
||||
|
||||
ToolOutput::success(output, json!({ "count": count }))
|
||||
}
|
||||
Err(e) => ToolOutput::error(format!("本地数据库搜索失败: {}", e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,42 @@
|
||||
// src/agent/tools/astro/research/mod.rs
|
||||
// 研究级工具:科研人员消费本地数据进行分析
|
||||
|
||||
pub mod abundance_analysis;
|
||||
pub mod batch_process;
|
||||
pub mod kinematics_tool;
|
||||
pub mod library;
|
||||
pub mod literature_monitor;
|
||||
pub mod metadata;
|
||||
pub mod note;
|
||||
pub mod observation;
|
||||
pub mod observation_plan;
|
||||
pub mod paper;
|
||||
pub mod period_search;
|
||||
pub mod radial_velocity;
|
||||
pub mod rag;
|
||||
pub mod sed_fit;
|
||||
pub mod spectral_lines;
|
||||
pub mod spectrum_params;
|
||||
pub mod target;
|
||||
pub mod variable_star;
|
||||
pub mod vizier;
|
||||
|
||||
pub use abundance_analysis::AnalyzeAbundancePatternTool;
|
||||
pub use batch_process::BatchProcessTool;
|
||||
pub use kinematics_tool::AnalyzeKinematicsTool;
|
||||
pub use library::{GetCitationNetworkTool, SearchLocalLibraryTool};
|
||||
pub use literature_monitor::LiteratureMonitorTool;
|
||||
pub use metadata::GetPaperMetadataTool;
|
||||
pub use note::SaveNoteTool;
|
||||
pub use observation::FindObservationTool;
|
||||
pub use observation_plan::PlanObservationTool;
|
||||
pub use paper::{GetPaperContentTool, GetPaperOutlineTool};
|
||||
pub use period_search::FindPeriodTool;
|
||||
pub use radial_velocity::MeasureRadialVelocityTool;
|
||||
pub use rag::RagSearchTool;
|
||||
pub use sed_fit::FitSedTool;
|
||||
pub use spectral_lines::MeasureSpectralLinesTool;
|
||||
pub use spectrum_params::GetSpectrumParametersTool;
|
||||
pub use target::QueryTargetTool;
|
||||
pub use variable_star::ClassifyVariableStarTool;
|
||||
pub use vizier::CatalogOperationTool;
|
||||
|
||||
@@ -135,7 +135,7 @@ impl AgentTool for FindObservationTool {
|
||||
let state = &ctx.app_state;
|
||||
|
||||
let source = match args.get("source").and_then(|v| v.as_str()) {
|
||||
Some(s) => match Source::from_str(s) {
|
||||
Some(s) => match Source::parse(s) {
|
||||
Ok(src) => src,
|
||||
Err(e) => return ToolOutput::error(e),
|
||||
},
|
||||
@@ -143,7 +143,7 @@ impl AgentTool for FindObservationTool {
|
||||
};
|
||||
let product_type = match args.get("product").and_then(|v| v.as_str()) {
|
||||
None => ProductType::Spectrum,
|
||||
Some(s) => match ProductType::from_str(s) {
|
||||
Some(s) => match ProductType::parse(s) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return ToolOutput::error(e),
|
||||
},
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// src/agent/tools/astro/research/observation_plan.rs
|
||||
//
|
||||
// PlanObservationTool —— 观测提案辅助
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
use tracing::info;
|
||||
|
||||
use crate::agent::tools::{AgentTool, ToolContext, ToolOutput};
|
||||
|
||||
pub struct PlanObservationTool;
|
||||
|
||||
#[async_trait]
|
||||
impl AgentTool for PlanObservationTool {
|
||||
fn name(&self) -> &str {
|
||||
"plan_observation"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"观测提案辅助"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"观测提案辅助:评估目标的可观测性。计算目标在给定台站的高度角、大气质量、\
|
||||
月相影响,并估算所需曝光时间。支持 Keck、Lick、LAMOST、Gemini North 等台站。"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ra": { "type": "number", "description": "目标赤经 (度, J2000)" },
|
||||
"dec": { "type": "number", "description": "目标赤纬 (度, J2000)" },
|
||||
"observatory": {
|
||||
"type": "string",
|
||||
"enum": ["keck", "lick", "lamost", "gemini_north"],
|
||||
"description": "观测台站,默认 keck",
|
||||
"default": "keck"
|
||||
},
|
||||
"target_v_mag": {
|
||||
"type": "number",
|
||||
"description": "目标 V 波段星等(用于曝光时间估算),可选"
|
||||
},
|
||||
"target_snr": {
|
||||
"type": "number",
|
||||
"description": "目标信噪比(用于曝光时间估算),可选",
|
||||
"default": 100.0
|
||||
}
|
||||
},
|
||||
"required": ["ra", "dec"]
|
||||
})
|
||||
}
|
||||
|
||||
fn group(&self) -> &str {
|
||||
"as:research"
|
||||
}
|
||||
|
||||
fn is_concurrency_safe(&self, _args: &serde_json::Value) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_readonly(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn execute(&self, args: serde_json::Value, _ctx: &ToolContext) -> ToolOutput {
|
||||
let ra = match args.get("ra").and_then(|r| r.as_f64()) {
|
||||
Some(r) => r,
|
||||
None => return ToolOutput::error("缺少必需参数 'ra'"),
|
||||
};
|
||||
let dec = match args.get("dec").and_then(|d| d.as_f64()) {
|
||||
Some(d) => d,
|
||||
None => return ToolOutput::error("缺少必需参数 'dec'"),
|
||||
};
|
||||
|
||||
let obs_name = args
|
||||
.get("observatory")
|
||||
.and_then(|o| o.as_str())
|
||||
.unwrap_or("keck");
|
||||
|
||||
let obs = match obs_name {
|
||||
"keck" => crate::services::analysis::observability::Observatory::keck(),
|
||||
"lick" => crate::services::analysis::observability::Observatory::lick_3m(),
|
||||
"lamost" => crate::services::analysis::observability::Observatory::lamost(),
|
||||
"gemini_north" => crate::services::analysis::observability::Observatory::gemini_north(),
|
||||
_ => return ToolOutput::error(format!("不支持的台站: {}", obs_name)),
|
||||
};
|
||||
|
||||
let target_v_mag = args.get("target_v_mag").and_then(|v| v.as_f64());
|
||||
let target_snr = args
|
||||
.get("target_snr")
|
||||
.and_then(|s| s.as_f64())
|
||||
.unwrap_or(100.0);
|
||||
|
||||
info!(
|
||||
"[PlanObservation] RA={:.4}, Dec={:.4}, 台站={}",
|
||||
ra, dec, obs_name
|
||||
);
|
||||
|
||||
let result = crate::services::analysis::observability::assess_observability(
|
||||
ra,
|
||||
dec,
|
||||
&obs,
|
||||
target_v_mag,
|
||||
Some(target_snr),
|
||||
);
|
||||
|
||||
let report =
|
||||
crate::services::analysis::observability::format_observability(&result, &obs.name);
|
||||
let json_str = serde_json::to_string_pretty(&result).unwrap_or_default();
|
||||
let content = format!("{}\n\n```json\n{}\n```", report, json_str);
|
||||
|
||||
ToolOutput::success(content, json!(result))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// src/agent/tools/astro/research/period_search.rs
|
||||
//
|
||||
// FindPeriodTool —— 光变周期搜索(Lomb-Scargle + BLS)
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
use tracing::info;
|
||||
|
||||
use crate::agent::tools::{AgentTool, ToolContext, ToolOutput};
|
||||
|
||||
pub struct FindPeriodTool;
|
||||
|
||||
#[async_trait]
|
||||
impl AgentTool for FindPeriodTool {
|
||||
fn name(&self) -> &str {
|
||||
"find_period"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"周期搜索"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"搜索光变曲线的周期。支持 Lomb-Scargle(通用变星)和 BLS(凌星检测)两种方法。\
|
||||
自动报告最优周期、FAP(误报概率)和次优周期。"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "array",
|
||||
"items": { "type": "number" },
|
||||
"description": "时间数组 (天)"
|
||||
},
|
||||
"flux": {
|
||||
"type": "array",
|
||||
"items": { "type": "number" },
|
||||
"description": "通量/星等数组"
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": ["lomb-scargle", "bls"],
|
||||
"description": "搜索方法,默认 lomb-scargle",
|
||||
"default": "lomb-scargle"
|
||||
},
|
||||
"min_period": {
|
||||
"type": "number",
|
||||
"description": "最小周期 (天),可选"
|
||||
},
|
||||
"max_period": {
|
||||
"type": "number",
|
||||
"description": "最大周期 (天),可选"
|
||||
},
|
||||
"n_freq": {
|
||||
"type": "integer",
|
||||
"description": "频率点数,默认 5000",
|
||||
"default": 5000
|
||||
}
|
||||
},
|
||||
"required": ["time", "flux"]
|
||||
})
|
||||
}
|
||||
|
||||
fn group(&self) -> &str {
|
||||
"as:research"
|
||||
}
|
||||
|
||||
fn is_concurrency_safe(&self, _args: &serde_json::Value) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_readonly(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn execute(&self, args: serde_json::Value, _ctx: &ToolContext) -> ToolOutput {
|
||||
let time: Vec<f64> = match args.get("time").and_then(|t| t.as_array()) {
|
||||
Some(arr) => arr.iter().filter_map(|v| v.as_f64()).collect(),
|
||||
None => return ToolOutput::error("缺少必需参数 'time'"),
|
||||
};
|
||||
let flux: Vec<f64> = match args.get("flux").and_then(|f| f.as_array()) {
|
||||
Some(arr) => arr.iter().filter_map(|v| v.as_f64()).collect(),
|
||||
None => return ToolOutput::error("缺少必需参数 'flux'"),
|
||||
};
|
||||
|
||||
if time.len() != flux.len() {
|
||||
return ToolOutput::error("时间和通量数组长度不一致");
|
||||
}
|
||||
if time.len() < 4 {
|
||||
return ToolOutput::error("数据点太少(需要 ≥4)");
|
||||
}
|
||||
|
||||
let method = args
|
||||
.get("method")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("lomb-scargle");
|
||||
|
||||
let min_period = args.get("min_period").and_then(|p| p.as_f64());
|
||||
let max_period = args.get("max_period").and_then(|p| p.as_f64());
|
||||
let n_freq = args.get("n_freq").and_then(|n| n.as_u64()).unwrap_or(5000) as usize;
|
||||
|
||||
info!(
|
||||
"[FindPeriod] 方法={}, 数据点={}, 搜索范围=[{}, {}] 天",
|
||||
method,
|
||||
time.len(),
|
||||
min_period
|
||||
.map(|p| format!("{:.4}", p))
|
||||
.unwrap_or_else(|| "auto".to_string()),
|
||||
max_period
|
||||
.map(|p| format!("{:.4}", p))
|
||||
.unwrap_or_else(|| "auto".to_string())
|
||||
);
|
||||
|
||||
match method {
|
||||
"lomb-scargle" => {
|
||||
match crate::services::timeseries::periodicity::lomb_scargle(
|
||||
&time, &flux, min_period, max_period, n_freq,
|
||||
) {
|
||||
Ok(result) => {
|
||||
let report =
|
||||
crate::services::timeseries::periodicity::format_period_result(&result);
|
||||
let json_str = serde_json::to_string_pretty(&result).unwrap_or_default();
|
||||
let content = format!("{}\n\n```json\n{}\n```", report, json_str);
|
||||
ToolOutput::success(content, json!(result))
|
||||
}
|
||||
Err(e) => ToolOutput::error(format!("周期搜索失败: {}", e)),
|
||||
}
|
||||
}
|
||||
"bls" => {
|
||||
match crate::services::timeseries::periodicity::bls(
|
||||
&time, &flux, min_period, max_period, n_freq, 0.01, 0.1,
|
||||
) {
|
||||
Ok(result) => {
|
||||
let content = format!(
|
||||
"BLS 周期搜索结果:\n\n- 最优周期: {:.6} 天\n- 凌星深度: {:.4}\n- 凌星持续时间: {:.4} 天\n- SDE: {:.2}\n",
|
||||
result.best_period, result.depth, result.duration, result.sde
|
||||
);
|
||||
ToolOutput::success(content, json!(result))
|
||||
}
|
||||
Err(e) => ToolOutput::error(format!("BLS 搜索失败: {}", e)),
|
||||
}
|
||||
}
|
||||
_ => ToolOutput::error(format!("不支持的方法: {}", method)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// src/agent/tools/astro/research/radial_velocity.rs
|
||||
//
|
||||
// MeasureRadialVelocityTool —— 交叉相关法测径向速度
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
use tracing::info;
|
||||
|
||||
use crate::agent::tools::{AgentTool, ToolContext, ToolOutput};
|
||||
|
||||
pub struct MeasureRadialVelocityTool;
|
||||
|
||||
#[async_trait]
|
||||
impl AgentTool for MeasureRadialVelocityTool {
|
||||
fn name(&self) -> &str {
|
||||
"measure_radial_velocity"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"径向速度测量"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"通过交叉相关法测量光谱的径向速度(RV)。使用内置恒星类型模板或用户自定义模板,\
|
||||
通过 Doppler shift 匹配测量视向速度。支持 OBAFGKM 恒星类型。"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"wavelength": {
|
||||
"type": "array",
|
||||
"items": { "type": "number" },
|
||||
"description": "观测光谱波长数组 (Å)"
|
||||
},
|
||||
"flux": {
|
||||
"type": "array",
|
||||
"items": { "type": "number" },
|
||||
"description": "观测光谱通量数组"
|
||||
},
|
||||
"template_type": {
|
||||
"type": "string",
|
||||
"enum": ["O", "B", "A", "F", "G", "K", "M", "Mwarf", "Giant"],
|
||||
"description": "模板恒星类型(不指定则使用 G 型)",
|
||||
"default": "G"
|
||||
},
|
||||
"rv_range": {
|
||||
"type": "array",
|
||||
"items": { "type": "number" },
|
||||
"description": "RV 搜索范围 [min, max] (km/s),默认 [-500, 500]",
|
||||
"default": [-500.0, 500.0]
|
||||
},
|
||||
"rv_step": {
|
||||
"type": "number",
|
||||
"description": "RV 搜索步长 (km/s),默认 1.0",
|
||||
"default": 1.0
|
||||
}
|
||||
},
|
||||
"required": ["wavelength", "flux"]
|
||||
})
|
||||
}
|
||||
|
||||
fn group(&self) -> &str {
|
||||
"as:research"
|
||||
}
|
||||
|
||||
fn is_concurrency_safe(&self, _args: &serde_json::Value) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_readonly(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn execute(&self, args: serde_json::Value, _ctx: &ToolContext) -> ToolOutput {
|
||||
let wavelength: Vec<f64> = match args.get("wavelength").and_then(|w| w.as_array()) {
|
||||
Some(arr) => arr.iter().filter_map(|v| v.as_f64()).collect(),
|
||||
None => return ToolOutput::error("缺少必需参数 'wavelength'"),
|
||||
};
|
||||
let flux: Vec<f64> = match args.get("flux").and_then(|w| w.as_array()) {
|
||||
Some(arr) => arr.iter().filter_map(|v| v.as_f64()).collect(),
|
||||
None => return ToolOutput::error("缺少必需参数 'flux'"),
|
||||
};
|
||||
|
||||
if wavelength.len() != flux.len() {
|
||||
return ToolOutput::error("波长和通量数组长度不一致");
|
||||
}
|
||||
if wavelength.len() < 10 {
|
||||
return ToolOutput::error("数据点太少(需要 ≥10)");
|
||||
}
|
||||
|
||||
let template_str = args
|
||||
.get("template_type")
|
||||
.and_then(|t| t.as_str())
|
||||
.unwrap_or("G");
|
||||
|
||||
let template = match template_str {
|
||||
"O" => crate::services::spectrum::cross_correlate::TemplateType::O,
|
||||
"B" => crate::services::spectrum::cross_correlate::TemplateType::B,
|
||||
"A" => crate::services::spectrum::cross_correlate::TemplateType::A,
|
||||
"F" => crate::services::spectrum::cross_correlate::TemplateType::F,
|
||||
"G" => crate::services::spectrum::cross_correlate::TemplateType::G,
|
||||
"K" => crate::services::spectrum::cross_correlate::TemplateType::K,
|
||||
"M" => crate::services::spectrum::cross_correlate::TemplateType::M,
|
||||
"Mwarf" => crate::services::spectrum::cross_correlate::TemplateType::Mwarf,
|
||||
"Giant" => crate::services::spectrum::cross_correlate::TemplateType::Giant,
|
||||
_ => return ToolOutput::error(format!("不支持的模板类型: {}", template_str)),
|
||||
};
|
||||
|
||||
let rv_range = args
|
||||
.get("rv_range")
|
||||
.and_then(|r| r.as_array())
|
||||
.and_then(|a| {
|
||||
if a.len() == 2 {
|
||||
Some((a[0].as_f64()?, a[1].as_f64()?))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or((-500.0, 500.0));
|
||||
|
||||
let rv_step = args.get("rv_step").and_then(|s| s.as_f64()).unwrap_or(1.0);
|
||||
|
||||
info!(
|
||||
"[MeasureRV] 模板={:?}, RV范围=[{:.0}, {:.0}] km/s, 步长={:.1} km/s",
|
||||
template, rv_range.0, rv_range.1, rv_step
|
||||
);
|
||||
|
||||
match crate::services::spectrum::cross_correlate::measure_rv(
|
||||
&wavelength,
|
||||
&flux,
|
||||
template,
|
||||
rv_range,
|
||||
rv_step,
|
||||
) {
|
||||
Ok(result) => {
|
||||
let json_str = serde_json::to_string_pretty(&result).unwrap_or_default();
|
||||
let content = format!(
|
||||
"径向速度测量结果:\n\n- RV = {:.2} ± {:.2} km/s\n- CCF 峰值 = {:.4}\n- 模板: {}\n- 波长范围: {:.0}-{:.0} Å\n- SNR: {}\n\n{}",
|
||||
result.rv, result.rv_error, result.ccf_peak, result.template_name,
|
||||
result.wavelength_range.0, result.wavelength_range.1,
|
||||
result.snr.map(|s| format!("{:.1}", s)).unwrap_or_else(|| "N/A".to_string()),
|
||||
json_str
|
||||
);
|
||||
ToolOutput::success(content, json!(result))
|
||||
}
|
||||
Err(e) => ToolOutput::error(format!("径向速度测量失败: {}", e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,11 @@ impl AgentTool for RagSearchTool {
|
||||
"rag_search"
|
||||
}
|
||||
|
||||
/// 检索结果来自外部论文语料,可能包含对抗性内容
|
||||
fn untrusted_output(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"文献库RAG检索"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// src/agent/tools/astro/research/sed_fit.rs
|
||||
//
|
||||
// FitSedTool —— 多波段 SED 拟合
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
use tracing::info;
|
||||
|
||||
use crate::agent::tools::{AgentTool, ToolContext, ToolOutput};
|
||||
|
||||
pub struct FitSedTool;
|
||||
|
||||
#[async_trait]
|
||||
impl AgentTool for FitSedTool {
|
||||
fn name(&self) -> &str {
|
||||
"fit_sed"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"SED 拟合"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"多波段光谱能量分布(SED)拟合。输入多个波段的测光数据(波长、通量、误差),\
|
||||
拟合黑体辐射模型,输出有效温度 T_eff、半径 R、消光 A_V、光度 L 及不确定度。"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data_points": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"wavelength": { "type": "number", "description": "中心波长 (Å)" },
|
||||
"flux": { "type": "number", "description": "流量" },
|
||||
"flux_error": { "type": "number", "description": "流量误差" },
|
||||
"band": { "type": "string", "description": "波段名" }
|
||||
},
|
||||
"required": ["wavelength", "flux", "flux_error", "band"]
|
||||
},
|
||||
"description": "测光数据点列表"
|
||||
},
|
||||
"distance_pc": {
|
||||
"type": "number",
|
||||
"description": "距离 (pc),可选,用于计算光度"
|
||||
}
|
||||
},
|
||||
"required": ["data_points"]
|
||||
})
|
||||
}
|
||||
|
||||
fn group(&self) -> &str {
|
||||
"as:research"
|
||||
}
|
||||
|
||||
fn is_concurrency_safe(&self, _args: &serde_json::Value) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_readonly(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn execute(&self, args: serde_json::Value, _ctx: &ToolContext) -> ToolOutput {
|
||||
let data_points: Vec<crate::services::analysis::sed::SedDataPoint> =
|
||||
match args.get("data_points").and_then(|d| d.as_array()) {
|
||||
Some(arr) => {
|
||||
let mut points = Vec::new();
|
||||
for item in arr {
|
||||
let wavelength = match item.get("wavelength").and_then(|w| w.as_f64()) {
|
||||
Some(w) => w,
|
||||
None => continue,
|
||||
};
|
||||
let flux = match item.get("flux").and_then(|f| f.as_f64()) {
|
||||
Some(f) => f,
|
||||
None => continue,
|
||||
};
|
||||
let flux_error = item
|
||||
.get("flux_error")
|
||||
.and_then(|e| e.as_f64())
|
||||
.unwrap_or(flux * 0.1);
|
||||
let band = item
|
||||
.get("band")
|
||||
.and_then(|b| b.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
points.push(crate::services::analysis::sed::SedDataPoint {
|
||||
wavelength,
|
||||
flux,
|
||||
flux_error,
|
||||
band,
|
||||
source: "user".to_string(),
|
||||
});
|
||||
}
|
||||
points
|
||||
}
|
||||
None => return ToolOutput::error("缺少必需参数 'data_points'"),
|
||||
};
|
||||
|
||||
if data_points.len() < 3 {
|
||||
return ToolOutput::error("至少需要 3 个测光点进行 SED 拟合");
|
||||
}
|
||||
|
||||
let distance_pc = args.get("distance_pc").and_then(|d| d.as_f64());
|
||||
|
||||
info!(
|
||||
"[FitSed] 数据点={}, 距离={:?} pc",
|
||||
data_points.len(),
|
||||
distance_pc
|
||||
);
|
||||
|
||||
match crate::services::analysis::sed::fit_sed(&data_points, distance_pc) {
|
||||
Ok(result) => {
|
||||
let report = crate::services::analysis::sed::format_sed_result(&result);
|
||||
let json_str = serde_json::to_string_pretty(&result).unwrap_or_default();
|
||||
let content = format!("{}\n\n```json\n{}\n```", report, json_str);
|
||||
ToolOutput::success(content, json!(result))
|
||||
}
|
||||
Err(e) => ToolOutput::error(format!("SED 拟合失败: {}", e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// src/agent/tools/astro/research/spectral_lines.rs
|
||||
//
|
||||
// MeasureSpectralLinesTool —— 谱线识别与等值宽度测量
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
use tracing::info;
|
||||
|
||||
use crate::agent::tools::{AgentTool, ToolContext, ToolOutput};
|
||||
|
||||
pub struct MeasureSpectralLinesTool;
|
||||
|
||||
#[async_trait]
|
||||
impl AgentTool for MeasureSpectralLinesTool {
|
||||
fn name(&self) -> &str {
|
||||
"measure_spectral_lines"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"谱线测量"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"识别光谱中的常见谱线(Balmer 系列、Ca II H&K、Na D、Mg b、Fe 等),\
|
||||
自动计算等值宽度(EW)和半高全宽(FWHM)。输入光谱的波长和通量数组,\
|
||||
返回每条谱线的测量结果。"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"wavelength": {
|
||||
"type": "array",
|
||||
"items": { "type": "number" },
|
||||
"description": "波长数组 (Å)"
|
||||
},
|
||||
"flux": {
|
||||
"type": "array",
|
||||
"items": { "type": "number" },
|
||||
"description": "通量数组"
|
||||
},
|
||||
"lines": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "要测量的谱线名称列表(可选)。不指定则测量所有内置谱线。可选值: Hα, Hβ, Hγ, Hδ, Ca II K, Ca II H, Na I D2, Na I D1, Mg I b1 等"
|
||||
},
|
||||
"window_half_width": {
|
||||
"type": "number",
|
||||
"description": "搜索窗口半宽 (Å),默认 20",
|
||||
"default": 20.0
|
||||
}
|
||||
},
|
||||
"required": ["wavelength", "flux"]
|
||||
})
|
||||
}
|
||||
|
||||
fn group(&self) -> &str {
|
||||
"as:research"
|
||||
}
|
||||
|
||||
fn is_concurrency_safe(&self, _args: &serde_json::Value) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_readonly(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn execute(&self, args: serde_json::Value, _ctx: &ToolContext) -> ToolOutput {
|
||||
let wavelength: Vec<f32> = match args.get("wavelength").and_then(|w| w.as_array()) {
|
||||
Some(arr) => arr
|
||||
.iter()
|
||||
.filter_map(|v| v.as_f64().map(|f| f as f32))
|
||||
.collect(),
|
||||
None => return ToolOutput::error("缺少必需参数 'wavelength'"),
|
||||
};
|
||||
let flux: Vec<f32> = match args.get("flux").and_then(|w| w.as_array()) {
|
||||
Some(arr) => arr
|
||||
.iter()
|
||||
.filter_map(|v| v.as_f64().map(|f| f as f32))
|
||||
.collect(),
|
||||
None => return ToolOutput::error("缺少必需参数 'flux'"),
|
||||
};
|
||||
|
||||
let window_half_width = args
|
||||
.get("window_half_width")
|
||||
.and_then(|w| w.as_f64())
|
||||
.unwrap_or(20.0);
|
||||
|
||||
if wavelength.len() != flux.len() {
|
||||
return ToolOutput::error("波长和通量数组长度不一致");
|
||||
}
|
||||
if wavelength.len() < 3 {
|
||||
return ToolOutput::error("数据点太少(需要 ≥3)");
|
||||
}
|
||||
|
||||
let table = crate::services::spectrum::lines::builtin_line_table();
|
||||
|
||||
// 筛选要测量的谱线
|
||||
let target_lines: Vec<crate::services::spectrum::lines::SpectralLine> =
|
||||
if let Some(names) = args.get("lines").and_then(|l| l.as_array()) {
|
||||
names
|
||||
.iter()
|
||||
.filter_map(|n| n.as_str())
|
||||
.filter_map(|name| crate::services::spectrum::lines::find_line(name, &table))
|
||||
.cloned()
|
||||
.collect()
|
||||
} else {
|
||||
// 测量所有在光谱范围内的谱线
|
||||
let w_min = wavelength.iter().cloned().fold(f32::INFINITY, f32::min) as f64;
|
||||
let w_max = wavelength.iter().cloned().fold(f32::NEG_INFINITY, f32::max) as f64;
|
||||
crate::services::spectrum::lines::find_lines_in_range(w_min, w_max, &table)
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect()
|
||||
};
|
||||
|
||||
if target_lines.is_empty() {
|
||||
return ToolOutput::error("在当前光谱范围内未找到匹配的谱线");
|
||||
}
|
||||
|
||||
info!(
|
||||
"[MeasureSpectralLines] 测量 {} 条谱线,窗口 ±{:.1} Å",
|
||||
target_lines.len(),
|
||||
window_half_width
|
||||
);
|
||||
|
||||
let measurements = crate::services::spectrum::lines::measure_lines(
|
||||
&wavelength,
|
||||
&flux,
|
||||
&target_lines,
|
||||
window_half_width,
|
||||
);
|
||||
|
||||
let successful: Vec<_> = measurements.into_iter().filter_map(|r| r.ok()).collect();
|
||||
|
||||
if successful.is_empty() {
|
||||
return ToolOutput::error("所有谱线测量均失败");
|
||||
}
|
||||
|
||||
let table_str = crate::services::spectrum::lines::format_measurements_table(&successful);
|
||||
|
||||
let content = format!(
|
||||
"谱线测量完成({} / {} 条成功):\n\n{}",
|
||||
successful.len(),
|
||||
target_lines.len(),
|
||||
table_str
|
||||
);
|
||||
|
||||
let metadata = json!({
|
||||
"measurements": successful.iter().map(|m| {
|
||||
json!({
|
||||
"line": m.line.name,
|
||||
"wavelength": m.center_wavelength,
|
||||
"ew": m.ew,
|
||||
"ew_error": m.ew_error,
|
||||
"fwhm": m.fwhm,
|
||||
"continuum_flux": m.continuum_flux
|
||||
})
|
||||
}).collect::<Vec<_>>(),
|
||||
"total_lines": target_lines.len(),
|
||||
"successful_measurements": successful.len()
|
||||
});
|
||||
|
||||
ToolOutput::success(content, metadata)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// src/agent/tools/astro/research/spectrum_params.rs
|
||||
//
|
||||
// GetSpectrumParametersTool —— 从 FITS 文件中提取恒星大气参数
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
use tracing::info;
|
||||
|
||||
use crate::agent::tools::{AgentTool, ToolContext, ToolOutput};
|
||||
|
||||
pub struct GetSpectrumParametersTool;
|
||||
|
||||
#[async_trait]
|
||||
impl AgentTool for GetSpectrumParametersTool {
|
||||
fn name(&self) -> &str {
|
||||
"get_spectrum_parameters"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"光谱参数提取"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"从已下载的 FITS 光谱文件中自动提取恒星大气参数(Teff, logg, [Fe/H], 径向速度等)。\
|
||||
支持 LAMOST、SDSS/BOSS、APOGEE、DESI 数据。结果结构化返回,可直接用于后续分析。"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {
|
||||
"type": "string",
|
||||
"enum": ["lamost", "sdss", "desi", "gaia"],
|
||||
"description": "数据源"
|
||||
},
|
||||
"subtype": {
|
||||
"type": "string",
|
||||
"description": "产品子类型(SDSS: spec/apstar/aspcap)"
|
||||
},
|
||||
"source_id": {
|
||||
"type": "string",
|
||||
"description": "源标识符"
|
||||
},
|
||||
"file_path": {
|
||||
"type": "string",
|
||||
"description": "FITS 文件路径(相对于 library/ 目录)"
|
||||
}
|
||||
},
|
||||
"required": ["source", "source_id", "file_path"]
|
||||
})
|
||||
}
|
||||
|
||||
fn group(&self) -> &str {
|
||||
"as:research"
|
||||
}
|
||||
|
||||
fn is_concurrency_safe(&self, _args: &serde_json::Value) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_readonly(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn execute(&self, args: serde_json::Value, ctx: &ToolContext) -> ToolOutput {
|
||||
let source_str = match args.get("source").and_then(|s| s.as_str()) {
|
||||
Some(s) => s,
|
||||
None => return ToolOutput::error("缺少必需参数 'source'"),
|
||||
};
|
||||
let source_id = match args.get("source_id").and_then(|s| s.as_str()) {
|
||||
Some(s) => s,
|
||||
None => return ToolOutput::error("缺少必需参数 'source_id'"),
|
||||
};
|
||||
let file_path = match args.get("file_path").and_then(|s| s.as_str()) {
|
||||
Some(s) => s,
|
||||
None => return ToolOutput::error("缺少必需参数 'file_path'"),
|
||||
};
|
||||
let subtype = args.get("subtype").and_then(|s| s.as_str());
|
||||
|
||||
let source = match crate::services::observation::Source::parse(source_str) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return ToolOutput::error(e),
|
||||
};
|
||||
|
||||
let product = crate::services::observation::types::ProductSpec {
|
||||
product: crate::services::observation::types::ProductType::Spectrum,
|
||||
subtype: subtype.map(|s| s.to_string()),
|
||||
};
|
||||
|
||||
info!(
|
||||
"[GetSpectrumParameters] source={}, id={}, file={}",
|
||||
source_str, source_id, file_path
|
||||
);
|
||||
|
||||
// 构造完整路径
|
||||
let full_path = if std::path::Path::new(file_path).is_absolute() {
|
||||
file_path.to_string()
|
||||
} else {
|
||||
let library_dir = ctx
|
||||
.app_state
|
||||
.config
|
||||
.storage
|
||||
.library_dir
|
||||
.to_str()
|
||||
.unwrap_or("library");
|
||||
format!("{}/{}", library_dir, file_path)
|
||||
};
|
||||
|
||||
match crate::services::spectrum::parameters::extract_parameters(
|
||||
&ctx.app_state,
|
||||
source,
|
||||
&product,
|
||||
source_id,
|
||||
&full_path,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(params) => {
|
||||
let json_str = serde_json::to_string_pretty(¶ms).unwrap_or_default();
|
||||
let content = format!(
|
||||
"光谱参数提取成功 ({}, {}):\n\n{}",
|
||||
source_str, source_id, json_str
|
||||
);
|
||||
ToolOutput::success(content, json!(params))
|
||||
}
|
||||
Err(e) => ToolOutput::error(format!("参数提取失败: {}", e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// src/agent/tools/astro/research/variable_star.rs
|
||||
//
|
||||
// ClassifyVariableStarTool —— 变星分类与特征提取
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
use tracing::info;
|
||||
|
||||
use crate::agent::tools::{AgentTool, ToolContext, ToolOutput};
|
||||
|
||||
pub struct ClassifyVariableStarTool;
|
||||
|
||||
#[async_trait]
|
||||
impl AgentTool for ClassifyVariableStarTool {
|
||||
fn name(&self) -> &str {
|
||||
"classify_variable_star"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"变星分类"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"自动分类变星类型(RR Lyrae、Cepheid、食双星、脉动变星、AGN 等),\
|
||||
提取振幅、偏度、峰度等统计特征。可选传入已知周期以提高分类准确度。"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "array",
|
||||
"items": { "type": "number" },
|
||||
"description": "时间数组 (天)"
|
||||
},
|
||||
"flux": {
|
||||
"type": "array",
|
||||
"items": { "type": "number" },
|
||||
"description": "通量/星等数组"
|
||||
},
|
||||
"period": {
|
||||
"type": "number",
|
||||
"description": "已知周期 (天),可选。传入可提高分类准确度"
|
||||
}
|
||||
},
|
||||
"required": ["time", "flux"]
|
||||
})
|
||||
}
|
||||
|
||||
fn group(&self) -> &str {
|
||||
"as:research"
|
||||
}
|
||||
|
||||
fn is_concurrency_safe(&self, _args: &serde_json::Value) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_readonly(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn execute(&self, args: serde_json::Value, _ctx: &ToolContext) -> ToolOutput {
|
||||
let time: Vec<f64> = match args.get("time").and_then(|t| t.as_array()) {
|
||||
Some(arr) => arr.iter().filter_map(|v| v.as_f64()).collect(),
|
||||
None => return ToolOutput::error("缺少必需参数 'time'"),
|
||||
};
|
||||
let flux: Vec<f64> = match args.get("flux").and_then(|f| f.as_array()) {
|
||||
Some(arr) => arr.iter().filter_map(|v| v.as_f64()).collect(),
|
||||
None => return ToolOutput::error("缺少必需参数 'flux'"),
|
||||
};
|
||||
|
||||
let period = args.get("period").and_then(|p| p.as_f64());
|
||||
|
||||
if time.len() != flux.len() {
|
||||
return ToolOutput::error("时间和通量数组长度不一致");
|
||||
}
|
||||
if time.len() < 5 {
|
||||
return ToolOutput::error("数据点太少(需要 ≥5)");
|
||||
}
|
||||
|
||||
info!(
|
||||
"[ClassifyVariable] 数据点={}, 周期={}",
|
||||
time.len(),
|
||||
period
|
||||
.map(|p| format!("{:.4} 天", p))
|
||||
.unwrap_or_else(|| "未指定".to_string())
|
||||
);
|
||||
|
||||
match crate::services::timeseries::classification::classify_variable_star(
|
||||
&time, &flux, period,
|
||||
) {
|
||||
Ok(result) => {
|
||||
let report =
|
||||
crate::services::timeseries::classification::format_classification(&result);
|
||||
let json_str = serde_json::to_string_pretty(&result).unwrap_or_default();
|
||||
let content = format!("{}\n\n```json\n{}\n```", report, json_str);
|
||||
ToolOutput::success(content, json!(result))
|
||||
}
|
||||
Err(e) => ToolOutput::error(format!("分类失败: {}", e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user