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

@@ -4,8 +4,9 @@ use erp_core::crypto::{self as pii, PiiCrypto};
use erp_core::error::{AppError, AppResult};
use erp_core::health_provider::{
AnomalyInfo, AppointmentSummaryDto, HealthDataProvider, HealthReportDto, LabItemDto,
LabReportDto, MedicationSummaryDto, MetricTrendAnalysis, PatientSummaryDto, RegressionStats,
ReportSectionDto, TimeRange, TrendAnalysisDto, TrendDirection, VitalSignDto,
LabReportDto, LabReportListItemDto, MedicationSummaryDto, MetricTrendAnalysis,
PatientSummaryDto, RegressionStats, ReportSectionDto, TimeRange, TrendAnalysisDto,
TrendDirection, VitalSignDto,
};
use num_traits::ToPrimitive;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect};
@@ -623,4 +624,48 @@ impl HealthDataProvider for HealthDataProviderImpl {
Ok(result)
}
async fn get_patient_lab_reports(
&self,
tenant_id: Uuid,
patient_id: Uuid,
limit: u64,
) -> AppResult<Vec<LabReportListItemDto>> {
let _ = find_patient(&self.db, tenant_id, patient_id).await?;
let records = lab_report::Entity::find()
.filter(lab_report::Column::TenantId.eq(tenant_id))
.filter(lab_report::Column::PatientId.eq(patient_id))
.filter(lab_report::Column::DeletedAt.is_null())
.order_by_desc(lab_report::Column::ReportDate)
.limit(limit)
.all(&self.db)
.await?;
let result = records
.into_iter()
.map(|r| {
let kek = self.crypto.kek();
let decrypted_items = r
.items
.as_ref()
.and_then(|v| v.as_str())
.and_then(|s| pii::decrypt(kek, s).ok())
.and_then(|s| serde_json::from_str(&s).ok())
.or(r.items.clone());
let abnormal_count = parse_lab_items(&decrypted_items)
.iter()
.filter(|i| i.is_abnormal)
.count();
LabReportListItemDto {
id: r.id,
report_type: r.report_type,
report_date: r.report_date.to_string(),
abnormal_count,
}
})
.collect();
Ok(result)
}
}