feat(ai): 改造 chat_handler 接入 AgentOrchestrator — ReAct Agent 首次跑通 + 新增会话权限码
This commit is contained in:
@@ -4,7 +4,10 @@ use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, TenantContext};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::dto::GenerateRequest;
|
||||
use crate::agent::tool::ToolContext;
|
||||
use crate::agent::tools::QueryPatientVitalsTool;
|
||||
use crate::agent::{AgentOrchestrator, ToolRegistry};
|
||||
use crate::dto::{ChatMessage, ChatMessageRole};
|
||||
use crate::state::AiState;
|
||||
|
||||
// === 请求 / 响应 ===
|
||||
@@ -13,6 +16,8 @@ use crate::state::AiState;
|
||||
pub struct ChatRequest {
|
||||
pub message: String,
|
||||
pub history: Option<Vec<ChatHistoryItem>>,
|
||||
/// 可选:关联患者 ID(从用户档案中获取)
|
||||
pub patient_id: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)]
|
||||
@@ -25,26 +30,46 @@ pub struct ChatHistoryItem {
|
||||
pub struct ChatResponse {
|
||||
pub reply: String,
|
||||
pub message_id: String,
|
||||
pub iterations: usize,
|
||||
}
|
||||
|
||||
const SYSTEM_PROMPT: &str = r#"你是 HMS 健康管理平台的 AI 客服助手"小华"。你的职责是:
|
||||
1. 回答用户的健康咨询问题
|
||||
2. 帮助用户了解体检报告指标
|
||||
3. 提供预约挂号、用药提醒等服务指导
|
||||
4. 推荐健康生活方式
|
||||
const SYSTEM_PROMPT: &str = r#"你是 HMS 健康管理平台的 AI 健康顾问"小华"。
|
||||
|
||||
注意:
|
||||
- 你不能替代医生的诊断,遇到需要诊断的问题请建议用户就医
|
||||
- 不能推荐具体药物,只能提供一般性健康建议
|
||||
- 语气要亲切、专业、耐心
|
||||
- 回复要简洁明了,避免过长
|
||||
- 如果用户问的问题超出健康范围,礼貌引导回到健康话题"#;
|
||||
## 核心策略
|
||||
根据用户表达的内容和情绪,自然地采用以下策略方向:
|
||||
|
||||
1. 【情绪安抚】当用户表达焦虑、恐惧、沮丧时:
|
||||
- 先共情认可感受,不急于给建议
|
||||
- 用通俗语言解释,避免医学术语
|
||||
- 分享积极案例,降低恐惧感
|
||||
|
||||
2. 【医疗科普】当用户询问指标含义、疾病知识时:
|
||||
- 调用 search_medical_knowledge 获取准确信息(如可用)
|
||||
- 用比喻和类比让老年患者也能理解
|
||||
- 强调"具体请以医生诊断为准"
|
||||
|
||||
3. 【服务推荐】当用户表达就医需求或身体不适时:
|
||||
- 调用 query_appointments 查看已有预约(如可用)
|
||||
- 主动提出帮用户预约
|
||||
|
||||
4. 【风险预警】当用户描述的症状或数据异常时:
|
||||
- 调用 query_patient_vitals 查看体征数据
|
||||
- 明确告知风险等级和需要注意的事项
|
||||
- 高风险时建议尽快就医
|
||||
|
||||
5. 【引导到院】当用户有明确就诊意向或高风险预警时:
|
||||
- 提供科室位置、出诊医生信息
|
||||
- 建议用户联系前台预约
|
||||
|
||||
## 策略不是互斥的,你可以在一轮对话中自然切换。
|
||||
## 永远不要:推荐具体药物、给出明确诊断、替代医生建议。
|
||||
## 如果没有可用的工具数据,就基于常识回答,并建议用户咨询医生。"#;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/ai/chat",
|
||||
request_body = ChatRequest,
|
||||
responses((status = 200, description = "AI 客服回复")),
|
||||
responses((status = 200, description = "AI Agent 回复")),
|
||||
tag = "AI 客服",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
@@ -69,30 +94,41 @@ where
|
||||
));
|
||||
}
|
||||
|
||||
let user_prompt = match body.history {
|
||||
Some(ref hist) if !hist.is_empty() => {
|
||||
let filtered: Vec<&ChatHistoryItem> = hist
|
||||
.iter()
|
||||
.filter(|h| h.role == "user" || h.role == "assistant")
|
||||
.collect();
|
||||
let start = filtered.len().saturating_sub(10);
|
||||
let ctx: String = filtered[start..]
|
||||
.iter()
|
||||
.map(|h| {
|
||||
format!(
|
||||
"{}: {}",
|
||||
if h.role == "user" { "用户" } else { "助手" },
|
||||
h.content
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
format!("历史对话:\n{}\n\n用户最新消息: {}", ctx, message)
|
||||
}
|
||||
_ => message.to_string(),
|
||||
};
|
||||
|
||||
let ai_state = AiState::from_ref(&state);
|
||||
|
||||
// 构建 Agent 消息历史
|
||||
let mut messages = vec![];
|
||||
|
||||
// 将前端传来的历史转换为 Agent ChatMessage
|
||||
if let Some(ref hist) = body.history {
|
||||
let filtered: Vec<&ChatHistoryItem> = hist
|
||||
.iter()
|
||||
.filter(|h| h.role == "user" || h.role == "assistant")
|
||||
.collect();
|
||||
let start = filtered.len().saturating_sub(10);
|
||||
for h in &filtered[start..] {
|
||||
messages.push(ChatMessage {
|
||||
role: if h.role == "user" {
|
||||
ChatMessageRole::User
|
||||
} else {
|
||||
ChatMessageRole::Assistant
|
||||
},
|
||||
content: h.content.clone(),
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 添加当前用户消息
|
||||
messages.push(ChatMessage {
|
||||
role: ChatMessageRole::User,
|
||||
content: message.to_string(),
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
});
|
||||
|
||||
// 解析 Provider
|
||||
let resolved = ai_state
|
||||
.provider_registry
|
||||
.resolve("auto")
|
||||
@@ -102,37 +138,51 @@ where
|
||||
erp_core::error::AppError::Internal("AI 服务暂时不可用,请稍后再试".into())
|
||||
})?;
|
||||
|
||||
let req = GenerateRequest {
|
||||
system_prompt: SYSTEM_PROMPT.to_string(),
|
||||
user_prompt,
|
||||
model: String::new(),
|
||||
temperature: 0.7,
|
||||
max_tokens: 1024,
|
||||
// 构建 ToolRegistry — Phase 0 只有 query_patient_vitals
|
||||
let mut registry = ToolRegistry::new();
|
||||
registry.register(std::sync::Arc::new(QueryPatientVitalsTool));
|
||||
|
||||
let tool_ctx = ToolContext {
|
||||
tenant_id: ctx.tenant_id,
|
||||
user_id: ctx.user_id,
|
||||
patient_id: body.patient_id,
|
||||
db: ai_state.db.clone(),
|
||||
health_provider: ai_state.health_provider.clone(),
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
tenant_id = %ctx.tenant_id,
|
||||
user_id = %ctx.user_id,
|
||||
patient_id = ?body.patient_id,
|
||||
msg_len = message.len(),
|
||||
"AI chat request"
|
||||
"AI Agent chat request"
|
||||
);
|
||||
|
||||
let resp = resolved.provider().generate(req).await.map_err(|e| {
|
||||
tracing::error!(error = %e, "AI chat generate failed");
|
||||
erp_core::error::AppError::Internal("AI 服务暂时不可用,请稍后再试".into())
|
||||
})?;
|
||||
// 执行 Agent ReAct 循环
|
||||
let provider_arc = resolved.into_arc();
|
||||
let orchestrator = AgentOrchestrator::new(provider_arc, std::sync::Arc::new(registry));
|
||||
let result = orchestrator
|
||||
.run(SYSTEM_PROMPT, &mut messages, &tool_ctx)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(error = %e, "AI Agent run failed");
|
||||
erp_core::error::AppError::Internal("AI 服务暂时不可用,请稍后再试".into())
|
||||
})?;
|
||||
|
||||
let message_id = uuid::Uuid::now_v7().to_string();
|
||||
|
||||
tracing::info!(
|
||||
tenant_id = %ctx.tenant_id,
|
||||
message_id = %message_id,
|
||||
tokens = resp.output_tokens,
|
||||
"AI chat response sent"
|
||||
iterations = result.iterations,
|
||||
input_tokens = result.total_input_tokens,
|
||||
output_tokens = result.total_output_tokens,
|
||||
"AI Agent response sent"
|
||||
);
|
||||
|
||||
Ok(Json(ApiResponse::ok(ChatResponse {
|
||||
reply: resp.content,
|
||||
reply: result.reply,
|
||||
message_id,
|
||||
iterations: result.iterations,
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -100,6 +100,31 @@ impl ErpModule for AiModule {
|
||||
description: "创建/编辑/删除 Copilot 规则".into(),
|
||||
module: "ai".into(),
|
||||
},
|
||||
// AI 客服会话权限
|
||||
PermissionDescriptor {
|
||||
code: "ai.chat.send".into(),
|
||||
name: "AI 客服对话".into(),
|
||||
description: "向 AI 客服发送消息".into(),
|
||||
module: "ai".into(),
|
||||
},
|
||||
PermissionDescriptor {
|
||||
code: "ai.chat.session.list".into(),
|
||||
name: "查看 AI 会话列表".into(),
|
||||
description: "查看用户的 AI 客服会话列表".into(),
|
||||
module: "ai".into(),
|
||||
},
|
||||
PermissionDescriptor {
|
||||
code: "ai.chat.session.manage".into(),
|
||||
name: "管理 AI 会话".into(),
|
||||
description: "创建/关闭 AI 客服会话".into(),
|
||||
module: "ai".into(),
|
||||
},
|
||||
PermissionDescriptor {
|
||||
code: "ai.chat.session.history".into(),
|
||||
name: "查看 AI 会话历史".into(),
|
||||
description: "查看 AI 客服会话消息历史".into(),
|
||||
module: "ai".into(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user