feat(ai): 实现 AI 数据桥接 — 4 个 HealthDataProvider 方法从 stub 替换为真实查询
- get_lab_report: 查询 lab_report + patient,解析 JSON items 构造 LabReportDto - get_vital_signs: 查询 vital_signs 时间序列,按指标提取 8 种体征数据 - get_patient_summary: 聚合 patient + diagnosis + medication_record + health_record - get_full_report: 查询 health_record + 关联诊断和化验报告构造章节 - AiState 新增 health_provider 字段,erp-server 注入 HealthDataProviderImpl - 4 个 SSE handler 从 placeholder JSON 改为调用 provider + sanitizer 真实数据流
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use axum::extract::{Extension, FromRef, Path, Query, State};
|
||||
use axum::response::sse::{Event, KeepAlive, Sse};
|
||||
use axum::Json;
|
||||
use erp_core::health_provider::TimeRange;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, TenantContext};
|
||||
use futures::StreamExt;
|
||||
@@ -35,6 +36,12 @@ where
|
||||
erp_core::error::AppError::Validation("report_id 必填".into())
|
||||
})?;
|
||||
|
||||
let lab_dto = state
|
||||
.health_provider
|
||||
.get_lab_report(ctx.tenant_id, report_id)
|
||||
.await?;
|
||||
let sanitized_data = state.analysis.sanitizer.sanitize_lab_report(&lab_dto)?;
|
||||
|
||||
let prompt = state
|
||||
.prompt
|
||||
.get_active_prompt(ctx.tenant_id, "lab_report_interpretation")
|
||||
@@ -55,7 +62,7 @@ where
|
||||
report_id.to_string(),
|
||||
prompt.system_prompt,
|
||||
prompt.user_prompt_template,
|
||||
serde_json::json!({"placeholder": true}),
|
||||
sanitized_data,
|
||||
model,
|
||||
temperature,
|
||||
max_tokens,
|
||||
@@ -65,53 +72,7 @@ where
|
||||
let analysis_id_clone = analysis_id;
|
||||
let state_clone = state.clone();
|
||||
|
||||
let sse_stream = async_stream::stream! {
|
||||
let mut full_content = String::new();
|
||||
let mut index: u32 = 0;
|
||||
|
||||
let mut stream = std::pin::pin!(stream);
|
||||
while let Some(result) = stream.next().await {
|
||||
match result {
|
||||
Ok(chunk) => {
|
||||
full_content.push_str(&chunk);
|
||||
index += 1;
|
||||
let event = AnalysisSseEvent::Chunk {
|
||||
content: chunk,
|
||||
index,
|
||||
};
|
||||
let data = serde_json::to_string(&event).unwrap_or_default();
|
||||
yield Ok(Event::default().event("chunk").data(data));
|
||||
}
|
||||
Err(e) => {
|
||||
let event = AnalysisSseEvent::Error {
|
||||
message: e.to_string(),
|
||||
};
|
||||
let data = serde_json::to_string(&event).unwrap_or_default();
|
||||
yield Ok(Event::default().event("error").data(data));
|
||||
let _ = state_clone
|
||||
.analysis
|
||||
.fail_analysis(analysis_id_clone, e.to_string())
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 完成后存储结果
|
||||
let metadata = serde_json::json!({"analysis_type": "lab_report"});
|
||||
let _ = state_clone
|
||||
.analysis
|
||||
.complete_analysis(analysis_id_clone, full_content, metadata)
|
||||
.await;
|
||||
|
||||
let done_event = AnalysisSseEvent::Done {
|
||||
analysis_id: analysis_id_clone,
|
||||
status: "completed".into(),
|
||||
};
|
||||
let data = serde_json::to_string(&done_event).unwrap_or_default();
|
||||
yield Ok(Event::default().event("done").data(data));
|
||||
};
|
||||
|
||||
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "lab_report");
|
||||
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
|
||||
}
|
||||
|
||||
@@ -129,6 +90,26 @@ where
|
||||
erp_core::error::AppError::Validation("patient_id 必填".into())
|
||||
})?;
|
||||
|
||||
let metrics = body.metrics.unwrap_or_else(|| {
|
||||
vec![
|
||||
"systolic_bp_morning".into(),
|
||||
"diastolic_bp_morning".into(),
|
||||
"heart_rate".into(),
|
||||
"weight".into(),
|
||||
"blood_sugar".into(),
|
||||
]
|
||||
});
|
||||
let range = TimeRange {
|
||||
start: chrono::Utc::now() - chrono::Duration::days(90),
|
||||
end: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let vital_dtos = state
|
||||
.health_provider
|
||||
.get_vital_signs(ctx.tenant_id, patient_id, &metrics, &range)
|
||||
.await?;
|
||||
let sanitized_data = state.analysis.sanitizer.sanitize_vital_signs(&vital_dtos)?;
|
||||
|
||||
let prompt = state
|
||||
.prompt
|
||||
.get_active_prompt(ctx.tenant_id, "health_trend_analysis")
|
||||
@@ -149,7 +130,7 @@ where
|
||||
patient_id.to_string(),
|
||||
prompt.system_prompt,
|
||||
prompt.user_prompt_template,
|
||||
serde_json::json!({"placeholder": true}),
|
||||
sanitized_data,
|
||||
model,
|
||||
temperature,
|
||||
max_tokens,
|
||||
@@ -177,6 +158,15 @@ where
|
||||
erp_core::error::AppError::Validation("patient_id 必填".into())
|
||||
})?;
|
||||
|
||||
let summary_dto = state
|
||||
.health_provider
|
||||
.get_patient_summary(ctx.tenant_id, patient_id)
|
||||
.await?;
|
||||
let sanitized_data = state
|
||||
.analysis
|
||||
.sanitizer
|
||||
.sanitize_patient_summary(&summary_dto)?;
|
||||
|
||||
let prompt = state
|
||||
.prompt
|
||||
.get_active_prompt(ctx.tenant_id, "personalized_checkup_plan")
|
||||
@@ -197,7 +187,7 @@ where
|
||||
patient_id.to_string(),
|
||||
prompt.system_prompt,
|
||||
prompt.user_prompt_template,
|
||||
serde_json::json!({"placeholder": true}),
|
||||
sanitized_data,
|
||||
model,
|
||||
temperature,
|
||||
max_tokens,
|
||||
@@ -225,6 +215,15 @@ where
|
||||
erp_core::error::AppError::Validation("report_id 必填".into())
|
||||
})?;
|
||||
|
||||
let report_dto = state
|
||||
.health_provider
|
||||
.get_full_report(ctx.tenant_id, report_id)
|
||||
.await?;
|
||||
let sanitized_data = state
|
||||
.analysis
|
||||
.sanitizer
|
||||
.sanitize_health_report(&report_dto)?;
|
||||
|
||||
let prompt = state
|
||||
.prompt
|
||||
.get_active_prompt(ctx.tenant_id, "report_summary_generation")
|
||||
@@ -245,7 +244,7 @@ where
|
||||
report_id.to_string(),
|
||||
prompt.system_prompt,
|
||||
prompt.user_prompt_template,
|
||||
serde_json::json!({"placeholder": true}),
|
||||
sanitized_data,
|
||||
model,
|
||||
temperature,
|
||||
max_tokens,
|
||||
|
||||
Reference in New Issue
Block a user