feat(ai): Agent 分析 Tool — AnalyzeLabReport + AnalyzeHealthTrends
- AnalyzeLabReportTool: 获取化验报告详细指标(异常标记+参考范围) - AnalyzeHealthTrendsTool: 趋势分析(回归方向/日变化/异常检测) - 沙箱: MedicalStaff 专属分析 Tool,Patient 不可用
This commit is contained in:
@@ -63,6 +63,8 @@ pub fn get_sandbox_config(role: &UserRole) -> SandboxConfig {
|
||||
"query_patient_medications".into(),
|
||||
"search_medical_knowledge".into(),
|
||||
"query_patient_profile".into(),
|
||||
"analyze_lab_report".into(),
|
||||
"analyze_health_trends".into(),
|
||||
]),
|
||||
system_prompt_suffix: MEDICAL_STAFF_PROMPT_SUFFIX,
|
||||
output_filter: OutputFilter {
|
||||
|
||||
181
crates/erp-ai/src/agent/tools/analyze_health_trends.rs
Normal file
181
crates/erp-ai/src/agent/tools/analyze_health_trends.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use async_trait::async_trait;
|
||||
use erp_core::health_provider::TimeRange;
|
||||
|
||||
use crate::agent::tool::{AgentTool, DisplayHint, ToolContext, ToolResult};
|
||||
|
||||
/// 获取健康趋势分析数据(回归方向/日变化/异常点),Agent 自行解读
|
||||
pub struct AnalyzeHealthTrendsTool;
|
||||
|
||||
#[async_trait]
|
||||
impl AgentTool for AnalyzeHealthTrendsTool {
|
||||
fn name(&self) -> &str {
|
||||
"analyze_health_trends"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"获取患者健康指标的趋势分析数据,包括线性回归方向、每日变化量和异常检测。可指定天数范围和关注的指标类型。"
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"days": {
|
||||
"type": "integer",
|
||||
"description": "分析最近多少天的数据,默认 30 天"
|
||||
},
|
||||
"metrics": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "可选:关注的指标列表,如 ['systolic_bp_morning', 'blood_sugar']"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &ToolContext, params: serde_json::Value) -> ToolResult {
|
||||
let patient_id = match ctx.patient_id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return ToolResult {
|
||||
output: "未关联患者档案,无法分析健康趋势".to_string(),
|
||||
display_hint: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let days = params["days"].as_i64().unwrap_or(30);
|
||||
let metrics: Vec<String> = params["metrics"]
|
||||
.as_array()
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
vec![
|
||||
"systolic_bp_morning".into(),
|
||||
"diastolic_bp_morning".into(),
|
||||
"heart_rate".into(),
|
||||
"blood_sugar".into(),
|
||||
]
|
||||
});
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let start = now - chrono::Duration::days(days);
|
||||
let range = TimeRange { start, end: now };
|
||||
|
||||
match ctx
|
||||
.health_provider
|
||||
.get_trend_analysis_data(ctx.tenant_id, patient_id, &metrics, &range)
|
||||
.await
|
||||
{
|
||||
Ok(trend) => {
|
||||
let mut output = format!(
|
||||
"健康趋势分析({} 至 {},共 {} 个指标):\n\n",
|
||||
trend.period_start,
|
||||
trend.period_end,
|
||||
trend.metrics.len()
|
||||
);
|
||||
|
||||
let mut metric_names = Vec::new();
|
||||
let mut has_anomaly = false;
|
||||
|
||||
for m in &trend.metrics {
|
||||
metric_names.push(m.metric.clone());
|
||||
|
||||
output.push_str(&format!(
|
||||
"【{}】({},{} 个数据点)\n",
|
||||
m.metric, m.unit, m.data_point_count
|
||||
));
|
||||
|
||||
if let Some(ref reg) = m.regression {
|
||||
let direction = match reg.direction {
|
||||
erp_core::health_provider::TrendDirection::Rising => "上升",
|
||||
erp_core::health_provider::TrendDirection::Falling => "下降",
|
||||
erp_core::health_provider::TrendDirection::Stable => "稳定",
|
||||
};
|
||||
output.push_str(&format!(
|
||||
" 趋势: {}(日变化: {:.4},期间变化: {:.2},R²: {:.3})\n",
|
||||
direction, reg.daily_change, reg.period_change, reg.r_squared
|
||||
));
|
||||
} else {
|
||||
output.push_str(" 趋势: 数据不足,无法计算\n");
|
||||
}
|
||||
|
||||
if !m.anomalies.is_empty() {
|
||||
has_anomaly = true;
|
||||
output.push_str(&format!(" ⚠ 异常值({} 个):\n", m.anomalies.len()));
|
||||
for a in m.anomalies.iter().take(5) {
|
||||
output.push_str(&format!(
|
||||
" - {}:值 {:.1}(均值 {:.1}±{:.1},偏差 {:.1}σ)\n",
|
||||
a.date, a.value, a.mean, a.std_dev, a.deviation
|
||||
));
|
||||
}
|
||||
}
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
let summary = if has_anomaly {
|
||||
format!(
|
||||
"发现 {} 个指标存在异常值",
|
||||
trend
|
||||
.metrics
|
||||
.iter()
|
||||
.filter(|m| !m.anomalies.is_empty())
|
||||
.count()
|
||||
)
|
||||
} else {
|
||||
"所有指标趋势正常".to_string()
|
||||
};
|
||||
|
||||
let display_hint = DisplayHint::TrendChart {
|
||||
metrics: metric_names,
|
||||
period: format!("{}天", days),
|
||||
summary: summary.clone(),
|
||||
};
|
||||
|
||||
ToolResult {
|
||||
output,
|
||||
display_hint: Some(display_hint),
|
||||
}
|
||||
}
|
||||
Err(e) => ToolResult {
|
||||
output: format!("获取趋势分析数据失败: {}", e),
|
||||
display_hint: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tool_name() {
|
||||
let tool = AnalyzeHealthTrendsTool;
|
||||
assert_eq!(tool.name(), "analyze_health_trends");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_metrics() {
|
||||
let params = serde_json::json!({});
|
||||
let metrics: Vec<String> = params["metrics"]
|
||||
.as_array()
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
vec![
|
||||
"systolic_bp_morning".into(),
|
||||
"diastolic_bp_morning".into(),
|
||||
"heart_rate".into(),
|
||||
"blood_sugar".into(),
|
||||
]
|
||||
});
|
||||
assert_eq!(metrics.len(), 4);
|
||||
}
|
||||
}
|
||||
122
crates/erp-ai/src/agent/tools/analyze_lab_report.rs
Normal file
122
crates/erp-ai/src/agent/tools/analyze_lab_report.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::agent::tool::{AgentTool, DisplayHint, ToolContext, ToolResult};
|
||||
|
||||
/// 获取化验报告详细指标数据(名称/值/参考范围/异常标记),Agent 自行解读
|
||||
pub struct AnalyzeLabReportTool;
|
||||
|
||||
#[async_trait]
|
||||
impl AgentTool for AnalyzeLabReportTool {
|
||||
fn name(&self) -> &str {
|
||||
"analyze_lab_report"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"获取指定化验报告的详细指标数据,包括每个指标的名称、检测值、单位、参考范围和是否异常。需要提供报告 ID。"
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"required": ["report_id"],
|
||||
"properties": {
|
||||
"report_id": {
|
||||
"type": "string",
|
||||
"description": "化验报告 ID"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &ToolContext, params: serde_json::Value) -> ToolResult {
|
||||
let report_id = match params["report_id"]
|
||||
.as_str()
|
||||
.and_then(|s| uuid::Uuid::parse_str(s).ok())
|
||||
{
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return ToolResult {
|
||||
output: "请提供有效的化验报告 ID".to_string(),
|
||||
display_hint: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
match ctx
|
||||
.health_provider
|
||||
.get_lab_report(ctx.tenant_id, report_id)
|
||||
.await
|
||||
{
|
||||
Ok(report) => {
|
||||
let mut output = format!(
|
||||
"化验报告详情({},{},{})\n\n",
|
||||
report.department, report.report_date, report.sex
|
||||
);
|
||||
|
||||
let abnormal_items: Vec<&erp_core::health_provider::LabItemDto> =
|
||||
report.items.iter().filter(|i| i.is_abnormal).collect();
|
||||
|
||||
if !abnormal_items.is_empty() {
|
||||
output.push_str(&format!("⚠ 异常指标({} 项):\n", abnormal_items.len()));
|
||||
for item in &abnormal_items {
|
||||
output.push_str(&format!(
|
||||
" - {}:{} {}(参考范围: {})\n",
|
||||
item.name, item.value, item.unit, item.reference_range
|
||||
));
|
||||
}
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
output.push_str("所有检测指标:\n");
|
||||
for item in &report.items {
|
||||
let flag = if item.is_abnormal { " ⚠" } else { "" };
|
||||
output.push_str(&format!(
|
||||
"- {}:{} {}(参考: {}){}\n",
|
||||
item.name, item.value, item.unit, item.reference_range, flag
|
||||
));
|
||||
}
|
||||
|
||||
let display_hint = if !abnormal_items.is_empty() {
|
||||
Some(DisplayHint::LabReportCard {
|
||||
report_date: report.report_date.clone(),
|
||||
abnormal_count: abnormal_items.len(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
ToolResult {
|
||||
output,
|
||||
display_hint,
|
||||
}
|
||||
}
|
||||
Err(e) => ToolResult {
|
||||
output: format!("获取化验报告失败: {}", e),
|
||||
display_hint: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tool_name() {
|
||||
let tool = AnalyzeLabReportTool;
|
||||
assert_eq!(tool.name(), "analyze_lab_report");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parameters_require_report_id() {
|
||||
let tool = AnalyzeLabReportTool;
|
||||
let schema = tool.parameters_schema();
|
||||
assert!(
|
||||
schema["required"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.contains(&serde_json::json!("report_id"))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
// Agent Tool 实现
|
||||
|
||||
pub mod analyze_health_trends;
|
||||
pub mod analyze_lab_report;
|
||||
pub mod query_appointments;
|
||||
pub mod query_lab_reports;
|
||||
pub mod query_medications;
|
||||
@@ -7,6 +9,8 @@ pub mod query_patient_profile;
|
||||
pub mod query_vitals;
|
||||
pub mod search_medical_knowledge;
|
||||
|
||||
pub use analyze_health_trends::AnalyzeHealthTrendsTool;
|
||||
pub use analyze_lab_report::AnalyzeLabReportTool;
|
||||
pub use query_appointments::QueryAppointmentsTool;
|
||||
pub use query_lab_reports::QueryLabReportsTool;
|
||||
pub use query_medications::QueryMedicationsTool;
|
||||
|
||||
@@ -7,6 +7,8 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::agent::orchestrator::AgentRunParams;
|
||||
use crate::agent::sandbox::{get_sandbox_config, resolve_role};
|
||||
use crate::agent::tool::ToolContext;
|
||||
use crate::agent::tools::AnalyzeHealthTrendsTool;
|
||||
use crate::agent::tools::AnalyzeLabReportTool;
|
||||
use crate::agent::tools::QueryPatientProfileTool;
|
||||
use crate::agent::tools::QueryPatientVitalsTool;
|
||||
use crate::agent::tools::SearchMedicalKnowledgeTool;
|
||||
@@ -125,6 +127,8 @@ where
|
||||
registry.register(std::sync::Arc::new(QueryMedicationsTool));
|
||||
registry.register(std::sync::Arc::new(SearchMedicalKnowledgeTool));
|
||||
registry.register(std::sync::Arc::new(QueryPatientProfileTool));
|
||||
registry.register(std::sync::Arc::new(AnalyzeLabReportTool));
|
||||
registry.register(std::sync::Arc::new(AnalyzeHealthTrendsTool));
|
||||
|
||||
// 根据用户角色获取沙箱配置
|
||||
let user_role = resolve_role(&ctx.roles);
|
||||
|
||||
Reference in New Issue
Block a user