feat(ai): Phase 2A-4 新增 3 个 Agent Tool — 化验报告/预约/用药查询

新增 3 个 AI Agent Tool 扩展医护沙箱能力:
- query_patient_lab_reports: 查询患者化验报告列表(含异常计数)
- query_patient_appointments: 查询患者即将到来的预约
- query_patient_medications: 查询患者当前用药列表

同时:
- HealthDataProvider trait 新增 get_patient_lab_reports 方法 + LabReportListItemDto
- erp-health 实现新 trait 方法(含 PII 解密)
- sandbox.rs 更新角色权限:Patient 可查体征/化验/用药,MedicalStaff 额外可查预约
- 修复 ai_prompt_tests.rs 中 AnalysisService::new 签名变更的遗留编译错误
- 新增 5 个 agent 测试覆盖新 Tool 和沙箱权限过滤
This commit is contained in:
iven
2026-05-19 00:19:10 +08:00
parent 89581b070f
commit b2053d5bcc
10 changed files with 401 additions and 12 deletions

View File

@@ -40,7 +40,11 @@ pub fn get_sandbox_config(role: &UserRole) -> SandboxConfig {
match role {
UserRole::Patient => SandboxConfig {
role: role.clone(),
allowed_tools: HashSet::from(["query_patient_vitals".into()]),
allowed_tools: HashSet::from([
"query_patient_vitals".into(),
"query_patient_lab_reports".into(),
"query_patient_medications".into(),
]),
system_prompt_suffix: PATIENT_PROMPT_SUFFIX,
output_filter: OutputFilter {
append_disclaimer: true,
@@ -50,7 +54,12 @@ pub fn get_sandbox_config(role: &UserRole) -> SandboxConfig {
},
UserRole::MedicalStaff => SandboxConfig {
role: role.clone(),
allowed_tools: HashSet::from(["query_patient_vitals".into()]),
allowed_tools: HashSet::from([
"query_patient_vitals".into(),
"query_patient_lab_reports".into(),
"query_patient_appointments".into(),
"query_patient_medications".into(),
]),
system_prompt_suffix: MEDICAL_STAFF_PROMPT_SUFFIX,
output_filter: OutputFilter {
append_disclaimer: false,

View File

@@ -1,5 +1,11 @@
// Agent Tool 实现 — Phase 0 添加 query_patient_vitals
// Agent Tool 实现
pub mod query_appointments;
pub mod query_lab_reports;
pub mod query_medications;
pub mod query_vitals;
pub use query_appointments::QueryAppointmentsTool;
pub use query_lab_reports::QueryLabReportsTool;
pub use query_medications::QueryMedicationsTool;
pub use query_vitals::QueryPatientVitalsTool;

View File

@@ -0,0 +1,68 @@
use async_trait::async_trait;
use crate::agent::tool::{AgentTool, ToolContext, ToolResult};
/// 查询患者预约记录
pub struct QueryAppointmentsTool;
#[async_trait]
impl AgentTool for QueryAppointmentsTool {
fn name(&self) -> &str {
"query_patient_appointments"
}
fn description(&self) -> &str {
"查询患者的即将到来的预约记录,包括科室、医生、时间和状态。"
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {}
})
}
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,
};
}
};
match ctx
.health_provider
.get_upcoming_appointments(ctx.tenant_id, patient_id)
.await
{
Ok(appointments) => {
if appointments.is_empty() {
return ToolResult {
output: "该患者暂无即将到来的预约".to_string(),
display_hint: None,
};
}
let mut output = String::from("即将到来的预约:\n");
for a in &appointments {
output.push_str(&format!(
"- {} | {} | {} | 状态: {}\n",
a.scheduled_at, a.department, a.doctor_name, a.status
));
}
ToolResult {
output,
display_hint: None,
}
}
Err(e) => ToolResult {
output: format!("查询预约记录失败: {}", e),
display_hint: None,
},
}
}
}

View File

@@ -0,0 +1,85 @@
use async_trait::async_trait;
use crate::agent::tool::{AgentTool, DisplayHint, ToolContext, ToolResult};
/// 查询患者化验报告列表(简要摘要)
pub struct QueryLabReportsTool;
#[async_trait]
impl AgentTool for QueryLabReportsTool {
fn name(&self) -> &str {
"query_patient_lab_reports"
}
fn description(&self) -> &str {
"查询患者的化验报告列表(如血常规、肾功能、肝功能等)。返回每份报告的类型、日期和异常指标数量。"
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"limit": {
"type": "integer",
"description": "返回最近几份报告,默认 5 份"
}
}
})
}
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 limit = params["limit"].as_i64().unwrap_or(5) as u64;
match ctx
.health_provider
.get_patient_lab_reports(ctx.tenant_id, patient_id, limit)
.await
{
Ok(reports) => {
if reports.is_empty() {
return ToolResult {
output: "该患者暂无化验报告记录".to_string(),
display_hint: None,
};
}
let mut output = String::from("化验报告列表:\n");
for r in &reports {
let abnormal = if r.abnormal_count > 0 {
format!("{} 项异常)", r.abnormal_count)
} else {
"(正常)".to_string()
};
output.push_str(&format!(
"- [{}] {} {}{}\n",
r.report_date, r.report_type, abnormal, r.id
));
}
let display_hint = reports.first().map(|r| DisplayHint::LabReportCard {
report_date: r.report_date.clone(),
abnormal_count: r.abnormal_count,
});
ToolResult {
output,
display_hint,
}
}
Err(e) => ToolResult {
output: format!("查询化验报告失败: {}", e),
display_hint: None,
},
}
}
}

View File

@@ -0,0 +1,65 @@
use async_trait::async_trait;
use crate::agent::tool::{AgentTool, ToolContext, ToolResult};
/// 查询患者当前用药列表
pub struct QueryMedicationsTool;
#[async_trait]
impl AgentTool for QueryMedicationsTool {
fn name(&self) -> &str {
"query_patient_medications"
}
fn description(&self) -> &str {
"查询患者当前的用药列表,包括药品名称、剂量和用药频率。"
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {}
})
}
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,
};
}
};
match ctx
.health_provider
.get_medication_list(ctx.tenant_id, patient_id)
.await
{
Ok(medications) => {
if medications.is_empty() {
return ToolResult {
output: "该患者暂无当前用药记录".to_string(),
display_hint: None,
};
}
let mut output = String::from("当前用药列表:\n");
for m in &medications {
output.push_str(&format!("- {} {} {}\n", m.name, m.dosage, m.frequency));
}
ToolResult {
output,
display_hint: None,
}
}
Err(e) => ToolResult {
output: format!("查询用药记录失败: {}", e),
display_hint: None,
},
}
}
}