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:
@@ -8,6 +8,7 @@ use crate::agent::orchestrator::AgentRunParams;
|
||||
use crate::agent::sandbox::{get_sandbox_config, resolve_role};
|
||||
use crate::agent::tool::ToolContext;
|
||||
use crate::agent::tools::QueryPatientVitalsTool;
|
||||
use crate::agent::tools::SearchMedicalKnowledgeTool;
|
||||
use crate::agent::tools::{QueryAppointmentsTool, QueryLabReportsTool, QueryMedicationsTool};
|
||||
use crate::agent::{AgentOrchestrator, ToolRegistry};
|
||||
use crate::config_resolver;
|
||||
@@ -121,6 +122,7 @@ where
|
||||
registry.register(std::sync::Arc::new(QueryLabReportsTool));
|
||||
registry.register(std::sync::Arc::new(QueryAppointmentsTool));
|
||||
registry.register(std::sync::Arc::new(QueryMedicationsTool));
|
||||
registry.register(std::sync::Arc::new(SearchMedicalKnowledgeTool));
|
||||
|
||||
// 根据用户角色获取沙箱配置
|
||||
let user_role = resolve_role(&ctx.roles);
|
||||
|
||||
296
crates/erp-ai/src/handler/knowledge_handler.rs
Normal file
296
crates/erp-ai/src/handler/knowledge_handler.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
use axum::Json;
|
||||
use axum::extract::{Extension, FromRef, Path, State};
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, TenantContext};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::service::knowledge::{
|
||||
CreateKnowledgeGuideReq, CreateKnowledgeReferenceReq, ListKnowledgeQuery,
|
||||
UpdateKnowledgeGuideReq, UpdateKnowledgeReferenceReq,
|
||||
};
|
||||
use crate::state::AiState;
|
||||
|
||||
// === References ===
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ListKnowledgeParams {
|
||||
pub analysis_type: Option<String>,
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/ai/knowledge/references",
|
||||
responses((status = 200, description = "参考资料列表")),
|
||||
tag = "知识库",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn list_references<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
axum::extract::Query(params): axum::extract::Query<ListKnowledgeParams>,
|
||||
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
|
||||
where
|
||||
AiState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "ai.knowledge.list")?;
|
||||
|
||||
let query = ListKnowledgeQuery {
|
||||
analysis_type: params.analysis_type,
|
||||
page: params.page,
|
||||
page_size: params.page_size,
|
||||
};
|
||||
let items = state
|
||||
.knowledge
|
||||
.list_references(ctx.tenant_id, &query)
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse::ok(serde_json::json!({
|
||||
"data": items,
|
||||
"total": items.len(),
|
||||
}))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/ai/knowledge/references",
|
||||
responses((status = 200, description = "创建参考资料")),
|
||||
tag = "知识库",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn create_reference<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Json(body): Json<CreateKnowledgeReferenceReq>,
|
||||
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
|
||||
where
|
||||
AiState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "ai.knowledge.manage")?;
|
||||
|
||||
if body.title.trim().is_empty() {
|
||||
return Err(erp_core::error::AppError::Validation("标题不能为空".into()));
|
||||
}
|
||||
|
||||
let id = state
|
||||
.knowledge
|
||||
.create_reference(ctx.tenant_id, ctx.user_id, body)
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse::ok(serde_json::json!({ "id": id }))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/ai/knowledge/references/{id}",
|
||||
responses((status = 200, description = "更新参考资料")),
|
||||
tag = "知识库",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn update_reference<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
Json(body): Json<UpdateKnowledgeReferenceReq>,
|
||||
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
|
||||
where
|
||||
AiState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "ai.knowledge.manage")?;
|
||||
|
||||
state
|
||||
.knowledge
|
||||
.update_reference(ctx.tenant_id, ctx.user_id, id, body)
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse::ok(serde_json::json!({ "id": id }))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/ai/knowledge/references/{id}",
|
||||
responses((status = 200, description = "删除参考资料")),
|
||||
tag = "知识库",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn delete_reference<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
|
||||
where
|
||||
AiState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "ai.knowledge.manage")?;
|
||||
|
||||
state.knowledge.delete_reference(ctx.tenant_id, id).await?;
|
||||
|
||||
Ok(Json(ApiResponse::ok(serde_json::json!({ "id": id }))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/ai/knowledge/references/{id}/re-embed",
|
||||
responses((status = 200, description = "重新生成向量")),
|
||||
tag = "知识库",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn re_embed_reference<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
|
||||
where
|
||||
AiState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "ai.knowledge.manage")?;
|
||||
|
||||
state
|
||||
.knowledge
|
||||
.re_embed_reference(ctx.tenant_id, id)
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse::ok(serde_json::json!({ "id": id }))))
|
||||
}
|
||||
|
||||
// === Guides ===
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/ai/knowledge/guides",
|
||||
responses((status = 200, description = "临床指南列表")),
|
||||
tag = "知识库",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn list_guides<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
axum::extract::Query(params): axum::extract::Query<ListKnowledgeParams>,
|
||||
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
|
||||
where
|
||||
AiState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "ai.knowledge.list")?;
|
||||
|
||||
let query = ListKnowledgeQuery {
|
||||
analysis_type: params.analysis_type,
|
||||
page: params.page,
|
||||
page_size: params.page_size,
|
||||
};
|
||||
let items = state.knowledge.list_guides(ctx.tenant_id, &query).await?;
|
||||
|
||||
Ok(Json(ApiResponse::ok(serde_json::json!({
|
||||
"data": items,
|
||||
"total": items.len(),
|
||||
}))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/ai/knowledge/guides",
|
||||
responses((status = 200, description = "创建临床指南")),
|
||||
tag = "知识库",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn create_guide<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Json(body): Json<CreateKnowledgeGuideReq>,
|
||||
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
|
||||
where
|
||||
AiState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "ai.knowledge.manage")?;
|
||||
|
||||
if body.title.trim().is_empty() {
|
||||
return Err(erp_core::error::AppError::Validation("标题不能为空".into()));
|
||||
}
|
||||
|
||||
let id = state
|
||||
.knowledge
|
||||
.create_guide(ctx.tenant_id, ctx.user_id, body)
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse::ok(serde_json::json!({ "id": id }))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/ai/knowledge/guides/{id}",
|
||||
responses((status = 200, description = "更新临床指南")),
|
||||
tag = "知识库",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn update_guide<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
Json(body): Json<UpdateKnowledgeGuideReq>,
|
||||
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
|
||||
where
|
||||
AiState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "ai.knowledge.manage")?;
|
||||
|
||||
state
|
||||
.knowledge
|
||||
.update_guide(ctx.tenant_id, ctx.user_id, id, body)
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse::ok(serde_json::json!({ "id": id }))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/ai/knowledge/guides/{id}",
|
||||
responses((status = 200, description = "删除临床指南")),
|
||||
tag = "知识库",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn delete_guide<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
|
||||
where
|
||||
AiState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "ai.knowledge.manage")?;
|
||||
|
||||
state.knowledge.delete_guide(ctx.tenant_id, id).await?;
|
||||
|
||||
Ok(Json(ApiResponse::ok(serde_json::json!({ "id": id }))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/ai/knowledge/guides/{id}/re-embed",
|
||||
responses((status = 200, description = "重新生成向量")),
|
||||
tag = "知识库",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn re_embed_guide<S>(
|
||||
State(state): State<AiState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
|
||||
where
|
||||
AiState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "ai.knowledge.manage")?;
|
||||
|
||||
state.knowledge.re_embed_guide(ctx.tenant_id, id).await?;
|
||||
|
||||
Ok(Json(ApiResponse::ok(serde_json::json!({ "id": id }))))
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use crate::state::AiState;
|
||||
pub mod chat_handler;
|
||||
pub mod config_handler;
|
||||
pub mod insight_handler;
|
||||
pub mod knowledge_handler;
|
||||
pub mod risk_handler;
|
||||
pub mod rule_handler;
|
||||
pub mod suggestion_handler;
|
||||
|
||||
Reference in New Issue
Block a user