feat(ai): Phase 3A RAG 知识库 — CRUD API + Agent Tool + 向量知识源 + 前端管理页
- 知识库 REST API: 10 个端点 (references/guides CRUD + re-embed) - search_medical_knowledge Agent Tool: 语义检索参考资料和临床指南 - VectorKnowledgeSource: 实现 KnowledgeSource trait,自动降级 - 沙箱配置: Patient/MedicalStaff 允许使用知识库检索 - 前端 AiKnowledgePage: Tabs(参考资料/临床指南) + Table + Modal CRUD - 权限码 seed 迁移: ai.knowledge.list + ai.knowledge.manage + 菜单
This commit is contained in:
@@ -44,6 +44,7 @@ pub fn get_sandbox_config(role: &UserRole) -> SandboxConfig {
|
||||
"query_patient_vitals".into(),
|
||||
"query_patient_lab_reports".into(),
|
||||
"query_patient_medications".into(),
|
||||
"search_medical_knowledge".into(),
|
||||
]),
|
||||
system_prompt_suffix: PATIENT_PROMPT_SUFFIX,
|
||||
output_filter: OutputFilter {
|
||||
@@ -59,6 +60,7 @@ pub fn get_sandbox_config(role: &UserRole) -> SandboxConfig {
|
||||
"query_patient_lab_reports".into(),
|
||||
"query_patient_appointments".into(),
|
||||
"query_patient_medications".into(),
|
||||
"search_medical_knowledge".into(),
|
||||
]),
|
||||
system_prompt_suffix: MEDICAL_STAFF_PROMPT_SUFFIX,
|
||||
output_filter: OutputFilter {
|
||||
|
||||
@@ -4,8 +4,10 @@ pub mod query_appointments;
|
||||
pub mod query_lab_reports;
|
||||
pub mod query_medications;
|
||||
pub mod query_vitals;
|
||||
pub mod search_medical_knowledge;
|
||||
|
||||
pub use query_appointments::QueryAppointmentsTool;
|
||||
pub use query_lab_reports::QueryLabReportsTool;
|
||||
pub use query_medications::QueryMedicationsTool;
|
||||
pub use query_vitals::QueryPatientVitalsTool;
|
||||
pub use search_medical_knowledge::SearchMedicalKnowledgeTool;
|
||||
|
||||
112
crates/erp-ai/src/agent/tools/search_medical_knowledge.rs
Normal file
112
crates/erp-ai/src/agent/tools/search_medical_knowledge.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::agent::tool::{AgentTool, ToolContext, ToolResult};
|
||||
use crate::service::embedding::EmbeddingService;
|
||||
|
||||
/// 语义检索医学知识库(参考资料 + 临床指南)
|
||||
pub struct SearchMedicalKnowledgeTool;
|
||||
|
||||
#[async_trait]
|
||||
impl AgentTool for SearchMedicalKnowledgeTool {
|
||||
fn name(&self) -> &str {
|
||||
"search_medical_knowledge"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"在医学知识库中语义检索相关参考资料和临床指南。输入查询文本,返回最相关的知识条目。可选按分析类型过滤(如 trend、lab_report、dialysis_risk)。"
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"required": ["query"],
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "搜索查询文本,如'高血压管理指南'或'血红蛋白偏低原因'"
|
||||
},
|
||||
"analysis_type": {
|
||||
"type": "string",
|
||||
"description": "可选:按分析类型过滤(trend、lab_report、dialysis_risk 等)"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &ToolContext, params: serde_json::Value) -> ToolResult {
|
||||
let query_text = match params["query"].as_str() {
|
||||
Some(q) if !q.trim().is_empty() => q.to_string(),
|
||||
_ => {
|
||||
return ToolResult {
|
||||
output: "请提供搜索查询文本".to_string(),
|
||||
display_hint: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let analysis_type = params["analysis_type"].as_str();
|
||||
|
||||
let embedding_svc = EmbeddingService::from_settings(&ctx.db).await;
|
||||
|
||||
if !embedding_svc.is_configured() {
|
||||
return ToolResult {
|
||||
output: "Embedding API 未配置,无法进行语义搜索".to_string(),
|
||||
display_hint: None,
|
||||
};
|
||||
}
|
||||
|
||||
let embedding = match embedding_svc.embed(&query_text).await {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
return ToolResult {
|
||||
output: format!("生成查询向量失败: {}", e),
|
||||
display_hint: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let results = match crate::knowledge::vector_search::KnowledgeSearchRepository::search(
|
||||
&ctx.db,
|
||||
ctx.tenant_id,
|
||||
analysis_type,
|
||||
&embedding,
|
||||
5,
|
||||
0.6,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return ToolResult {
|
||||
output: format!("知识库搜索失败: {}", e),
|
||||
display_hint: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if results.is_empty() {
|
||||
return ToolResult {
|
||||
output: "未找到相关的医学知识条目".to_string(),
|
||||
display_hint: None,
|
||||
};
|
||||
}
|
||||
|
||||
let mut output = String::from("知识库检索结果:\n\n");
|
||||
for (i, r) in results.iter().enumerate() {
|
||||
output.push_str(&format!(
|
||||
"{}. [{}] {}(相似度: {}%)\n 来源: {}\n {}\n\n",
|
||||
i + 1,
|
||||
r.source_table,
|
||||
r.title,
|
||||
(r.similarity * 100.0) as u32,
|
||||
r.source_name,
|
||||
r.content.chars().take(200).collect::<String>(),
|
||||
));
|
||||
}
|
||||
|
||||
ToolResult {
|
||||
output,
|
||||
display_hint: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user