diff --git a/crates/erp-ai/src/knowledge/mod.rs b/crates/erp-ai/src/knowledge/mod.rs new file mode 100644 index 0000000..1ee1d65 --- /dev/null +++ b/crates/erp-ai/src/knowledge/mod.rs @@ -0,0 +1,116 @@ +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::error::AiResult; + +/// 知识源 trait — 统一结构化和未来向量检索的知识获取接口 +#[async_trait] +pub trait KnowledgeSource: Send + Sync { + /// 根据查询获取知识上下文 + async fn get_context(&self, query: &KnowledgeQuery) -> AiResult; + + /// 知识源类型标识 + fn source_type(&self) -> &str; + + /// 健康检查 + async fn health_check(&self) -> AiResult; +} + +/// 知识查询参数 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KnowledgeQuery { + pub tenant_id: Uuid, + pub analysis_type: String, + pub patient_context: Option, + pub query_text: Option, +} + +/// 脱敏患者摘要(不含 PII) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PatientSummary { + pub age: Option, + pub gender: Option, + pub tags: Vec, +} + +/// 知识上下文(返回给 Prompt 注入) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KnowledgeContext { + pub source: String, + pub context_text: String, + pub references: Vec, + pub confidence: f32, +} + +/// 参考引用 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Reference { + pub title: String, + pub source: String, + pub relevance_score: f32, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn knowledge_query_construction() { + let query = KnowledgeQuery { + tenant_id: Uuid::now_v7(), + analysis_type: "lab_report".into(), + patient_context: Some(PatientSummary { + age: Some(65), + gender: Some("male".into()), + tags: vec!["高血压".into(), "糖尿病".into()], + }), + query_text: Some("血红蛋白偏低".into()), + }; + assert_eq!(query.analysis_type, "lab_report"); + assert_eq!(query.patient_context.as_ref().unwrap().tags.len(), 2); + } + + #[test] + fn knowledge_context_serde_roundtrip() { + let ctx = KnowledgeContext { + source: "structured".into(), + context_text: "【规则】血压 >140 需关注".into(), + references: vec![Reference { + title: "高血压指南".into(), + source: "system".into(), + relevance_score: 0.95, + }], + confidence: 0.9, + }; + let json = serde_json::to_string(&ctx).unwrap(); + let back: KnowledgeContext = serde_json::from_str(&json).unwrap(); + assert_eq!(back.source, "structured"); + assert_eq!(back.references.len(), 1); + assert!((back.confidence - 0.9).abs() < 0.01); + } + + #[test] + fn patient_summary_serde() { + let summary = PatientSummary { + age: Some(70), + gender: Some("female".into()), + tags: vec![], + }; + let json = serde_json::to_string(&summary).unwrap(); + let back: PatientSummary = serde_json::from_str(&json).unwrap(); + assert_eq!(back.age, Some(70)); + } + + #[test] + fn truncate_context_text() { + let long_text: String = "x".repeat(10000); + let max_chars = 8000; + let truncated = if long_text.len() > max_chars { + long_text[..max_chars].to_string() + } else { + long_text + }; + assert_eq!(truncated.len(), max_chars); + } +} diff --git a/crates/erp-ai/src/lib.rs b/crates/erp-ai/src/lib.rs index 7d7b9f8..c0e0dfe 100644 --- a/crates/erp-ai/src/lib.rs +++ b/crates/erp-ai/src/lib.rs @@ -3,6 +3,7 @@ pub mod dto; pub mod entity; pub mod error; pub mod handler; +pub mod knowledge; pub mod module; pub mod prompt; pub mod provider;