= {
+ critical: '#cf1322',
+ high: 'red',
+ medium: 'orange',
+ low: 'green',
+};
+
+export default function RichMessage({ hints }: { hints: DisplayHint[] }) {
+ const { token } = theme.useToken();
+
+ return (
+
+ {hints.map((hint, i) => (
+
+ ))}
+
+ );
+}
+
+function RichHint({ hint, token }: { hint: DisplayHint; token: { colorBorderSecondary: string; colorTextSecondary: string; colorPrimary: string } }) {
+ switch (hint.type) {
+ case 'insight_card':
+ return (
+
+
+ {hint.title}
+
+ }
+ styles={{ body: { padding: '6px 12px' } }}
+ >
+
+ {hint.items.map((item, j) => (
+
+ {item}
+
+ ))}
+
+
+ );
+
+ case 'risk_alert':
+ return (
+
+
+ {hint.message}
+
+ );
+
+ case 'lab_report_card':
+ return (
+
+
+ 化验报告 {hint.report_date}
+
+ }
+ styles={{ body: { padding: '6px 12px', fontSize: 12 } }}
+ >
+ {hint.abnormal_count > 0 ? (
+
+ {hint.abnormal_count} 项异常
+
+ ) : (
+ 正常
+ )}
+
+ );
+
+ case 'trend_chart':
+ return (
+
+
+ 趋势分析({hint.period})
+
+ }
+ styles={{ body: { padding: '6px 12px', fontSize: 12 } }}
+ >
+
+ {hint.metrics.map((m, j) => (
+ {m}
+ ))}
+
+ {hint.summary}
+
+ );
+
+ case 'patient_profile':
+ return (
+
+
+ 患者档案
+
+ }
+ styles={{ body: { padding: '6px 12px', fontSize: 12 } }}
+ >
+ {hint.chronic_conditions.length > 0 && (
+
+ {hint.chronic_conditions.map((c, j) => (
+ {c}
+ ))}
+
+ )}
+ {hint.medication_count > 0 && (
+ 当前用药 {hint.medication_count} 种
+ )}
+
+ );
+
+ case 'vital_card':
+ return (
+
+
+ 体征数据
+
+ }
+ styles={{ body: { padding: '6px 12px', fontSize: 12 } }}
+ >
+
+ {hint.values.slice(-5).map(([date, val], j) => (
+
+ {date.slice(5)}:{' '}
+ {val} {hint.unit}
+
+ ))}
+
+
+ );
+
+ case 'action_confirm':
+ return (
+
+ {hint.summary}
+
+ );
+
+ case 'text':
+ default:
+ return null;
+ }
+}
diff --git a/crates/erp-ai/src/agent/orchestrator.rs b/crates/erp-ai/src/agent/orchestrator.rs
index 7c210c6..987f671 100644
--- a/crates/erp-ai/src/agent/orchestrator.rs
+++ b/crates/erp-ai/src/agent/orchestrator.rs
@@ -49,6 +49,7 @@ pub struct AgentRunResult {
pub total_output_tokens: u32,
pub iterations: usize,
pub tool_calls: Vec,
+ pub display_hints: Vec,
}
impl AgentOrchestrator {
@@ -76,6 +77,7 @@ impl AgentOrchestrator {
let mut total_input_tokens = 0u32;
let mut total_output_tokens = 0u32;
let mut tool_call_logs: Vec = Vec::new();
+ let mut display_hints: Vec = Vec::new();
loop {
iterations += 1;
@@ -107,6 +109,7 @@ impl AgentOrchestrator {
total_output_tokens,
iterations,
tool_calls: tool_call_logs,
+ display_hints,
});
}
};
@@ -155,21 +158,25 @@ impl AgentOrchestrator {
// 执行每个 Tool Call(受沙箱 allowed_tools 约束)
for tc in &tool_calls {
let start = std::time::Instant::now();
- let (tool_result, success) = match self.tool_registry.get(&tc.name) {
+ let (tool_result, success, hint) = match self.tool_registry.get(&tc.name) {
Some(tool) => {
if let Some(allowed) = allowed_tools {
if !allowed.contains(tc.name.as_str()) {
- (format!("Tool '{}' 在当前角色下不可用", tc.name), false)
+ (
+ format!("Tool '{}' 在当前角色下不可用", tc.name),
+ false,
+ None,
+ )
} else {
let result = tool.execute(ctx, tc.arguments.clone()).await;
- (result.output, true)
+ (result.output, true, result.display_hint)
}
} else {
let result = tool.execute(ctx, tc.arguments.clone()).await;
- (result.output, true)
+ (result.output, true, result.display_hint)
}
}
- None => (format!("未知 Tool: {}", tc.name), false),
+ None => (format!("未知 Tool: {}", tc.name), false, None),
};
let duration = start.elapsed();
@@ -179,6 +186,10 @@ impl AgentOrchestrator {
success,
});
+ if let Some(h) = hint {
+ display_hints.push(h);
+ }
+
messages.push(ChatMessage {
role: ChatMessageRole::Tool,
content: tool_result,
diff --git a/crates/erp-ai/src/agent/tool.rs b/crates/erp-ai/src/agent/tool.rs
index faa5fc4..6377dd5 100644
--- a/crates/erp-ai/src/agent/tool.rs
+++ b/crates/erp-ai/src/agent/tool.rs
@@ -30,7 +30,7 @@ pub struct ToolResult {
}
/// 前端渲染提示 — 告诉前端如何富化展示 Tool 返回的数据
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DisplayHint {
VitalCard {
diff --git a/crates/erp-ai/src/handler/chat_handler.rs b/crates/erp-ai/src/handler/chat_handler.rs
index 0c38f19..827ef64 100644
--- a/crates/erp-ai/src/handler/chat_handler.rs
+++ b/crates/erp-ai/src/handler/chat_handler.rs
@@ -40,6 +40,8 @@ pub struct ChatResponse {
pub reply: String,
pub message_id: String,
pub iterations: usize,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub display_hints: Option>,
}
#[utoipa::path(
@@ -252,5 +254,10 @@ where
reply: result.reply,
message_id,
iterations: result.iterations,
+ display_hints: if result.display_hints.is_empty() {
+ None
+ } else {
+ Some(result.display_hints)
+ },
})))
}