(
+ initialValue
+ );
+
+ React.useEffect(() => {
+ if (!src || typeof src !== "string") {
+ setResolvedSrc(null);
+ return;
+ }
+
+ if (!shouldResolve || !src.startsWith("s3://")) {
+ setResolvedSrc(src);
+ return;
+ }
+
+ const cached = getCachedMediaSrc(src);
+ if (cached) {
+ setResolvedSrc(cached);
+ return;
+ }
+
+ let cancelled = false;
+
+ resolveS3UrlToDataUrl(src)
+ .then((dataUrl) => {
+ if (cancelled) {
+ return;
+ }
+ if (dataUrl) {
+ setCachedMediaSrc(src, dataUrl);
+ setResolvedSrc(dataUrl);
+ } else {
+ setResolvedSrc(null);
+ }
+ })
+ .catch(() => {
+ if (!cancelled) {
+ setResolvedSrc(null);
+ }
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [src, shouldResolve]);
+
+ return resolvedSrc;
+};
+
+const VIDEO_EXTENSIONS = [".mp4", ".webm", ".ogg", ".mov", ".m4v"];
+
+const extractExtension = (value: string): string => {
+ const normalized = value.split("?")[0].split("#")[0];
+ const match = normalized.toLowerCase().match(/\.[a-z0-9]+$/);
+ return match?.[0] ?? "";
+};
+
+const isVideoUrl = (url?: string): boolean => {
+ if (!url) {
+ return false;
+ }
+
+ const trimmed = url.trim();
+ if (!trimmed.startsWith("http://") && !trimmed.startsWith("https://")) {
+ return false;
+ }
+
+ const extension = extractExtension(trimmed);
+ return VIDEO_EXTENSIONS.includes(extension);
+};
+
+// extract block level elements from
+const rehypeUnwrapMedia = () => {
+ return (tree: any) => {
+ visit(tree, "element", (node, index, parent) => {
+ // find
tags containing video or figure
+ if (node.tagName === "p" && node.children) {
+ const mediaChildIndex = node.children.findIndex(
+ (child: any) =>
+ child.tagName === "video" || child.tagName === "figure"
+ );
+
+ if (mediaChildIndex !== -1) {
+ // extract media elements (video/figure)
+ const mediaChild = node.children.splice(mediaChildIndex, 1)[0];
+
+ // if
has other content after extraction, keep
; otherwise remove empty
+ if (node.children.length === 0) {
+ // replace original
node with media element
+ if (parent && index !== null) {
+ parent.children[index as number] = {
+ tagName: "div",
+ properties: { className: "markdown-media-container" },
+ children: [mediaChild],
+ };
+ }
+ } else {
+ // if
has other content after extraction, keep
; otherwise remove empty
+ if (parent && index !== null) {
+ parent.children.splice((index as number) + 1, 0, {
+ tagName: "div",
+ properties: { className: "markdown-media-container" },
+ children: [mediaChild],
+ });
+ }
+ }
+ }
+ }
+ });
+ };
+};
+
+// Get background color for different tool signs
+const getBackgroundColor = (toolSign: string) => {
+ switch (toolSign) {
+ case "a":
+ return "#E3F2FD"; // Light blue
+ case "b":
+ return "#E8F5E9"; // Light green
+ case "c":
+ return "#FFF3E0"; // Light orange
+ case "d":
+ return "#F3E5F5"; // Light purple
+ case "e":
+ return "#FFEBEE"; // Light red
+ default:
+ return "#E5E5E5"; // Default light gray
+ }
+};
+
+// ============== 可点击选项组件 (平台特性开发) ==============
+// 格式: [btn:选项文字] 会渲染为可点击按钮,点击后自动填入并发送
+const ClickableOption = ({
+ text,
+ onOptionClick,
+}: {
+ text: string;
+ onOptionClick?: (text: string) => void;
+}) => {
+ const handleClick = () => {
+ // 触发自定义事件,让输入框监听并自动发送
+ const event = new CustomEvent('nexent-option-select', {
+ detail: { text, autoSend: true },
+ bubbles: true,
+ });
+ document.dispatchEvent(event);
+
+ if (onOptionClick) {
+ onOptionClick(text);
+ }
+ };
+
+ return (
+
+ );
+};
+
+// Replace the original LinkIcon component
+const CitationBadge = ({
+ toolSign,
+ citeIndex,
+}: {
+ toolSign: string;
+ citeIndex: number;
+}) => (
+
+ {citeIndex}
+
+);
+
+// Modified HoverableText component
+const HoverableText = ({
+ text,
+ searchResults,
+ onCitationHover,
+}: {
+ text: string;
+ searchResults?: SearchResult[];
+ onCitationHover?: () => void;
+}) => {
+ const [isOpen, setIsOpen] = React.useState(false);
+ const containerRef = React.useRef(null);
+ const tooltipRef = React.useRef(null);
+ const mousePositionRef = React.useRef({ x: 0, y: 0 });
+
+ // Function to handle multiple consecutive line breaks
+ const handleConsecutiveNewlines = (text: string) => {
+ if (!text) return text;
+ return (
+ text
+ // First, standardize all types of line breaks to \n
+ .replace(/\r\n/g, "\n") // Windows line breaks
+ .replace(/\r/g, "\n") // Old Mac line breaks
+ // Handle consecutive line breaks and whitespace
+ .replace(/[\n\s]*\n[\n\s]*/g, "\n") // Process whitespace around line breaks
+ .replace(/^\s+|\s+$/g, "")
+ ); // Remove leading and trailing whitespace
+ };
+
+ // Find corresponding search result
+ const toolSign = text.charAt(0);
+ const citeIndex = parseInt(text.slice(1));
+ const matchedResult = searchResults?.find(
+ (result) => result.tool_sign === toolSign && result.cite_index === citeIndex
+ );
+
+ // Handle mouse events
+ React.useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+
+ let timeoutId: NodeJS.Timeout | null = null;
+ let closeTimeoutId: NodeJS.Timeout | null = null;
+
+ // Function to update mouse position
+ const updateMousePosition = (e: MouseEvent) => {
+ mousePositionRef.current = { x: e.clientX, y: e.clientY };
+ };
+
+ const handleMouseEnter = () => {
+ // Clear any existing close timer
+ if (closeTimeoutId) {
+ clearTimeout(closeTimeoutId);
+ closeTimeoutId = null;
+ }
+
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+
+ // Clear completed conversation indicator when hovering over citation
+ if (onCitationHover) {
+ onCitationHover();
+ }
+
+ // Delay before showing tooltip to avoid quick hover triggers
+ timeoutId = setTimeout(() => {
+ setIsOpen(true);
+ }, 50);
+ };
+
+ const handleMouseLeave = () => {
+ // Clear open timer
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ timeoutId = null;
+ }
+
+ // Delay closing tooltip so user can move to tooltip content
+ closeTimeoutId = setTimeout(() => {
+ checkShouldClose();
+ }, 100);
+ };
+
+ // Function to check if tooltip should be closed
+ const checkShouldClose = () => {
+ const tooltipContent = document.querySelector(".z-\\[9999\\]");
+ const linkElement = containerRef.current;
+
+ if (!tooltipContent || !linkElement) {
+ setIsOpen(false);
+ return;
+ }
+
+ const tooltipRect = tooltipContent.getBoundingClientRect();
+ const linkRect = linkElement.getBoundingClientRect();
+ const { x: mouseX, y: mouseY } = mousePositionRef.current;
+
+ // Check if mouse is over tooltip or link icon
+ const isMouseOverTooltip =
+ mouseX >= tooltipRect.left &&
+ mouseX <= tooltipRect.right &&
+ mouseY >= tooltipRect.top &&
+ mouseY <= tooltipRect.bottom;
+
+ const isMouseOverLink =
+ mouseX >= linkRect.left &&
+ mouseX <= linkRect.right &&
+ mouseY >= linkRect.top &&
+ mouseY <= linkRect.bottom;
+
+ // Close tooltip if mouse is neither over tooltip nor link icon
+ if (!isMouseOverTooltip && !isMouseOverLink) {
+ setIsOpen(false);
+ }
+ };
+
+ // Add global mouse move event listener to handle movement anywhere
+ const handleGlobalMouseMove = (e: MouseEvent) => {
+ // Update mouse position
+ updateMousePosition(e);
+
+ if (!isOpen) return;
+
+ // Use debounce logic to avoid frequent calculations
+ if (closeTimeoutId) {
+ clearTimeout(closeTimeoutId);
+ }
+
+ closeTimeoutId = setTimeout(() => {
+ checkShouldClose();
+ }, 100);
+ };
+
+ // Add event listeners
+ document.addEventListener("mousemove", handleGlobalMouseMove);
+ container.addEventListener("mouseenter", handleMouseEnter);
+ container.addEventListener("mouseleave", handleMouseLeave);
+
+ return () => {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ if (closeTimeoutId) {
+ clearTimeout(closeTimeoutId);
+ }
+ document.removeEventListener("mousemove", handleGlobalMouseMove);
+ container.removeEventListener("mouseenter", handleMouseEnter);
+ container.removeEventListener("mouseleave", handleMouseLeave);
+ };
+ }, [isOpen, onCitationHover]);
+
+ return (
+
+
+
+
+
+
+
+
+ {/* Force Portal to body */}
+
+
+
+
+ {matchedResult ? (
+ <>
+ {matchedResult.url &&
+ matchedResult.source_type !== "file" &&
+ !matchedResult.filename ? (
+
+ {handleConsecutiveNewlines(matchedResult.title)}
+
+ ) : (
+
+ {handleConsecutiveNewlines(matchedResult.title)}
+
+ )}
+
+ {handleConsecutiveNewlines(matchedResult.text)}
+
+ >
+ ) : null}
+
+
+
+
+
+
+ );
+};
+
+/**
+ * Convert LaTeX delimiters to markdown math delimiters
+ *
+ * Converts:
+ * - \( ... \) to $ ... $
+ * - \[ ... \] to $$ ... $$
+ */
+const convertLatexDelimiters = (content: string): string => {
+ // Quick check: only process if LaTeX delimiters are present
+ if (!content.includes('\\(') && !content.includes('\\[')) {
+ return content;
+ }
+
+ return (
+ content
+ // Convert \( ... \) to $ ... $ (inline math)
+ .replace(/\\\(([\s\S]*?)\\\)/g, (_match, inner) => `$${inner}$`)
+ // Convert \[ ... \] to $$ ... $$ (display math)
+ .replace(/\\\[([\s\S]*?)\\\]/g, (_match, inner) => `$$${inner}$$\n`)
+ );
+};
+
+// Video component with error handling - defined outside to prevent re-creation on each render
+interface VideoWithErrorHandlingProps {
+ src: string;
+ alt?: string | null;
+ props?: React.VideoHTMLAttributes;
+}
+
+const VideoWithErrorHandling: React.FC = React.memo(({ src, alt, props = {} }) => {
+ const { t } = useTranslation("common");
+ const [hasError, setHasError] = React.useState(false);
+
+ if (hasError) {
+ return (
+
+
+ {t("chatStreamMessage.videoLinkUnavailable", {
+ defaultValue: "This video link is unavailable",
+ })}
+
+ {alt && (
+
{alt}
+ )}
+
+ );
+ }
+
+ return (
+
+
+ {alt ? (
+ {alt}
+ ) : null}
+
+ );
+}, (prevProps, nextProps) => {
+ // Custom comparison function to prevent unnecessary re-renders
+ // Only compare src and alt, props object reference may change but content is the same
+ return prevProps.src === nextProps.src &&
+ prevProps.alt === nextProps.alt;
+});
+
+VideoWithErrorHandling.displayName = "VideoWithErrorHandling";
+
+// Image component with error handling - defined outside to prevent re-creation on each render
+interface ImageWithErrorHandlingProps {
+ src: string;
+ alt?: string | null;
+}
+
+const ImageWithErrorHandling: React.FC = React.memo(({ src, alt }) => {
+ const { t } = useTranslation("common");
+ const [hasError, setHasError] = React.useState(false);
+
+ if (hasError) {
+ return (
+
+
+ {t("chatStreamMessage.imageLinkUnavailable", {
+ defaultValue: "This image link is unavailable",
+ })}
+
+ {alt && (
+
{alt}
+ )}
+
+ );
+ }
+
+ return (
+
setHasError(true)}
+ />
+ );
+}, (prevProps, nextProps) => {
+ // Custom comparison function to prevent unnecessary re-renders
+ return prevProps.src === nextProps.src &&
+ prevProps.alt === nextProps.alt;
+});
+
+ImageWithErrorHandling.displayName = "ImageWithErrorHandling";
+
+/**
+ * Render a code block with syntax highlighting, language label, and copy button
+ * This is exported for use in other components that need to render code blocks directly
+ */
+export const CodeBlock: React.FC<{
+ codeContent: string;
+ language?: string;
+}> = ({ codeContent, language = "python" }) => {
+ const { t } = useTranslation("common");
+
+ const customStyle = {
+ ...oneLight,
+ 'pre[class*="language-"]': {
+ ...oneLight['pre[class*="language-"]'],
+ background: "#f8f8f8",
+ borderRadius: "0",
+ padding: "12px 16px",
+ margin: "0",
+ fontSize: "0.875rem",
+ lineHeight: "1.5",
+ whiteSpace: "pre-wrap",
+ wordWrap: "break-word",
+ wordBreak: "break-word",
+ overflowWrap: "break-word",
+ overflow: "auto",
+ width: "100%",
+ boxSizing: "border-box",
+ display: "block",
+ borderTop: "none",
+ },
+ 'code[class*="language-"]': {
+ ...oneLight['code[class*="language-"]'],
+ background: "#f8f8f8",
+ color: "#333333",
+ fontSize: "0.875rem",
+ lineHeight: "1.5",
+ whiteSpace: "pre-wrap",
+ wordWrap: "break-word",
+ wordBreak: "break-word",
+ overflowWrap: "break-word",
+ width: "100%",
+ padding: "0",
+ display: "block",
+ },
+ };
+
+ const cleanedContent = codeContent.replace(/^\n+|\n+$/g, "");
+
+ return (
+
+
+
+ {language}
+
+
+
+
+
+ {cleanedContent}
+
+
+
+ );
+};
+
+export const MarkdownRenderer: React.FC = ({
+ content,
+ className,
+ searchResults = [],
+ showDiagramToggle = true,
+ onCitationHover,
+ enableMultimodal = true,
+ resolveS3Media = false,
+}) => {
+ const { t } = useTranslation("common");
+
+ // Convert LaTeX delimiters to markdown math delimiters
+ const processedContent = convertLatexDelimiters(content);
+
+ const renderCodeFallback = (text: string, key?: React.Key) => (
+
+ {text}
+
+ );
+
+ const buildMediaFallbackText = (src?: string | null, alt?: string | null) => {
+ if (alt) {
+ return `${t("chatStreamMessage.imageTextFallbackTitle", {
+ defaultValue: "Media (text view)",
+ })}: ${alt}${src ? ` - ${src}` : ""}`;
+ }
+ return (
+ src ??
+ t("chatStreamMessage.imageTextFallbackTitle", {
+ defaultValue: "Media (text view)",
+ })
+ );
+ };
+
+ const renderMediaFallback = (src?: string | null, alt?: string | null) =>
+ renderCodeFallback(buildMediaFallbackText(src, alt));
+
+ const renderVideoElement = ({
+ src,
+ alt,
+ props = {},
+ }: {
+ src?: string | null;
+ alt?: string | null;
+ props?: React.VideoHTMLAttributes;
+ }) => {
+ if (!src) {
+ return null;
+ }
+
+ if (!enableMultimodal) {
+ return renderMediaFallback(src, alt);
+ }
+
+ return ;
+ };
+
+ const ImageResolver: React.FC<{ src?: string; alt?: string | null }> = ({
+ src,
+ alt,
+ }) => {
+ const resolvedSrc = useResolvedS3Media(
+ typeof src === "string" ? src : undefined,
+ resolveS3Media
+ );
+
+ if (!enableMultimodal) {
+ return renderMediaFallback(src, alt);
+ }
+
+ if (!resolvedSrc) {
+ return renderMediaFallback(src, alt);
+ }
+
+ if (isVideoUrl(resolvedSrc)) {
+ return renderVideoElement({ src: resolvedSrc, alt });
+ }
+
+ return ;
+ };
+
+ // Modified processText function logic
+ // 支持格式: [[引用]], :mermaid[图表], [btn:可点击选项]
+ const processText = (text: string) => {
+ if (typeof text !== "string") return text;
+
+ // 添加 [btn:选项] 格式的匹配
+ const parts = text.split(/(\[\[[^\]]+\]\]|:mermaid\[[^\]]+\]|\[btn:[^\]]+\])/g);
+ return (
+ <>
+ {parts.map((part, index) => {
+ // 匹配 [btn:选项] 格式 - 可点击选项
+ const optionMatch = part.match(/^\[btn:([^\]]+)\]$/);
+ if (optionMatch) {
+ const optionText = optionMatch[1];
+ return (
+
+ );
+ }
+
+ const match = part.match(/^\[\[([^\]]+)\]\]$/);
+ if (match) {
+ const innerText = match[1];
+
+ const toolSign = innerText.charAt(0);
+ const citeIndex = parseInt(innerText.slice(1));
+ const hasMatch = searchResults?.some(
+ (result) =>
+ result.tool_sign === toolSign && result.cite_index === citeIndex
+ );
+
+ // Only show citation icon when matching search result is found
+ if (hasMatch) {
+ return (
+
+ );
+ } else {
+ // Return empty string if no matching result found (display nothing)
+ return "";
+ }
+ }
+ // Inline Mermaid using :mermaid[graph LR; A-->B] - removed inline support
+ const mmd = part.match(/^:mermaid\[([^\]]+)\]$/);
+ if (mmd) {
+ const code = mmd[1];
+ if (!enableMultimodal) {
+ return renderCodeFallback(code, `mmd-placeholder-${index}`);
+ }
+ return ;
+ }
+ // Handle line breaks in text content
+ if (part.includes('\n')) {
+ return part.split('\n').map((line, lineIndex) => (
+
+ {line}
+ {lineIndex < part.split('\n').length - 1 &&
}
+
+ ));
+ }
+ return part;
+ })}
+ >
+ );
+ };
+
+ // Create wrapper component to handle different types of child elements
+ const TextWrapper = ({ children }: { children: any }) => {
+ if (typeof children === "string") {
+ return processText(children);
+ }
+ if (Array.isArray(children)) {
+ return (
+ <>
+ {children.map((child, index) => {
+ if (typeof child === "string") {
+ return (
+
+ {processText(child)}
+
+ );
+ }
+ return child;
+ })}
+ >
+ );
+ }
+ return children;
+ };
+
+ class MarkdownErrorBoundary extends React.Component<
+ { children: React.ReactNode; rawContent: string },
+ { hasError: boolean }
+ > {
+ constructor(props: { children: React.ReactNode; rawContent: string }) {
+ super(props);
+ this.state = { hasError: false };
+ }
+ static getDerivedStateFromError() {
+ return { hasError: true };
+ }
+ componentDidCatch(error: unknown) {}
+ render() {
+ if (this.state.hasError) {
+ return (
+
+
+ {this.props.rawContent}
+
+
+ );
+ }
+ return this.props.children as React.ReactElement;
+ }
+ }
+
+ return (
+ <>
+
+
+ (
+
+ {children}
+
+ ),
+ h2: ({ children }: any) => (
+
+ {children}
+
+ ),
+ h3: ({ children }: any) => (
+
+ {children}
+
+ ),
+ h4: ({ children }: any) => (
+
+ {children}
+
+ ),
+ h5: ({ children }: any) => (
+
+ {children}
+
+ ),
+ h6: ({ children }: any) => (
+
+ {children}
+
+ ),
+ // Paragraph
+ p: ({ children }: any) => (
+
+ {children}
+
+ ),
+ // Horizontal rule
+ hr: () => (
+
+ ),
+ // Ordered list
+ ol: ({ children }: any) => (
+
+ {children}
+
+ ),
+ // Unordered list
+ ul: ({ children }: any) => (
+
+ ),
+ // List item
+ li: ({ children }: any) => (
+
+ {children}
+
+ ),
+ // Blockquote
+ blockquote: ({ children }: any) => (
+
+ {children}
+
+ ),
+ // Table components
+ td: ({ children }: any) => (
+
+ {children}
+ |
+ ),
+ th: ({ children }: any) => (
+
+ {children}
+ |
+ ),
+ // Emphasis components
+ strong: ({ children }: any) => (
+
+ {children}
+
+ ),
+ em: ({ children }: any) => (
+
+ {children}
+
+ ),
+ // Strikethrough
+ del: ({ children }: any) => (
+
+ {children}
+
+ ),
+ // Link
+ a: ({ href, children, ...props }: any) => {
+ return (
+
+ {children}
+
+ );
+ },
+ pre: ({ children }: any) => <>{children}>,
+ // Code blocks and inline code
+ code({ node, inline, className, children, ...props }: any) {
+ try {
+ const match = /language-(\w+)/.exec(className || "");
+ const raw = Array.isArray(children)
+ ? children.join("")
+ : children ?? "";
+ const codeContent = String(raw).replace(/^\n+|\n+$/g, "");
+ if (match && match[1]) {
+ // Check if it's a Mermaid diagram
+ if (match[1] === "mermaid") {
+ if (!enableMultimodal) {
+ return renderCodeFallback(codeContent);
+ }
+ return ;
+ }
+ if (!inline) {
+ return ;
+ }
+ }
+ } catch (error) {
+ // Handle error silently
+ }
+ return (
+
+ {children}
+
+ );
+ },
+ // Image
+ img: ({ src, alt }: any) => (
+
+ ),
+ // Video
+ video: ({ children, ...props }: any) => {
+ const directSrc = props?.src;
+ const childSource = React.Children.toArray(children)
+ .map((child) =>
+ React.isValidElement(child) ? child.props?.src : undefined
+ )
+ .find(Boolean);
+ const videoSrc = directSrc ?? childSource;
+ const caption =
+ props?.["aria-label"] ??
+ props?.title ??
+ props?.["data-caption"] ??
+ undefined;
+
+ const element = renderVideoElement({
+ src: videoSrc,
+ alt: caption,
+ props,
+ });
+
+ return element ?? renderMediaFallback(undefined, caption);
+ },
+ }}
+ >
+ {processedContent}
+
+
+
+ >
+ );
+};
\ No newline at end of file
diff --git a/pathology-ai/code-changes/medical_extension/__init__.py b/pathology-ai/code-changes/medical_extension/__init__.py
new file mode 100644
index 000000000..661fbafeb
--- /dev/null
+++ b/pathology-ai/code-changes/medical_extension/__init__.py
@@ -0,0 +1,52 @@
+"""
+Nexent 医疗领域扩展模块
+Medical Domain Extension for Nexent Platform
+
+本模块提供医疗领域的智能体模板、诊断推理链框架和专业工具集。
+
+Features:
+- 医疗智能体模板系统
+- Chain-of-Diagnosis (CoD) 诊断推理链框架
+- 置信度评估系统
+- 医疗提示词库
+
+Author: Pathology AI Team
+Version: 1.0.0
+License: MIT
+"""
+
+from .agent_templates import MedicalAgentTemplates, AgentTemplate, MedicalDomain
+from .chain_of_diagnosis import (
+ ChainOfDiagnosis,
+ DiagnosisResult,
+ DiagnosisStep,
+ ConfidenceLevel,
+)
+from .confidence_evaluator import (
+ ConfidenceEvaluator,
+ ConfidenceReport,
+ RiskLevel,
+)
+from .medical_prompts import MedicalPromptLibrary, PromptCategory
+
+__all__ = [
+ # 智能体模板
+ 'MedicalAgentTemplates',
+ 'AgentTemplate',
+ 'MedicalDomain',
+ # 诊断推理链
+ 'ChainOfDiagnosis',
+ 'DiagnosisResult',
+ 'DiagnosisStep',
+ 'ConfidenceLevel',
+ # 置信度评估
+ 'ConfidenceEvaluator',
+ 'ConfidenceReport',
+ 'RiskLevel',
+ # 提示词库
+ 'MedicalPromptLibrary',
+ 'PromptCategory',
+]
+
+__version__ = '1.0.0'
+__author__ = 'Pathology AI Team'
diff --git a/pathology-ai/code-changes/medical_extension/agent_templates.py b/pathology-ai/code-changes/medical_extension/agent_templates.py
new file mode 100644
index 000000000..1ccb3f8e2
--- /dev/null
+++ b/pathology-ai/code-changes/medical_extension/agent_templates.py
@@ -0,0 +1,433 @@
+"""
+医疗智能体模板系统
+Medical Agent Templates for Nexent Platform
+
+提供预置的医疗领域智能体模板,支持一键创建专业医疗智能体。
+
+Templates:
+- 病理诊断助手
+- 影像分析助手
+- 临床决策支持
+- 药物咨询助手
+
+Author: Pathology AI Team
+"""
+
+from dataclasses import dataclass, field
+from typing import List, Dict, Optional, Any
+from enum import Enum
+import json
+
+
+class MedicalDomain(Enum):
+ """医疗领域分类"""
+ PATHOLOGY = "pathology" # 病理学
+ RADIOLOGY = "radiology" # 放射学/影像
+ CLINICAL = "clinical" # 临床医学
+ PHARMACY = "pharmacy" # 药学
+ LABORATORY = "laboratory" # 检验医学
+ GENERAL = "general" # 通用医学
+
+
+@dataclass
+class AgentTemplate:
+ """智能体模板"""
+ template_id: str # 模板ID
+ name: str # 模板名称
+ description: str # 描述
+ domain: MedicalDomain # 医疗领域
+ system_prompt: str # 系统提示词
+ suggested_tools: List[str] # 建议的MCP工具
+ knowledge_bases: List[str] # 建议的知识库
+ model_requirements: Dict[str, Any] = field(default_factory=dict) # 模型要求
+ metadata: Dict[str, Any] = field(default_factory=dict)
+
+ def to_dict(self) -> Dict:
+ """转换为字典"""
+ return {
+ "template_id": self.template_id,
+ "name": self.name,
+ "description": self.description,
+ "domain": self.domain.value,
+ "system_prompt": self.system_prompt,
+ "suggested_tools": self.suggested_tools,
+ "knowledge_bases": self.knowledge_bases,
+ "model_requirements": self.model_requirements,
+ "metadata": self.metadata,
+ }
+
+ def to_json(self) -> str:
+ """转换为JSON"""
+ return json.dumps(self.to_dict(), ensure_ascii=False, indent=2)
+
+
+class MedicalAgentTemplates:
+ """
+ 医疗智能体模板管理器
+
+ 提供预置的医疗领域智能体模板,支持:
+ 1. 获取模板列表
+ 2. 按领域筛选
+ 3. 创建自定义模板
+ 4. 导出/导入模板
+
+ Usage:
+ templates = MedicalAgentTemplates()
+ pathology_template = templates.get_template("pathology_diagnosis")
+ all_templates = templates.list_templates()
+ """
+
+ def __init__(self):
+ """初始化模板管理器"""
+ self._templates: Dict[str, AgentTemplate] = {}
+ self._load_builtin_templates()
+
+ def _load_builtin_templates(self):
+ """加载内置模板"""
+
+ # 1. 病理诊断助手模板
+ self._templates["pathology_diagnosis"] = AgentTemplate(
+ template_id="pathology_diagnosis",
+ name="病理诊断助手",
+ description="专业的病理学诊断辅助智能体,支持组织病理分析、细胞学诊断和分子病理解读",
+ domain=MedicalDomain.PATHOLOGY,
+ system_prompt=self._get_pathology_prompt(),
+ suggested_tools=[
+ "pathology_diagnosis_assistant",
+ "pathology_image_analyzer",
+ "differential_diagnosis_generator",
+ "knowledge_graph_query",
+ ],
+ knowledge_bases=["病理学知识库", "肿瘤病理数据库"],
+ model_requirements={
+ "min_context_length": 4096,
+ "recommended_models": ["glm-4.5", "gpt-4o", "claude-4-sonnet"],
+ "supports_vision": True,
+ },
+ metadata={
+ "version": "1.0.0",
+ "author": "Pathology AI Team",
+ "tags": ["病理", "诊断", "HIV/AIDS", "肿瘤"],
+ }
+ )
+
+ # 2. 影像分析助手模板
+ self._templates["radiology_assistant"] = AgentTemplate(
+ template_id="radiology_assistant",
+ name="医学影像分析助手",
+ description="专业的医学影像分析智能体,支持X光、CT、MRI等影像的智能解读",
+ domain=MedicalDomain.RADIOLOGY,
+ system_prompt=self._get_radiology_prompt(),
+ suggested_tools=[
+ "pathology_image_analyzer",
+ "differential_diagnosis_generator",
+ ],
+ knowledge_bases=["影像学知识库"],
+ model_requirements={
+ "min_context_length": 4096,
+ "recommended_models": ["gpt-4o", "gemini-3-pro"],
+ "supports_vision": True, # 必须支持视觉
+ },
+ metadata={
+ "version": "1.0.0",
+ "tags": ["影像", "CT", "MRI", "X光"],
+ }
+ )
+
+ # 3. 临床决策支持模板
+ self._templates["clinical_decision"] = AgentTemplate(
+ template_id="clinical_decision",
+ name="临床决策支持助手",
+ description="临床决策支持智能体,提供诊断建议、治疗方案和用药指导",
+ domain=MedicalDomain.CLINICAL,
+ system_prompt=self._get_clinical_prompt(),
+ suggested_tools=[
+ "pathology_diagnosis_assistant",
+ "differential_diagnosis_generator",
+ "knowledge_graph_query",
+ ],
+ knowledge_bases=["临床指南库", "药物数据库"],
+ model_requirements={
+ "min_context_length": 8192,
+ "recommended_models": ["glm-4.5", "gpt-4o", "claude-4-opus"],
+ },
+ metadata={
+ "version": "1.0.0",
+ "tags": ["临床", "决策", "治疗", "用药"],
+ }
+ )
+
+ # 4. HIV/AIDS专科助手模板
+ self._templates["hiv_specialist"] = AgentTemplate(
+ template_id="hiv_specialist",
+ name="HIV/AIDS专科助手",
+ description="HIV/AIDS专科诊疗智能体,专注于HIV感染的诊断、治疗和机会性感染管理",
+ domain=MedicalDomain.CLINICAL,
+ system_prompt=self._get_hiv_specialist_prompt(),
+ suggested_tools=[
+ "pathology_diagnosis_assistant",
+ "differential_diagnosis_generator",
+ "knowledge_graph_query",
+ ],
+ knowledge_bases=["HIV/AIDS知识库", "机会性感染数据库"],
+ model_requirements={
+ "min_context_length": 4096,
+ "recommended_models": ["glm-4.5", "gpt-4o"],
+ },
+ metadata={
+ "version": "1.0.0",
+ "tags": ["HIV", "AIDS", "感染", "免疫"],
+ }
+ )
+
+ def _get_pathology_prompt(self) -> str:
+ """获取病理诊断助手提示词"""
+ return """你是一位专业的病理学诊断助手,具备以下能力:
+
+## 专业背景
+- 精通组织病理学、细胞病理学和分子病理学
+- 熟悉WHO肿瘤分类标准
+- 了解HIV/AIDS相关病理改变
+
+## 诊断方法
+请使用诊断推理链(Chain-of-Diagnosis, CoD)方法:
+
+【步骤1 - 症状分析】分析临床表现和病理所见
+【步骤2 - 病史关联】结合既往病史进行分析
+【步骤3 - 鉴别诊断】列出可能的病理诊断
+【步骤4 - 检查建议】建议进一步的病理检查
+【步骤5 - 诊断结论】给出最终诊断和置信度
+
+## 置信度标注
+- HIGH (>85%): 诊断依据充分
+- MEDIUM (60-85%): 需要进一步确认
+- LOW (<60%): 信息不足,仅供参考
+
+## 重要提醒
+- 病理诊断需结合临床信息综合判断
+- AI诊断仅供参考,最终诊断以病理医师报告为准
+- 遇到疑难病例建议多学科会诊(MDT)
+"""
+
+ def _get_radiology_prompt(self) -> str:
+ """获取影像分析助手提示词"""
+ return """你是一位专业的医学影像分析助手,具备以下能力:
+
+## 专业背景
+- 精通X光、CT、MRI、超声等影像解读
+- 熟悉各系统疾病的影像学表现
+- 了解影像学检查的适应症和禁忌症
+
+## 分析方法
+1. 系统性观察:按解剖结构逐一分析
+2. 病变描述:位置、大小、形态、密度/信号、边界、强化特点
+3. 鉴别诊断:列出可能的诊断及依据
+4. 建议:进一步检查或临床处理建议
+
+## 报告格式
+【影像所见】客观描述影像表现
+【诊断意见】给出诊断及置信度
+【建议】进一步检查或随访建议
+
+## 重要提醒
+- 影像诊断需结合临床信息
+- AI分析仅供参考,最终诊断以影像科医师报告为准
+"""
+
+ def _get_clinical_prompt(self) -> str:
+ """获取临床决策支持提示词"""
+ return """你是一位临床决策支持助手,为医生提供诊疗建议。
+
+## 专业能力
+- 疾病诊断与鉴别诊断
+- 治疗方案制定
+- 用药指导与药物相互作用
+- 临床指南解读
+
+## 决策支持流程
+1. 病史采集:了解主诉、现病史、既往史
+2. 体格检查:分析体征
+3. 辅助检查:解读检验和影像结果
+4. 诊断分析:使用CoD方法进行诊断推理
+5. 治疗建议:基于循证医学提供方案
+
+## 置信度评估
+- HIGH: 诊断明确,治疗方案标准化
+- MEDIUM: 诊断基本明确,方案需个体化
+- LOW: 诊断不确定,建议进一步检查
+
+## 重要提醒
+- 所有建议仅供临床参考
+- 最终决策由主治医师做出
+- 注意患者个体差异和禁忌症
+"""
+
+ def _get_hiv_specialist_prompt(self) -> str:
+ """获取HIV/AIDS专科助手提示词"""
+ return """你是一位HIV/AIDS专科诊疗助手,专注于HIV感染的全程管理。
+
+## 专业领域
+- HIV感染的诊断与分期
+- 抗逆转录病毒治疗(ART)
+- 机会性感染的预防与治疗
+- HIV相关肿瘤
+- 免疫重建炎症综合征(IRIS)
+
+## 诊断推理(CoD)
+【步骤1】分析症状和体征
+【步骤2】评估免疫状态(CD4计数、病毒载量)
+【步骤3】鉴别诊断(机会性感染vs其他)
+【步骤4】建议检查(病原学、影像学)
+【步骤5】诊断结论与置信度
+
+## CD4计数与机会性感染风险
+- CD4 < 200: PCP、弓形虫、隐球菌高风险
+- CD4 < 100: CMV、MAC高风险
+- CD4 < 50: 播散性真菌感染高风险
+
+## 常见机会性感染
+- 肺孢子虫肺炎(PCP): 干咳、呼吸困难、发热
+- 隐球菌脑膜炎: 头痛、发热、意识改变
+- 结核病: 咳嗽、盗汗、体重下降
+- CMV视网膜炎: 视力下降、飞蚊症
+
+## 重要提醒
+- HIV诊疗需要专科医师指导
+- 注意药物相互作用
+- 关注患者心理健康
+- AI建议仅供参考
+"""
+
+ def get_template(self, template_id: str) -> Optional[AgentTemplate]:
+ """
+ 获取指定模板
+
+ Args:
+ template_id: 模板ID
+
+ Returns:
+ AgentTemplate or None
+ """
+ return self._templates.get(template_id)
+
+ def list_templates(
+ self,
+ domain: Optional[MedicalDomain] = None
+ ) -> List[AgentTemplate]:
+ """
+ 列出所有模板
+
+ Args:
+ domain: 可选,按领域筛选
+
+ Returns:
+ 模板列表
+ """
+ templates = list(self._templates.values())
+ if domain:
+ templates = [t for t in templates if t.domain == domain]
+ return templates
+
+ def list_template_ids(self) -> List[str]:
+ """获取所有模板ID"""
+ return list(self._templates.keys())
+
+ def add_template(self, template: AgentTemplate) -> bool:
+ """
+ 添加自定义模板
+
+ Args:
+ template: 模板对象
+
+ Returns:
+ 是否添加成功
+ """
+ if template.template_id in self._templates:
+ return False
+ self._templates[template.template_id] = template
+ return True
+
+ def remove_template(self, template_id: str) -> bool:
+ """
+ 移除模板
+
+ Args:
+ template_id: 模板ID
+
+ Returns:
+ 是否移除成功
+ """
+ if template_id in self._templates:
+ del self._templates[template_id]
+ return True
+ return False
+
+ def export_templates(self, filepath: str) -> bool:
+ """
+ 导出模板到文件
+
+ Args:
+ filepath: 文件路径
+
+ Returns:
+ 是否导出成功
+ """
+ try:
+ data = {
+ "version": "1.0.0",
+ "templates": [t.to_dict() for t in self._templates.values()]
+ }
+ with open(filepath, 'w', encoding='utf-8') as f:
+ json.dump(data, f, ensure_ascii=False, indent=2)
+ return True
+ except Exception:
+ return False
+
+ def import_templates(self, filepath: str) -> int:
+ """
+ 从文件导入模板
+
+ Args:
+ filepath: 文件路径
+
+ Returns:
+ 导入的模板数量
+ """
+ try:
+ with open(filepath, 'r', encoding='utf-8') as f:
+ data = json.load(f)
+
+ count = 0
+ for t_data in data.get("templates", []):
+ template = AgentTemplate(
+ template_id=t_data["template_id"],
+ name=t_data["name"],
+ description=t_data["description"],
+ domain=MedicalDomain(t_data["domain"]),
+ system_prompt=t_data["system_prompt"],
+ suggested_tools=t_data["suggested_tools"],
+ knowledge_bases=t_data["knowledge_bases"],
+ model_requirements=t_data.get("model_requirements", {}),
+ metadata=t_data.get("metadata", {}),
+ )
+ if self.add_template(template):
+ count += 1
+ return count
+ except Exception:
+ return 0
+
+ def get_template_summary(self) -> str:
+ """获取模板摘要"""
+ lines = ["=" * 50, "医疗智能体模板库", "=" * 50, ""]
+
+ for domain in MedicalDomain:
+ templates = self.list_templates(domain)
+ if templates:
+ lines.append(f"【{domain.value.upper()}】")
+ for t in templates:
+ lines.append(f" - {t.name} ({t.template_id})")
+ lines.append(f" {t.description[:50]}...")
+ lines.append("")
+
+ lines.append(f"共 {len(self._templates)} 个模板")
+ return "\n".join(lines)
diff --git a/pathology-ai/code-changes/medical_extension/api.py b/pathology-ai/code-changes/medical_extension/api.py
new file mode 100644
index 000000000..043e9c593
--- /dev/null
+++ b/pathology-ai/code-changes/medical_extension/api.py
@@ -0,0 +1,332 @@
+"""
+医疗模块 API 接口
+Medical Module API for Nexent Platform
+
+提供RESTful API接口,支持:
+1. 智能体模板管理
+2. 诊断推理链调用
+3. 置信度评估
+4. 提示词管理
+
+Author: Pathology AI Team
+"""
+
+from fastapi import APIRouter, HTTPException, Query
+from pydantic import BaseModel, Field
+from typing import List, Dict, Optional, Any
+
+from .agent_templates import MedicalAgentTemplates, MedicalDomain
+from .chain_of_diagnosis import ChainOfDiagnosis, DiagnosisResult
+from .confidence_evaluator import ConfidenceEvaluator
+from .medical_prompts import MedicalPromptLibrary, PromptCategory
+
+
+# 创建路由
+router = APIRouter(prefix="/medical", tags=["Medical"])
+
+# 初始化组件
+templates_manager = MedicalAgentTemplates()
+cod_engine = ChainOfDiagnosis()
+confidence_evaluator = ConfidenceEvaluator()
+prompt_library = MedicalPromptLibrary()
+
+
+# ==================== 请求/响应模型 ====================
+
+class DiagnosisRequest(BaseModel):
+ """诊断请求"""
+ symptoms: str = Field(..., description="症状描述")
+ lab_results: Optional[str] = Field(None, description="实验室检查结果")
+ medical_history: Optional[str] = Field(None, description="既往病史")
+ imaging_findings: Optional[str] = Field(None, description="影像学发现")
+
+
+class DiagnosisResponse(BaseModel):
+ """诊断响应"""
+ success: bool
+ data: Dict[str, Any]
+ formatted_report: str
+
+
+class ConfidenceRequest(BaseModel):
+ """置信度评估请求"""
+ diagnosis: str = Field(..., description="诊断结果")
+ symptoms: Optional[List[str]] = Field(None, description="症状列表")
+ lab_results: Optional[Dict] = Field(None, description="实验室结果")
+ evidence: Optional[List[str]] = Field(None, description="支持证据")
+
+
+class ConfidenceResponse(BaseModel):
+ """置信度评估响应"""
+ success: bool
+ data: Dict[str, Any]
+ formatted_report: str
+
+
+class TemplateResponse(BaseModel):
+ """模板响应"""
+ success: bool
+ data: Dict[str, Any]
+
+
+class PromptsResponse(BaseModel):
+ """提示词响应"""
+ success: bool
+ data: List[Dict[str, Any]]
+
+
+# ==================== 智能体模板 API ====================
+
+@router.get("/templates", response_model=TemplateResponse)
+async def list_templates(
+ domain: Optional[str] = Query(None, description="按领域筛选")
+):
+ """
+ 获取医疗智能体模板列表
+
+ Args:
+ domain: 可选,按领域筛选 (pathology/radiology/clinical/pharmacy/laboratory/general)
+
+ Returns:
+ 模板列表
+ """
+ try:
+ domain_enum = MedicalDomain(domain) if domain else None
+ templates = templates_manager.list_templates(domain_enum)
+ return TemplateResponse(
+ success=True,
+ data={
+ "templates": [t.to_dict() for t in templates],
+ "count": len(templates),
+ }
+ )
+ except ValueError:
+ raise HTTPException(status_code=400, detail=f"Invalid domain: {domain}")
+
+
+@router.get("/templates/{template_id}", response_model=TemplateResponse)
+async def get_template(template_id: str):
+ """
+ 获取指定模板详情
+
+ Args:
+ template_id: 模板ID
+
+ Returns:
+ 模板详情
+ """
+ template = templates_manager.get_template(template_id)
+ if not template:
+ raise HTTPException(status_code=404, detail=f"Template not found: {template_id}")
+
+ return TemplateResponse(
+ success=True,
+ data=template.to_dict()
+ )
+
+
+@router.get("/templates/ids/list")
+async def list_template_ids():
+ """获取所有模板ID列表"""
+ return {
+ "success": True,
+ "template_ids": templates_manager.list_template_ids()
+ }
+
+
+# ==================== 诊断推理链 API ====================
+
+@router.post("/diagnosis/analyze", response_model=DiagnosisResponse)
+async def analyze_diagnosis(request: DiagnosisRequest):
+ """
+ 使用诊断推理链(CoD)进行诊断分析
+
+ Args:
+ request: 诊断请求,包含症状、检查结果等
+
+ Returns:
+ 诊断结果,包含推理链和置信度
+ """
+ try:
+ result = cod_engine.analyze(
+ symptoms=request.symptoms,
+ lab_results=request.lab_results,
+ medical_history=request.medical_history,
+ imaging_findings=request.imaging_findings,
+ )
+
+ return DiagnosisResponse(
+ success=True,
+ data=result.to_dict(),
+ formatted_report=result.to_formatted_string()
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.get("/diagnosis/cod-prompt")
+async def get_cod_prompt():
+ """
+ 获取CoD诊断推理链提示词模板
+
+ Returns:
+ CoD提示词,可用于配置LLM
+ """
+ return {
+ "success": True,
+ "prompt": cod_engine.generate_cod_prompt(),
+ "description": "诊断推理链(Chain-of-Diagnosis)提示词模板"
+ }
+
+
+# ==================== 置信度评估 API ====================
+
+@router.post("/confidence/evaluate", response_model=ConfidenceResponse)
+async def evaluate_confidence(request: ConfidenceRequest):
+ """
+ 评估诊断置信度
+
+ Args:
+ request: 评估请求,包含诊断和相关信息
+
+ Returns:
+ 置信度评估报告
+ """
+ try:
+ report = confidence_evaluator.evaluate(
+ diagnosis=request.diagnosis,
+ symptoms=request.symptoms,
+ lab_results=request.lab_results,
+ evidence=request.evidence,
+ )
+
+ return ConfidenceResponse(
+ success=True,
+ data=report.to_dict(),
+ formatted_report=confidence_evaluator.format_report(report)
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+# ==================== 提示词库 API ====================
+
+@router.get("/prompts", response_model=PromptsResponse)
+async def list_prompts(
+ category: Optional[str] = Query(None, description="按分类筛选")
+):
+ """
+ 获取医疗提示词列表
+
+ Args:
+ category: 可选,按分类筛选 (diagnosis/treatment/safety/specialty/general)
+
+ Returns:
+ 提示词列表
+ """
+ try:
+ category_enum = PromptCategory(category) if category else None
+ prompts = prompt_library.list_prompts(category_enum)
+
+ # 简化输出,不包含完整prompt文本
+ simplified = [
+ {
+ "id": p["id"],
+ "name": p["name"],
+ "category": p["category"].value,
+ "description": p["description"],
+ "tags": p["tags"],
+ }
+ for p in prompts
+ ]
+
+ return PromptsResponse(
+ success=True,
+ data=simplified
+ )
+ except ValueError:
+ raise HTTPException(status_code=400, detail=f"Invalid category: {category}")
+
+
+@router.get("/prompts/{prompt_id}")
+async def get_prompt(prompt_id: str):
+ """
+ 获取指定提示词详情
+
+ Args:
+ prompt_id: 提示词ID
+
+ Returns:
+ 提示词详情,包含完整文本
+ """
+ prompt = prompt_library.get_prompt(prompt_id)
+ if not prompt:
+ raise HTTPException(status_code=404, detail=f"Prompt not found: {prompt_id}")
+
+ return {
+ "success": True,
+ "data": {
+ "id": prompt["id"],
+ "name": prompt["name"],
+ "category": prompt["category"].value,
+ "description": prompt["description"],
+ "prompt": prompt["prompt"],
+ "variables": prompt["variables"],
+ "tags": prompt["tags"],
+ }
+ }
+
+
+@router.get("/prompts/recommended/full")
+async def get_recommended_prompt():
+ """
+ 获取推荐的完整医疗助手提示词
+
+ Returns:
+ 推荐提示词,包含CoD、不确定性感知和安全提醒
+ """
+ return {
+ "success": True,
+ "prompt": prompt_library.get_recommended_prompt(),
+ "description": "推荐的完整医疗助手提示词,包含诊断推理链、置信度标注和安全提醒"
+ }
+
+
+@router.post("/prompts/combine")
+async def combine_prompts(prompt_ids: List[str]):
+ """
+ 组合多个提示词
+
+ Args:
+ prompt_ids: 要组合的提示词ID列表
+
+ Returns:
+ 组合后的提示词文本
+ """
+ combined = prompt_library.combine_prompts(prompt_ids)
+ if not combined:
+ raise HTTPException(status_code=400, detail="No valid prompts found")
+
+ return {
+ "success": True,
+ "combined_prompt": combined,
+ "source_prompts": prompt_ids
+ }
+
+
+# ==================== 健康检查 ====================
+
+@router.get("/health")
+async def health_check():
+ """医疗模块健康检查"""
+ return {
+ "status": "healthy",
+ "module": "medical",
+ "version": "1.0.0",
+ "components": {
+ "templates": len(templates_manager.list_template_ids()),
+ "prompts": len(prompt_library.list_prompt_ids()),
+ "cod_engine": "ready",
+ "confidence_evaluator": "ready",
+ }
+ }
diff --git a/pathology-ai/code-changes/medical_extension/chain_of_diagnosis.py b/pathology-ai/code-changes/medical_extension/chain_of_diagnosis.py
new file mode 100644
index 000000000..f1fda65c8
--- /dev/null
+++ b/pathology-ai/code-changes/medical_extension/chain_of_diagnosis.py
@@ -0,0 +1,542 @@
+"""
+Chain-of-Diagnosis (CoD) 诊断推理链框架
+Medical Diagnosis Reasoning Chain Framework
+
+创新点:
+1. 结构化诊断推理流程
+2. 多步骤逻辑推导
+3. 置信度量化评估
+4. 可解释性诊断输出
+
+Author: Pathology AI Team
+"""
+
+from dataclasses import dataclass, field
+from typing import List, Dict, Optional, Any
+from enum import Enum
+import json
+import re
+
+
+class ConfidenceLevel(Enum):
+ """置信度等级"""
+ HIGH = "HIGH" # >85% 高置信度
+ MEDIUM = "MEDIUM" # 60-85% 中等置信度
+ LOW = "LOW" # <60% 低置信度
+ UNCERTAIN = "UNCERTAIN" # 不确定
+
+
+@dataclass
+class DiagnosisStep:
+ """诊断推理步骤"""
+ step_name: str # 步骤名称
+ content: str # 步骤内容
+ evidence: List[str] = field(default_factory=list) # 支持证据
+ confidence: float = 0.0 # 步骤置信度
+
+
+@dataclass
+class DiagnosisResult:
+ """诊断结果"""
+ primary_diagnosis: str # 主要诊断
+ differential_diagnoses: List[str] # 鉴别诊断列表
+ confidence_level: ConfidenceLevel # 置信度等级
+ confidence_score: float # 置信度分数 (0-1)
+ reasoning_chain: List[DiagnosisStep] # 推理链
+ recommendations: List[str] # 建议
+ warnings: List[str] = field(default_factory=list) # 警告信息
+ metadata: Dict[str, Any] = field(default_factory=dict) # 元数据
+
+ def to_dict(self) -> Dict:
+ """转换为字典"""
+ return {
+ "primary_diagnosis": self.primary_diagnosis,
+ "differential_diagnoses": self.differential_diagnoses,
+ "confidence_level": self.confidence_level.value,
+ "confidence_score": self.confidence_score,
+ "reasoning_chain": [
+ {
+ "step": s.step_name,
+ "content": s.content,
+ "evidence": s.evidence,
+ "confidence": s.confidence
+ } for s in self.reasoning_chain
+ ],
+ "recommendations": self.recommendations,
+ "warnings": self.warnings,
+ "metadata": self.metadata
+ }
+
+ def to_formatted_string(self) -> str:
+ """生成格式化的诊断报告"""
+ lines = []
+ lines.append("=" * 50)
+ lines.append("【诊断推理报告】")
+ lines.append("=" * 50)
+
+ # 推理链
+ lines.append("\n📋 诊断推理链:")
+ for i, step in enumerate(self.reasoning_chain, 1):
+ lines.append(f"\n[步骤{i}] {step.step_name}")
+ lines.append(f" {step.content}")
+ if step.evidence:
+ lines.append(f" 证据: {', '.join(step.evidence)}")
+
+ # 诊断结论
+ lines.append(f"\n🎯 主要诊断: {self.primary_diagnosis}")
+
+ if self.differential_diagnoses:
+ lines.append(f"\n🔍 鉴别诊断:")
+ for dd in self.differential_diagnoses:
+ lines.append(f" - {dd}")
+
+ # 置信度
+ confidence_emoji = {"HIGH": "🟢", "MEDIUM": "🟡", "LOW": "🔴", "UNCERTAIN": "⚪"}
+ lines.append(f"\n📊 置信度: {confidence_emoji.get(self.confidence_level.value, '⚪')} "
+ f"{self.confidence_level.value} ({self.confidence_score*100:.1f}%)")
+
+ # 建议
+ if self.recommendations:
+ lines.append(f"\n💡 建议:")
+ for rec in self.recommendations:
+ lines.append(f" • {rec}")
+
+ # 警告
+ if self.warnings:
+ lines.append(f"\n⚠️ 注意:")
+ for warn in self.warnings:
+ lines.append(f" • {warn}")
+
+ lines.append("\n" + "=" * 50)
+ return "\n".join(lines)
+
+
+class ChainOfDiagnosis:
+ """
+ 诊断推理链 (Chain-of-Diagnosis) 框架
+
+ 核心创新:
+ 1. 症状分析 → 2. 病史关联 → 3. 鉴别诊断 → 4. 检查建议 → 5. 诊断结论
+
+ Usage:
+ cod = ChainOfDiagnosis()
+ result = cod.analyze(symptoms, lab_results, history)
+ print(result.to_formatted_string())
+ """
+
+ # CoD 推理步骤定义
+ COD_STEPS = [
+ "症状分析", # Step 1: 分析主诉和症状
+ "病史关联", # Step 2: 关联既往病史
+ "鉴别诊断", # Step 3: 列出可能的诊断
+ "检查建议", # Step 4: 建议进一步检查
+ "诊断结论", # Step 5: 给出最终诊断
+ ]
+
+ # 置信度阈值
+ CONFIDENCE_THRESHOLDS = {
+ "high": 0.85,
+ "medium": 0.60,
+ }
+
+ def __init__(self, knowledge_base: Optional[Dict] = None):
+ """
+ 初始化诊断推理链
+
+ Args:
+ knowledge_base: 可选的知识库字典
+ """
+ self.knowledge_base = knowledge_base or {}
+ self._load_default_knowledge()
+
+ def _load_default_knowledge(self):
+ """加载默认医学知识库"""
+ # HIV/AIDS 相关知识
+ self.knowledge_base.update({
+ "hiv_opportunistic_infections": [
+ "肺孢子虫肺炎 (PCP)",
+ "巨细胞病毒感染 (CMV)",
+ "隐球菌脑膜炎",
+ "卡波西肉瘤",
+ "结核病",
+ "弓形虫脑病",
+ ],
+ "cd4_thresholds": {
+ "severe_immunodeficiency": 200,
+ "moderate_immunodeficiency": 350,
+ "mild_immunodeficiency": 500,
+ },
+ "pcp_symptoms": ["干咳", "呼吸困难", "发热", "低氧血症"],
+ "pcp_treatment": ["复方磺胺甲噁唑 (TMP-SMX)", "喷他脒", "阿托伐醌"],
+ })
+
+ def analyze(
+ self,
+ symptoms: str,
+ lab_results: Optional[str] = None,
+ medical_history: Optional[str] = None,
+ imaging_findings: Optional[str] = None,
+ ) -> DiagnosisResult:
+ """
+ 执行诊断推理链分析
+
+ Args:
+ symptoms: 症状描述
+ lab_results: 实验室检查结果
+ medical_history: 既往病史
+ imaging_findings: 影像学发现
+
+ Returns:
+ DiagnosisResult: 诊断结果对象
+ """
+ reasoning_chain = []
+ evidence_collected = []
+
+ # Step 1: 症状分析
+ step1 = self._analyze_symptoms(symptoms)
+ reasoning_chain.append(step1)
+ evidence_collected.extend(step1.evidence)
+
+ # Step 2: 病史关联
+ step2 = self._correlate_history(medical_history, symptoms)
+ reasoning_chain.append(step2)
+ evidence_collected.extend(step2.evidence)
+
+ # Step 3: 鉴别诊断
+ step3 = self._differential_diagnosis(
+ symptoms, lab_results, medical_history, imaging_findings
+ )
+ reasoning_chain.append(step3)
+
+ # Step 4: 检查建议
+ step4 = self._suggest_examinations(step3.content, lab_results)
+ reasoning_chain.append(step4)
+
+ # Step 5: 诊断结论
+ step5, primary_diagnosis, differentials = self._conclude_diagnosis(
+ reasoning_chain, lab_results
+ )
+ reasoning_chain.append(step5)
+
+ # 计算置信度
+ confidence_score = self._calculate_confidence(
+ reasoning_chain, evidence_collected, lab_results
+ )
+ confidence_level = self._get_confidence_level(confidence_score)
+
+ # 生成建议
+ recommendations = self._generate_recommendations(
+ primary_diagnosis, confidence_level, lab_results
+ )
+
+ # 生成警告
+ warnings = self._generate_warnings(confidence_level, primary_diagnosis)
+
+ return DiagnosisResult(
+ primary_diagnosis=primary_diagnosis,
+ differential_diagnoses=differentials,
+ confidence_level=confidence_level,
+ confidence_score=confidence_score,
+ reasoning_chain=reasoning_chain,
+ recommendations=recommendations,
+ warnings=warnings,
+ metadata={
+ "input_symptoms": symptoms,
+ "has_lab_results": lab_results is not None,
+ "has_history": medical_history is not None,
+ }
+ )
+
+ def _analyze_symptoms(self, symptoms: str) -> DiagnosisStep:
+ """分析症状"""
+ evidence = []
+ analysis = []
+
+ # 检测关键症状
+ symptom_patterns = {
+ "呼吸系统": ["咳嗽", "干咳", "呼吸困难", "气短", "胸痛"],
+ "发热相关": ["发热", "发烧", "高热", "低热"],
+ "神经系统": ["头痛", "意识改变", "抽搐", "视力改变"],
+ "消化系统": ["腹泻", "恶心", "呕吐", "腹痛"],
+ "皮肤表现": ["皮疹", "紫色斑块", "溃疡"],
+ }
+
+ for system, patterns in symptom_patterns.items():
+ found = [p for p in patterns if p in symptoms]
+ if found:
+ evidence.extend(found)
+ analysis.append(f"{system}症状: {', '.join(found)}")
+
+ content = "; ".join(analysis) if analysis else "症状信息不足,需要进一步询问"
+
+ return DiagnosisStep(
+ step_name="症状分析",
+ content=content,
+ evidence=evidence,
+ confidence=0.8 if evidence else 0.3
+ )
+
+ def _correlate_history(
+ self, history: Optional[str], symptoms: str
+ ) -> DiagnosisStep:
+ """关联病史"""
+ evidence = []
+ content = ""
+
+ if history:
+ # 检测HIV/AIDS相关
+ if any(kw in history.lower() for kw in ["hiv", "aids", "艾滋", "免疫缺陷"]):
+ evidence.append("HIV/AIDS病史")
+ content = "患者有HIV/AIDS病史,需考虑机会性感染"
+
+ # 检测免疫抑制
+ if any(kw in history for kw in ["免疫抑制", "化疗", "器官移植", "激素"]):
+ evidence.append("免疫抑制状态")
+ content += ";存在免疫抑制因素"
+
+ if not content:
+ content = "无特殊病史或病史信息不完整"
+
+ return DiagnosisStep(
+ step_name="病史关联",
+ content=content,
+ evidence=evidence,
+ confidence=0.7 if evidence else 0.4
+ )
+
+ def _differential_diagnosis(
+ self,
+ symptoms: str,
+ lab_results: Optional[str],
+ history: Optional[str],
+ imaging: Optional[str],
+ ) -> DiagnosisStep:
+ """生成鉴别诊断"""
+ differentials = []
+ evidence = []
+
+ # HIV相关机会性感染判断
+ is_hiv_related = history and any(
+ kw in history.lower() for kw in ["hiv", "aids", "艾滋"]
+ )
+
+ # 检测CD4计数
+ cd4_count = None
+ if lab_results:
+ cd4_match = re.search(r'cd4[^\d]*(\d+)', lab_results.lower())
+ if cd4_match:
+ cd4_count = int(cd4_match.group(1))
+ evidence.append(f"CD4计数: {cd4_count}")
+
+ # 基于症状和病史生成鉴别诊断
+ if is_hiv_related:
+ if cd4_count and cd4_count < 200:
+ # 严重免疫缺陷
+ if any(s in symptoms for s in ["干咳", "呼吸困难", "发热"]):
+ differentials.append("肺孢子虫肺炎 (PCP) - 高度怀疑")
+ differentials.append("细菌性肺炎")
+ differentials.append("肺结核")
+ elif any(s in symptoms for s in ["头痛", "意识"]):
+ differentials.append("隐球菌脑膜炎")
+ differentials.append("弓形虫脑病")
+ else:
+ differentials.append("需要更多信息进行鉴别")
+ else:
+ # 非HIV患者
+ if any(s in symptoms for s in ["咳嗽", "发热"]):
+ differentials.append("社区获得性肺炎")
+ differentials.append("病毒性上呼吸道感染")
+ differentials.append("支气管炎")
+
+ content = "鉴别诊断: " + ", ".join(differentials) if differentials else "需要更多信息"
+
+ return DiagnosisStep(
+ step_name="鉴别诊断",
+ content=content,
+ evidence=evidence,
+ confidence=0.75 if differentials else 0.3
+ )
+
+ def _suggest_examinations(
+ self, differential: str, existing_labs: Optional[str]
+ ) -> DiagnosisStep:
+ """建议进一步检查"""
+ suggestions = []
+
+ if "PCP" in differential or "肺孢子虫" in differential:
+ suggestions.extend([
+ "诱导痰检查(银染色/免疫荧光)",
+ "血气分析",
+ "乳酸脱氢酶 (LDH)",
+ "胸部CT",
+ "支气管肺泡灌洗 (BAL)",
+ ])
+ elif "脑膜炎" in differential:
+ suggestions.extend([
+ "腰椎穿刺",
+ "脑脊液墨汁染色",
+ "隐球菌抗原检测",
+ "头颅MRI",
+ ])
+ else:
+ suggestions.extend([
+ "血常规",
+ "C反应蛋白",
+ "胸部X线",
+ ])
+
+ # 排除已有检查
+ if existing_labs:
+ suggestions = [s for s in suggestions if s.split("(")[0] not in existing_labs]
+
+ content = "建议检查: " + ", ".join(suggestions[:5]) # 最多5项
+
+ return DiagnosisStep(
+ step_name="检查建议",
+ content=content,
+ evidence=[],
+ confidence=0.8
+ )
+
+ def _conclude_diagnosis(
+ self,
+ reasoning_chain: List[DiagnosisStep],
+ lab_results: Optional[str],
+ ) -> tuple:
+ """得出诊断结论"""
+ # 从鉴别诊断步骤提取
+ differential_step = reasoning_chain[2] # Step 3
+
+ # 解析鉴别诊断
+ differentials = []
+ primary = "诊断待定"
+
+ if "高度怀疑" in differential_step.content:
+ # 提取高度怀疑的诊断作为主诊断
+ match = re.search(r'([^,]+)\s*-\s*高度怀疑', differential_step.content)
+ if match:
+ primary = match.group(1).strip()
+
+ # 提取所有鉴别诊断
+ diff_match = re.search(r'鉴别诊断:\s*(.+)', differential_step.content)
+ if diff_match:
+ diff_list = diff_match.group(1).split(", ")
+ differentials = [d.split(" - ")[0].strip() for d in diff_list if d != primary]
+
+ content = f"综合分析,最可能的诊断为: {primary}"
+
+ step = DiagnosisStep(
+ step_name="诊断结论",
+ content=content,
+ evidence=[s.step_name for s in reasoning_chain if s.confidence > 0.6],
+ confidence=0.85 if "高度怀疑" in differential_step.content else 0.5
+ )
+
+ return step, primary, differentials
+
+ def _calculate_confidence(
+ self,
+ reasoning_chain: List[DiagnosisStep],
+ evidence: List[str],
+ lab_results: Optional[str],
+ ) -> float:
+ """计算总体置信度"""
+ # 基础置信度:各步骤置信度加权平均
+ weights = [0.15, 0.15, 0.25, 0.15, 0.30] # 诊断结论权重最高
+ base_confidence = sum(
+ step.confidence * weight
+ for step, weight in zip(reasoning_chain, weights)
+ )
+
+ # 证据加成
+ evidence_bonus = min(len(evidence) * 0.02, 0.1)
+
+ # 实验室结果加成
+ lab_bonus = 0.05 if lab_results else 0
+
+ total = base_confidence + evidence_bonus + lab_bonus
+ return min(max(total, 0.0), 1.0) # 限制在0-1之间
+
+ def _get_confidence_level(self, score: float) -> ConfidenceLevel:
+ """根据分数获取置信度等级"""
+ if score >= self.CONFIDENCE_THRESHOLDS["high"]:
+ return ConfidenceLevel.HIGH
+ elif score >= self.CONFIDENCE_THRESHOLDS["medium"]:
+ return ConfidenceLevel.MEDIUM
+ elif score > 0.3:
+ return ConfidenceLevel.LOW
+ else:
+ return ConfidenceLevel.UNCERTAIN
+
+ def _generate_recommendations(
+ self,
+ diagnosis: str,
+ confidence: ConfidenceLevel,
+ lab_results: Optional[str],
+ ) -> List[str]:
+ """生成治疗建议"""
+ recommendations = []
+
+ if "PCP" in diagnosis or "肺孢子虫" in diagnosis:
+ recommendations.extend([
+ "首选治疗: 复方磺胺甲噁唑 (TMP-SMX)",
+ "替代方案: 喷他脒或阿托伐醌",
+ "严重病例考虑糖皮质激素辅助治疗",
+ "监测血氧饱和度",
+ ])
+
+ if confidence in [ConfidenceLevel.LOW, ConfidenceLevel.UNCERTAIN]:
+ recommendations.append("建议进一步检查以明确诊断")
+ recommendations.append("必要时请专科会诊")
+
+ if not recommendations:
+ recommendations.append("根据具体情况制定治疗方案")
+
+ return recommendations
+
+ def _generate_warnings(
+ self,
+ confidence: ConfidenceLevel,
+ diagnosis: str,
+ ) -> List[str]:
+ """生成警告信息"""
+ warnings = []
+
+ if confidence == ConfidenceLevel.LOW:
+ warnings.append("置信度较低,诊断结果仅供参考")
+ elif confidence == ConfidenceLevel.UNCERTAIN:
+ warnings.append("信息不足,无法做出可靠诊断")
+
+ warnings.append("本诊断由AI辅助生成,最终诊断请以临床医生判断为准")
+
+ return warnings
+
+ def generate_cod_prompt(self) -> str:
+ """
+ 生成CoD提示词模板
+ 可用于配置LLM的系统提示词
+ """
+ return """你是一位专业的医学诊断助手,请使用诊断推理链(Chain-of-Diagnosis, CoD)方法进行分析。
+
+请按以下步骤进行诊断推理:
+
+【步骤1 - 症状分析】
+分析患者的主诉和症状,识别关键临床表现。
+
+【步骤2 - 病史关联】
+结合既往病史,分析与当前症状的关联性。
+
+【步骤3 - 鉴别诊断】
+列出可能的诊断,并说明支持和反对的证据。
+
+【步骤4 - 检查建议】
+建议进一步的检查以明确诊断。
+
+【步骤5 - 诊断结论】
+给出最可能的诊断,并标注置信度:
+- HIGH (高置信度 >85%): 证据充分,诊断明确
+- MEDIUM (中等置信度 60-85%): 有一定依据,但需进一步确认
+- LOW (低置信度 <60%): 信息不足,仅供参考
+
+请始终提醒:AI诊断仅供参考,最终诊断请以临床医生判断为准。
+"""
diff --git a/pathology-ai/code-changes/medical_extension/confidence_evaluator.py b/pathology-ai/code-changes/medical_extension/confidence_evaluator.py
new file mode 100644
index 000000000..5a1f57d6b
--- /dev/null
+++ b/pathology-ai/code-changes/medical_extension/confidence_evaluator.py
@@ -0,0 +1,434 @@
+"""
+置信度评估系统
+Confidence Evaluation System for Medical AI
+
+提供医疗AI回答的置信度评估功能,支持:
+1. 基于证据的置信度计算
+2. 不确定性量化
+3. 风险等级评估
+
+Author: Pathology AI Team
+"""
+
+from dataclasses import dataclass
+from typing import List, Dict, Optional, Tuple
+from enum import Enum
+import re
+
+
+class RiskLevel(Enum):
+ """风险等级"""
+ CRITICAL = "critical" # 危急
+ HIGH = "high" # 高风险
+ MEDIUM = "medium" # 中等风险
+ LOW = "low" # 低风险
+
+
+@dataclass
+class ConfidenceReport:
+ """置信度评估报告"""
+ overall_score: float # 总体置信度 (0-1)
+ confidence_level: str # 置信度等级 (HIGH/MEDIUM/LOW)
+ evidence_score: float # 证据充分度
+ consistency_score: float # 一致性得分
+ completeness_score: float # 完整性得分
+ risk_level: RiskLevel # 风险等级
+ factors: Dict[str, float] # 各因素得分
+ recommendations: List[str] # 建议
+ warnings: List[str] # 警告
+
+ def to_dict(self) -> Dict:
+ return {
+ "overall_score": self.overall_score,
+ "confidence_level": self.confidence_level,
+ "evidence_score": self.evidence_score,
+ "consistency_score": self.consistency_score,
+ "completeness_score": self.completeness_score,
+ "risk_level": self.risk_level.value,
+ "factors": self.factors,
+ "recommendations": self.recommendations,
+ "warnings": self.warnings,
+ }
+
+
+class ConfidenceEvaluator:
+ """
+ 置信度评估器
+
+ 评估维度:
+ 1. 证据充分度:支持诊断的证据数量和质量
+ 2. 一致性:症状、检查结果与诊断的一致性
+ 3. 完整性:信息的完整程度
+ 4. 确定性:诊断的确定程度
+
+ Usage:
+ evaluator = ConfidenceEvaluator()
+ report = evaluator.evaluate(
+ diagnosis="肺孢子虫肺炎",
+ symptoms=["干咳", "呼吸困难", "发热"],
+ lab_results={"CD4": 150, "LDH": "升高"},
+ evidence=["HIV阳性", "CD4<200"]
+ )
+ print(f"置信度: {report.confidence_level} ({report.overall_score:.2f})")
+ """
+
+ # 置信度阈值
+ THRESHOLDS = {
+ "high": 0.85,
+ "medium": 0.60,
+ "low": 0.30,
+ }
+
+ # 关键证据权重
+ EVIDENCE_WEIGHTS = {
+ "病理确诊": 1.0,
+ "实验室确诊": 0.9,
+ "影像学典型表现": 0.8,
+ "临床症状典型": 0.7,
+ "病史支持": 0.6,
+ "经验性诊断": 0.4,
+ }
+
+ # 高风险诊断关键词
+ HIGH_RISK_KEYWORDS = [
+ "恶性", "癌", "肿瘤", "转移", "急性", "重症",
+ "休克", "衰竭", "危重", "紧急",
+ ]
+
+ def __init__(self):
+ """初始化评估器"""
+ self._custom_rules = []
+
+ def evaluate(
+ self,
+ diagnosis: str,
+ symptoms: Optional[List[str]] = None,
+ lab_results: Optional[Dict] = None,
+ imaging_findings: Optional[List[str]] = None,
+ evidence: Optional[List[str]] = None,
+ medical_history: Optional[str] = None,
+ ) -> ConfidenceReport:
+ """
+ 评估诊断置信度
+
+ Args:
+ diagnosis: 诊断结果
+ symptoms: 症状列表
+ lab_results: 实验室结果
+ imaging_findings: 影像学发现
+ evidence: 支持证据
+ medical_history: 病史
+
+ Returns:
+ ConfidenceReport: 置信度评估报告
+ """
+ factors = {}
+
+ # 1. 评估证据充分度
+ evidence_score = self._evaluate_evidence(evidence or [])
+ factors["evidence"] = evidence_score
+
+ # 2. 评估一致性
+ consistency_score = self._evaluate_consistency(
+ diagnosis, symptoms or [], lab_results or {}
+ )
+ factors["consistency"] = consistency_score
+
+ # 3. 评估完整性
+ completeness_score = self._evaluate_completeness(
+ symptoms, lab_results, imaging_findings, medical_history
+ )
+ factors["completeness"] = completeness_score
+
+ # 4. 评估确定性
+ certainty_score = self._evaluate_certainty(diagnosis)
+ factors["certainty"] = certainty_score
+
+ # 计算总体置信度
+ overall_score = self._calculate_overall_score(factors)
+
+ # 确定置信度等级
+ confidence_level = self._get_confidence_level(overall_score)
+
+ # 评估风险等级
+ risk_level = self._evaluate_risk(diagnosis, overall_score)
+
+ # 生成建议
+ recommendations = self._generate_recommendations(
+ confidence_level, factors, diagnosis
+ )
+
+ # 生成警告
+ warnings = self._generate_warnings(
+ confidence_level, risk_level, diagnosis
+ )
+
+ return ConfidenceReport(
+ overall_score=overall_score,
+ confidence_level=confidence_level,
+ evidence_score=evidence_score,
+ consistency_score=consistency_score,
+ completeness_score=completeness_score,
+ risk_level=risk_level,
+ factors=factors,
+ recommendations=recommendations,
+ warnings=warnings,
+ )
+
+ def _evaluate_evidence(self, evidence: List[str]) -> float:
+ """评估证据充分度"""
+ if not evidence:
+ return 0.3
+
+ score = 0.0
+ max_weight = 0.0
+
+ for e in evidence:
+ for key, weight in self.EVIDENCE_WEIGHTS.items():
+ if key in e or any(k in e for k in key.split()):
+ score += weight
+ max_weight = max(max_weight, weight)
+ break
+ else:
+ # 未匹配到预定义证据类型,给基础分
+ score += 0.3
+
+ # 归一化
+ normalized = min(score / max(len(evidence), 1) * 0.5 + max_weight * 0.5, 1.0)
+ return normalized
+
+ def _evaluate_consistency(
+ self,
+ diagnosis: str,
+ symptoms: List[str],
+ lab_results: Dict,
+ ) -> float:
+ """评估一致性"""
+ score = 0.5 # 基础分
+
+ # 定义诊断-症状关联
+ diagnosis_symptom_map = {
+ "肺孢子虫肺炎": ["干咳", "呼吸困难", "发热", "低氧"],
+ "PCP": ["干咳", "呼吸困难", "发热", "低氧"],
+ "隐球菌脑膜炎": ["头痛", "发热", "意识改变", "颈强直"],
+ "结核": ["咳嗽", "盗汗", "体重下降", "发热"],
+ "肺炎": ["咳嗽", "发热", "胸痛", "呼吸困难"],
+ }
+
+ # 检查症状一致性
+ for diag_key, expected_symptoms in diagnosis_symptom_map.items():
+ if diag_key in diagnosis:
+ matched = sum(1 for s in symptoms if any(es in s for es in expected_symptoms))
+ if matched > 0:
+ score += min(matched / len(expected_symptoms) * 0.3, 0.3)
+ break
+
+ # 检查实验室结果一致性
+ if lab_results:
+ # CD4计数与HIV相关诊断
+ cd4 = lab_results.get("CD4") or lab_results.get("cd4")
+ if cd4 and isinstance(cd4, (int, float)):
+ if "PCP" in diagnosis or "肺孢子虫" in diagnosis:
+ if cd4 < 200:
+ score += 0.2
+ elif cd4 < 350:
+ score += 0.1
+
+ return min(score, 1.0)
+
+ def _evaluate_completeness(
+ self,
+ symptoms: Optional[List[str]],
+ lab_results: Optional[Dict],
+ imaging: Optional[List[str]],
+ history: Optional[str],
+ ) -> float:
+ """评估信息完整性"""
+ score = 0.0
+
+ # 各项信息的权重
+ if symptoms and len(symptoms) > 0:
+ score += 0.3
+ if lab_results and len(lab_results) > 0:
+ score += 0.3
+ if imaging and len(imaging) > 0:
+ score += 0.2
+ if history and len(history) > 10:
+ score += 0.2
+
+ return score
+
+ def _evaluate_certainty(self, diagnosis: str) -> float:
+ """评估诊断确定性"""
+ # 不确定性关键词
+ uncertain_keywords = [
+ "可能", "疑似", "待排除", "不除外", "考虑",
+ "建议进一步", "需要确认", "待定",
+ ]
+
+ # 确定性关键词
+ certain_keywords = [
+ "确诊", "明确", "典型", "符合", "诊断明确",
+ ]
+
+ score = 0.5 # 基础分
+
+ for kw in uncertain_keywords:
+ if kw in diagnosis:
+ score -= 0.1
+
+ for kw in certain_keywords:
+ if kw in diagnosis:
+ score += 0.15
+
+ return max(min(score, 1.0), 0.1)
+
+ def _calculate_overall_score(self, factors: Dict[str, float]) -> float:
+ """计算总体置信度"""
+ weights = {
+ "evidence": 0.35,
+ "consistency": 0.25,
+ "completeness": 0.20,
+ "certainty": 0.20,
+ }
+
+ score = sum(
+ factors.get(k, 0) * w
+ for k, w in weights.items()
+ )
+
+ return round(score, 3)
+
+ def _get_confidence_level(self, score: float) -> str:
+ """获取置信度等级"""
+ if score >= self.THRESHOLDS["high"]:
+ return "HIGH"
+ elif score >= self.THRESHOLDS["medium"]:
+ return "MEDIUM"
+ elif score >= self.THRESHOLDS["low"]:
+ return "LOW"
+ else:
+ return "UNCERTAIN"
+
+ def _evaluate_risk(self, diagnosis: str, confidence: float) -> RiskLevel:
+ """评估风险等级"""
+ # 检查高风险关键词
+ has_high_risk = any(kw in diagnosis for kw in self.HIGH_RISK_KEYWORDS)
+
+ if has_high_risk and confidence < 0.6:
+ return RiskLevel.CRITICAL
+ elif has_high_risk:
+ return RiskLevel.HIGH
+ elif confidence < 0.5:
+ return RiskLevel.MEDIUM
+ else:
+ return RiskLevel.LOW
+
+ def _generate_recommendations(
+ self,
+ confidence_level: str,
+ factors: Dict[str, float],
+ diagnosis: str,
+ ) -> List[str]:
+ """生成建议"""
+ recommendations = []
+
+ if factors.get("evidence", 0) < 0.5:
+ recommendations.append("建议补充更多诊断依据")
+
+ if factors.get("completeness", 0) < 0.5:
+ recommendations.append("建议完善病史和检查资料")
+
+ if confidence_level in ["LOW", "UNCERTAIN"]:
+ recommendations.append("建议进一步检查以明确诊断")
+ recommendations.append("必要时请专科会诊")
+
+ if not recommendations:
+ recommendations.append("诊断依据充分,可按诊断进行治疗")
+
+ return recommendations
+
+ def _generate_warnings(
+ self,
+ confidence_level: str,
+ risk_level: RiskLevel,
+ diagnosis: str,
+ ) -> List[str]:
+ """生成警告"""
+ warnings = []
+
+ if risk_level == RiskLevel.CRITICAL:
+ warnings.append("⚠️ 危急情况:诊断不确定但可能为严重疾病,请立即处理")
+
+ if confidence_level == "UNCERTAIN":
+ warnings.append("⚠️ 置信度极低,诊断结果仅供参考")
+ elif confidence_level == "LOW":
+ warnings.append("⚠️ 置信度较低,建议谨慎采纳")
+
+ warnings.append("本评估由AI生成,最终诊断请以临床医生判断为准")
+
+ return warnings
+
+ def add_custom_rule(
+ self,
+ condition: callable,
+ score_modifier: float,
+ description: str,
+ ):
+ """
+ 添加自定义评估规则
+
+ Args:
+ condition: 条件函数,接收诊断信息,返回bool
+ score_modifier: 分数修正值 (-1 到 1)
+ description: 规则描述
+ """
+ self._custom_rules.append({
+ "condition": condition,
+ "modifier": score_modifier,
+ "description": description,
+ })
+
+ def format_report(self, report: ConfidenceReport) -> str:
+ """格式化置信度报告"""
+ lines = []
+ lines.append("=" * 40)
+ lines.append("【置信度评估报告】")
+ lines.append("=" * 40)
+
+ # 置信度等级
+ level_emoji = {
+ "HIGH": "🟢", "MEDIUM": "🟡",
+ "LOW": "🔴", "UNCERTAIN": "⚪"
+ }
+ lines.append(f"\n总体置信度: {level_emoji.get(report.confidence_level, '⚪')} "
+ f"{report.confidence_level} ({report.overall_score*100:.1f}%)")
+
+ # 各维度得分
+ lines.append(f"\n📊 评估维度:")
+ lines.append(f" • 证据充分度: {report.evidence_score*100:.0f}%")
+ lines.append(f" • 一致性: {report.consistency_score*100:.0f}%")
+ lines.append(f" • 完整性: {report.completeness_score*100:.0f}%")
+
+ # 风险等级
+ risk_emoji = {
+ "critical": "🔴", "high": "🟠",
+ "medium": "🟡", "low": "🟢"
+ }
+ lines.append(f"\n⚠️ 风险等级: {risk_emoji.get(report.risk_level.value, '⚪')} "
+ f"{report.risk_level.value.upper()}")
+
+ # 建议
+ if report.recommendations:
+ lines.append(f"\n💡 建议:")
+ for rec in report.recommendations:
+ lines.append(f" • {rec}")
+
+ # 警告
+ if report.warnings:
+ lines.append(f"\n⚠️ 警告:")
+ for warn in report.warnings:
+ lines.append(f" • {warn}")
+
+ lines.append("\n" + "=" * 40)
+ return "\n".join(lines)
diff --git a/pathology-ai/code-changes/medical_extension/medical_prompts.py b/pathology-ai/code-changes/medical_extension/medical_prompts.py
new file mode 100644
index 000000000..6bd81cc5a
--- /dev/null
+++ b/pathology-ai/code-changes/medical_extension/medical_prompts.py
@@ -0,0 +1,514 @@
+"""
+医疗提示词库
+Medical Prompt Library for Nexent Platform
+
+提供预置的医疗领域提示词模板,支持:
+1. CoD诊断推理链提示词
+2. 不确定性感知提示词
+3. 专科领域提示词
+4. 安全性提示词
+
+Author: Pathology AI Team
+"""
+
+from typing import Dict, List, Optional
+from enum import Enum
+
+
+class PromptCategory(Enum):
+ """提示词分类"""
+ DIAGNOSIS = "diagnosis" # 诊断类
+ TREATMENT = "treatment" # 治疗类
+ SAFETY = "safety" # 安全类
+ SPECIALTY = "specialty" # 专科类
+ GENERAL = "general" # 通用类
+
+
+class MedicalPromptLibrary:
+ """
+ 医疗提示词库
+
+ 提供标准化的医疗AI提示词模板,确保:
+ 1. 诊断推理的结构化
+ 2. 不确定性的明确表达
+ 3. 安全性提醒
+ 4. 专业性保证
+
+ Usage:
+ library = MedicalPromptLibrary()
+ cod_prompt = library.get_prompt("chain_of_diagnosis")
+ all_prompts = library.list_prompts()
+ """
+
+ def __init__(self):
+ """初始化提示词库"""
+ self._prompts: Dict[str, Dict] = {}
+ self._load_builtin_prompts()
+
+ def _load_builtin_prompts(self):
+ """加载内置提示词"""
+
+ # 1. 诊断推理链 (CoD) 核心提示词
+ self._prompts["chain_of_diagnosis"] = {
+ "id": "chain_of_diagnosis",
+ "name": "诊断推理链 (CoD)",
+ "category": PromptCategory.DIAGNOSIS,
+ "description": "结构化的诊断推理方法,分步骤进行临床分析",
+ "prompt": self._get_cod_prompt(),
+ "variables": ["patient_info"],
+ "tags": ["核心", "诊断", "推理"],
+ }
+
+ # 2. 不确定性感知提示词
+ self._prompts["uncertainty_aware"] = {
+ "id": "uncertainty_aware",
+ "name": "不确定性感知",
+ "category": PromptCategory.SAFETY,
+ "description": "在回答中明确标注置信度和不确定性",
+ "prompt": self._get_uncertainty_prompt(),
+ "variables": [],
+ "tags": ["安全", "置信度", "不确定性"],
+ }
+
+ # 3. 安全性基础提示词
+ self._prompts["safety_base"] = {
+ "id": "safety_base",
+ "name": "医疗安全基础",
+ "category": PromptCategory.SAFETY,
+ "description": "医疗AI的基础安全提醒",
+ "prompt": self._get_safety_prompt(),
+ "variables": [],
+ "tags": ["安全", "免责", "基础"],
+ }
+
+ # 4. HIV/AIDS专科提示词
+ self._prompts["hiv_specialist"] = {
+ "id": "hiv_specialist",
+ "name": "HIV/AIDS专科",
+ "category": PromptCategory.SPECIALTY,
+ "description": "HIV/AIDS诊疗专业提示词",
+ "prompt": self._get_hiv_prompt(),
+ "variables": ["cd4_count", "viral_load"],
+ "tags": ["HIV", "AIDS", "感染", "专科"],
+ }
+
+ # 5. 病理诊断提示词
+ self._prompts["pathology_diagnosis"] = {
+ "id": "pathology_diagnosis",
+ "name": "病理诊断",
+ "category": PromptCategory.SPECIALTY,
+ "description": "病理学诊断专业提示词",
+ "prompt": self._get_pathology_prompt(),
+ "variables": ["specimen_type", "staining_method"],
+ "tags": ["病理", "诊断", "专科"],
+ }
+
+ # 6. 鉴别诊断提示词
+ self._prompts["differential_diagnosis"] = {
+ "id": "differential_diagnosis",
+ "name": "鉴别诊断",
+ "category": PromptCategory.DIAGNOSIS,
+ "description": "系统性鉴别诊断方法",
+ "prompt": self._get_differential_prompt(),
+ "variables": ["chief_complaint"],
+ "tags": ["诊断", "鉴别", "系统"],
+ }
+
+ # 7. 治疗建议提示词
+ self._prompts["treatment_suggestion"] = {
+ "id": "treatment_suggestion",
+ "name": "治疗建议",
+ "category": PromptCategory.TREATMENT,
+ "description": "基于循证医学的治疗建议",
+ "prompt": self._get_treatment_prompt(),
+ "variables": ["diagnosis", "patient_condition"],
+ "tags": ["治疗", "用药", "建议"],
+ }
+
+ # 8. 完整医疗助手提示词(组合版)
+ self._prompts["medical_assistant_full"] = {
+ "id": "medical_assistant_full",
+ "name": "完整医疗助手",
+ "category": PromptCategory.GENERAL,
+ "description": "包含CoD、不确定性感知和安全提醒的完整提示词",
+ "prompt": self._get_full_assistant_prompt(),
+ "variables": [],
+ "tags": ["完整", "推荐", "综合"],
+ }
+
+ def _get_cod_prompt(self) -> str:
+ """诊断推理链提示词"""
+ return """## 诊断推理链 (Chain-of-Diagnosis, CoD)
+
+请按以下步骤进行诊断推理:
+
+### 【步骤1 - 症状分析】
+- 识别主诉和主要症状
+- 分析症状的特点(部位、性质、程度、时间)
+- 注意伴随症状
+
+### 【步骤2 - 病史关联】
+- 既往病史与当前症状的关系
+- 用药史和过敏史
+- 家族史和社会史
+
+### 【步骤3 - 鉴别诊断】
+- 列出可能的诊断(按可能性排序)
+- 分析支持和反对每个诊断的证据
+- 考虑常见病和危重病
+
+### 【步骤4 - 检查建议】
+- 建议必要的实验室检查
+- 建议必要的影像学检查
+- 说明检查目的
+
+### 【步骤5 - 诊断结论】
+- 给出最可能的诊断
+- 标注置信度等级
+- 说明诊断依据
+"""
+
+ def _get_uncertainty_prompt(self) -> str:
+ """不确定性感知提示词"""
+ return """## 不确定性标注规范
+
+在给出诊断或建议时,请标注置信度:
+
+### 置信度等级
+- **HIGH (高置信度 >85%)**
+ - 证据充分,诊断明确
+ - 符合典型临床表现
+ - 有确诊性检查结果支持
+
+- **MEDIUM (中等置信度 60-85%)**
+ - 有一定依据,但需进一步确认
+ - 部分符合典型表现
+ - 需要排除其他诊断
+
+- **LOW (低置信度 <60%)**
+ - 信息不足,仅供参考
+ - 表现不典型
+ - 需要更多检查
+
+- **UNCERTAIN (不确定)**
+ - 无法做出可靠判断
+ - 信息严重不足
+ - 建议进一步检查
+
+### 标注格式
+在诊断结论后标注:[置信度: HIGH/MEDIUM/LOW/UNCERTAIN]
+"""
+
+ def _get_safety_prompt(self) -> str:
+ """安全性提示词"""
+ return """## 医疗安全提醒
+
+### 重要声明
+1. 本AI仅提供辅助参考,不能替代专业医生的诊断
+2. 最终诊断和治疗决策应由执业医师做出
+3. 紧急情况请立即就医或拨打急救电话
+
+### 使用限制
+- 不提供处方药物的具体剂量
+- 不对危急重症做出延误治疗的建议
+- 不替代必要的医学检查
+
+### 免责说明
+AI诊断建议仅供参考,使用者应自行承担相应风险。
+如有疑问,请咨询专业医疗人员。
+"""
+
+ def _get_hiv_prompt(self) -> str:
+ """HIV/AIDS专科提示词"""
+ return """## HIV/AIDS诊疗专家
+
+### 专业领域
+- HIV感染的诊断与分期
+- 抗逆转录病毒治疗(ART)方案
+- 机会性感染的预防与治疗
+- HIV相关肿瘤
+- 免疫重建炎症综合征(IRIS)
+
+### CD4计数与感染风险
+| CD4计数 | 风险等级 | 常见机会性感染 |
+|---------|----------|----------------|
+| <200 | 高风险 | PCP、弓形虫、隐球菌 |
+| <100 | 极高风险 | CMV、MAC |
+| <50 | 危重 | 播散性真菌感染 |
+
+### 常见机会性感染诊断要点
+1. **肺孢子虫肺炎(PCP)**
+ - 症状:干咳、进行性呼吸困难、发热
+ - 检查:诱导痰、BAL、LDH升高
+ - 治疗:TMP-SMX
+
+2. **隐球菌脑膜炎**
+ - 症状:头痛、发热、意识改变
+ - 检查:腰穿、墨汁染色、隐球菌抗原
+ - 治疗:两性霉素B + 氟康唑
+
+3. **结核病**
+ - 症状:咳嗽、盗汗、体重下降
+ - 检查:痰涂片、培养、GeneXpert
+ - 注意:与ART的药物相互作用
+"""
+
+ def _get_pathology_prompt(self) -> str:
+ """病理诊断提示词"""
+ return """## 病理诊断专家
+
+### 专业能力
+- 组织病理学诊断
+- 细胞病理学诊断
+- 分子病理学解读
+- 免疫组化分析
+
+### 诊断流程
+1. **标本信息**:部位、类型、固定方式
+2. **大体描述**:大小、颜色、质地、切面
+3. **镜下所见**:细胞形态、组织结构、特殊发现
+4. **特殊染色/免疫组化**:结果及意义
+5. **病理诊断**:诊断名称、分级分期
+6. **备注**:建议进一步检查或会诊
+
+### HIV相关病理改变
+- 淋巴结:滤泡增生→耗竭
+- 肺:PCP间质性肺炎
+- 皮肤:卡波西肉瘤
+- 脑:弓形虫脑病、PML
+
+### 报告规范
+- 使用标准化术语
+- 明确诊断依据
+- 标注置信度
+- 必要时建议会诊
+"""
+
+ def _get_differential_prompt(self) -> str:
+ """鉴别诊断提示词"""
+ return """## 鉴别诊断方法
+
+### 系统性鉴别诊断步骤
+
+1. **确定主要问题**
+ - 明确主诉
+ - 识别关键症状
+
+2. **生成诊断假设**
+ - 常见病优先
+ - 不遗漏危重病
+ - 考虑年龄、性别、基础疾病
+
+3. **收集鉴别信息**
+ - 针对性病史询问
+ - 针对性体格检查
+ - 必要的辅助检查
+
+4. **评估每个诊断**
+ - 支持证据
+ - 反对证据
+ - 可能性评估
+
+5. **得出结论**
+ - 最可能诊断
+ - 需排除诊断
+ - 进一步检查建议
+
+### 鉴别诊断表格格式
+| 诊断 | 支持证据 | 反对证据 | 可能性 |
+|------|----------|----------|--------|
+| ... | ... | ... | 高/中/低 |
+"""
+
+ def _get_treatment_prompt(self) -> str:
+ """治疗建议提示词"""
+ return """## 治疗建议规范
+
+### 治疗建议原则
+1. 基于循证医学证据
+2. 考虑患者个体情况
+3. 权衡利弊风险
+4. 尊重患者意愿
+
+### 建议格式
+1. **一般治疗**
+ - 休息、饮食、护理
+
+2. **药物治疗**
+ - 药物名称(通用名)
+ - 用法用量范围
+ - 疗程建议
+ - 注意事项
+
+3. **其他治疗**
+ - 手术/介入指征
+ - 康复治疗
+ - 中医治疗
+
+4. **随访建议**
+ - 复查时间
+ - 复查项目
+ - 注意事项
+
+### 安全提醒
+- 具体剂量请遵医嘱
+- 注意药物相互作用
+- 关注不良反应
+- 特殊人群调整
+"""
+
+ def _get_full_assistant_prompt(self) -> str:
+ """完整医疗助手提示词"""
+ return """# 医疗诊断助手
+
+你是一位专业的医疗诊断助手,具备以下能力和规范:
+
+## 一、诊断方法:诊断推理链 (CoD)
+
+请按以下步骤进行诊断推理:
+
+【步骤1 - 症状分析】
+分析患者的主诉和症状,识别关键临床表现。
+
+【步骤2 - 病史关联】
+结合既往病史,分析与当前症状的关联性。
+
+【步骤3 - 鉴别诊断】
+列出可能的诊断,并说明支持和反对的证据。
+
+【步骤4 - 检查建议】
+建议进一步的检查以明确诊断。
+
+【步骤5 - 诊断结论】
+给出最可能的诊断,并标注置信度。
+
+## 二、置信度标注
+
+在诊断结论中标注置信度:
+- **HIGH** (>85%): 证据充分,诊断明确
+- **MEDIUM** (60-85%): 有一定依据,需进一步确认
+- **LOW** (<60%): 信息不足,仅供参考
+- **UNCERTAIN**: 无法做出可靠判断
+
+格式:[置信度: HIGH/MEDIUM/LOW/UNCERTAIN]
+
+## 三、安全提醒
+
+⚠️ 重要声明:
+1. 本AI仅提供辅助参考,不能替代专业医生的诊断
+2. 最终诊断和治疗决策应由执业医师做出
+3. 紧急情况请立即就医或拨打急救电话
+4. AI诊断建议仅供参考,使用者应自行承担相应风险
+
+## 四、回答规范
+
+1. 使用专业但易懂的语言
+2. 结构清晰,逻辑严谨
+3. 明确标注不确定性
+4. 必要时建议就医或会诊
+"""
+
+ def get_prompt(self, prompt_id: str) -> Optional[Dict]:
+ """
+ 获取指定提示词
+
+ Args:
+ prompt_id: 提示词ID
+
+ Returns:
+ 提示词字典或None
+ """
+ return self._prompts.get(prompt_id)
+
+ def get_prompt_text(self, prompt_id: str) -> Optional[str]:
+ """
+ 获取提示词文本
+
+ Args:
+ prompt_id: 提示词ID
+
+ Returns:
+ 提示词文本或None
+ """
+ prompt = self._prompts.get(prompt_id)
+ return prompt["prompt"] if prompt else None
+
+ def list_prompts(
+ self,
+ category: Optional[PromptCategory] = None
+ ) -> List[Dict]:
+ """
+ 列出所有提示词
+
+ Args:
+ category: 可选,按分类筛选
+
+ Returns:
+ 提示词列表
+ """
+ prompts = list(self._prompts.values())
+ if category:
+ prompts = [p for p in prompts if p["category"] == category]
+ return prompts
+
+ def list_prompt_ids(self) -> List[str]:
+ """获取所有提示词ID"""
+ return list(self._prompts.keys())
+
+ def combine_prompts(self, prompt_ids: List[str]) -> str:
+ """
+ 组合多个提示词
+
+ Args:
+ prompt_ids: 提示词ID列表
+
+ Returns:
+ 组合后的提示词文本
+ """
+ parts = []
+ for pid in prompt_ids:
+ prompt = self.get_prompt_text(pid)
+ if prompt:
+ parts.append(prompt)
+ return "\n\n---\n\n".join(parts)
+
+ def add_custom_prompt(
+ self,
+ prompt_id: str,
+ name: str,
+ prompt_text: str,
+ category: PromptCategory = PromptCategory.GENERAL,
+ description: str = "",
+ tags: Optional[List[str]] = None,
+ ) -> bool:
+ """
+ 添加自定义提示词
+
+ Args:
+ prompt_id: 提示词ID
+ name: 名称
+ prompt_text: 提示词文本
+ category: 分类
+ description: 描述
+ tags: 标签
+
+ Returns:
+ 是否添加成功
+ """
+ if prompt_id in self._prompts:
+ return False
+
+ self._prompts[prompt_id] = {
+ "id": prompt_id,
+ "name": name,
+ "category": category,
+ "description": description,
+ "prompt": prompt_text,
+ "variables": [],
+ "tags": tags or [],
+ }
+ return True
+
+ def get_recommended_prompt(self) -> str:
+ """获取推荐的完整提示词"""
+ return self.get_prompt_text("medical_assistant_full")
diff --git a/pathology-ai/code-changes/medical_extension/test_medical.py b/pathology-ai/code-changes/medical_extension/test_medical.py
new file mode 100644
index 000000000..85e5c770a
--- /dev/null
+++ b/pathology-ai/code-changes/medical_extension/test_medical.py
@@ -0,0 +1,150 @@
+"""
+医疗模块测试脚本
+Test script for Medical Module
+"""
+
+import sys
+import os
+
+# 添加路径
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from medical import (
+ ChainOfDiagnosis,
+ MedicalAgentTemplates,
+ ConfidenceEvaluator,
+ MedicalPromptLibrary,
+)
+
+
+def test_chain_of_diagnosis():
+ """测试诊断推理链"""
+ print("=" * 50)
+ print("测试: Chain-of-Diagnosis (CoD)")
+ print("=" * 50)
+
+ cod = ChainOfDiagnosis()
+
+ # 测试案例: HIV患者肺部感染
+ result = cod.analyze(
+ symptoms="干咳、呼吸困难、发热",
+ lab_results="CD4计数: 150, LDH升高",
+ medical_history="HIV阳性5年,未规律服药",
+ )
+
+ print(f"\n主要诊断: {result.primary_diagnosis}")
+ print(f"置信度: {result.confidence_level.value} ({result.confidence_score*100:.1f}%)")
+ print(f"鉴别诊断: {', '.join(result.differential_diagnoses)}")
+ print(f"推理步骤数: {len(result.reasoning_chain)}")
+
+ print("\n[OK] CoD测试通过")
+ return True
+
+
+def test_agent_templates():
+ """测试智能体模板"""
+ print("\n" + "=" * 50)
+ print("测试: Medical Agent Templates")
+ print("=" * 50)
+
+ templates = MedicalAgentTemplates()
+
+ # 列出所有模板
+ all_templates = templates.list_templates()
+ print(f"\n可用模板数量: {len(all_templates)}")
+
+ for t in all_templates:
+ print(f" - {t.name} ({t.template_id})")
+
+ # 获取病理模板
+ pathology = templates.get_template("pathology_diagnosis")
+ if pathology:
+ print(f"\n病理模板工具: {', '.join(pathology.suggested_tools)}")
+
+ print("\n[OK] 模板测试通过")
+ return True
+
+
+def test_confidence_evaluator():
+ """测试置信度评估"""
+ print("\n" + "=" * 50)
+ print("测试: Confidence Evaluator")
+ print("=" * 50)
+
+ evaluator = ConfidenceEvaluator()
+
+ report = evaluator.evaluate(
+ diagnosis="肺孢子虫肺炎 (PCP)",
+ symptoms=["干咳", "呼吸困难", "发热"],
+ lab_results={"CD4": 150, "LDH": "升高"},
+ evidence=["HIV阳性", "CD4<200", "典型症状"],
+ )
+
+ print(f"\n总体置信度: {report.confidence_level} ({report.overall_score*100:.1f}%)")
+ print(f"证据充分度: {report.evidence_score*100:.0f}%")
+ print(f"一致性: {report.consistency_score*100:.0f}%")
+ print(f"风险等级: {report.risk_level.value}")
+
+ print("\n[OK] 置信度评估测试通过")
+ return True
+
+
+def test_prompt_library():
+ """测试提示词库"""
+ print("\n" + "=" * 50)
+ print("测试: Medical Prompt Library")
+ print("=" * 50)
+
+ library = MedicalPromptLibrary()
+
+ # 列出所有提示词
+ all_prompts = library.list_prompts()
+ print(f"\n可用提示词数量: {len(all_prompts)}")
+
+ for p in all_prompts:
+ print(f" - {p['name']} ({p['id']})")
+
+ # 获取推荐提示词
+ recommended = library.get_recommended_prompt()
+ print(f"\n推荐提示词长度: {len(recommended)} 字符")
+
+ print("\n[OK] 提示词库测试通过")
+ return True
+
+
+def main():
+ """运行所有测试"""
+ print("\n" + "#" * 60)
+ print("# Nexent 医疗模块测试")
+ print("#" * 60)
+
+ tests = [
+ test_chain_of_diagnosis,
+ test_agent_templates,
+ test_confidence_evaluator,
+ test_prompt_library,
+ ]
+
+ passed = 0
+ failed = 0
+
+ for test in tests:
+ try:
+ if test():
+ passed += 1
+ else:
+ failed += 1
+ except Exception as e:
+ print(f"\n[FAIL] {test.__name__}: {e}")
+ failed += 1
+
+ print("\n" + "#" * 60)
+ print(f"# 测试结果: {passed} 通过, {failed} 失败")
+ print("#" * 60)
+
+ return failed == 0
+
+
+if __name__ == "__main__":
+ success = main()
+ sys.exit(0 if success else 1)
diff --git a/pathology-ai/custom-tools.md b/pathology-ai/custom-tools.md
new file mode 100644
index 000000000..a6cf27613
--- /dev/null
+++ b/pathology-ai/custom-tools.md
@@ -0,0 +1,368 @@
+# 🔧 自定义工具说明
+
+本文档详细介绍病理学AI助手中新增的自定义MCP工具。
+
+## 工具列表(共15个)
+
+### 医疗诊断工具
+
+| 工具名称 | 功能简述 |
+|----------|----------|
+| chain_of_diagnosis | 5步结构化诊断推理(CoD) |
+| evaluate_diagnosis_confidence | 置信度与风险评估 |
+| search_pathology_images | 搜索本地病理图片 |
+| generate_medical_guide | 生成就医指南 |
+
+### 诊断模拟游戏
+
+| 工具名称 | 功能简述 |
+|----------|----------|
+| start_diagnosis_game | 启动诊断模拟游戏 |
+| diagnosis_action | 执行诊断游戏动作(问诊/体检/检查/诊断) |
+
+### 医学可视化工具
+
+| 工具名称 | 功能简述 |
+|----------|----------|
+| generate_knowledge_graph | 生成医学知识图谱(Mermaid) |
+| generate_diagnosis_flow | 生成诊断流程图 |
+| generate_medical_chart | 生成统计图表(柱状图/折线图/饼图) |
+| generate_radar_chart | 生成雷达图(多维度健康指标对比) |
+| generate_timeline | 生成时间线图(疾病发展/治疗计划) |
+| generate_gantt_chart | 生成甘特图(治疗疗程安排) |
+| generate_quadrant_chart | 生成象限图(风险评估/优先级分析) |
+| generate_state_diagram | 生成状态转换图(疾病状态变化) |
+| generate_sankey_diagram | 生成桑基图(流量和转换关系) |
+
+---
+
+## 1. chain_of_diagnosis
+
+### 功能
+实现 Chain-of-Diagnosis (CoD) 诊断推理链,将复杂的诊断过程分解为5个结构化步骤。
+
+### 参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| symptoms | str | 是 | 患者症状描述 |
+| medical_history | str | 否 | 既往病史 |
+| lab_results | str | 否 | 实验室检查结果 |
+| imaging_findings | str | 否 | 影像学发现 |
+
+### 输出格式
+
+```markdown
+## 🔬 Chain-of-Diagnosis 诊断推理
+
+### Step 1: 症状分析
+- 主要症状识别
+- 症状特征分析
+
+### Step 2: 病史关联
+- 相关病史
+- 风险因素
+
+### Step 3: 鉴别诊断
+- 可能诊断列表
+- 排除诊断
+
+### Step 4: 检查建议
+- 推荐检查项目
+- 优先级排序
+
+### Step 5: 初步结论
+- 最可能诊断
+- 置信度评估
+```
+
+### 示例调用
+
+```python
+result = chain_of_diagnosis(
+ symptoms="持续发热2周,体重下降,淋巴结肿大",
+ medical_history="无特殊病史",
+ lab_results="白细胞减少"
+)
+```
+
+---
+
+## 2. evaluate_diagnosis_confidence
+
+### 功能
+评估医疗诊断或回答的置信度,包括证据充分度、一致性、完整性等维度。
+
+### 参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| diagnosis | str | 是 | 诊断结果 |
+| symptoms | str | 否 | 症状列表,用逗号分隔 |
+| evidence | str | 否 | 支持证据,用逗号分隔 |
+| lab_results | str | 否 | 实验室结果 |
+
+### 输出格式
+
+```markdown
+## 📊 置信度评估报告
+
+### 总体置信度: HIGH/MEDIUM/LOW/UNCERTAIN
+
+### 评估维度
+| 维度 | 得分 | 说明 |
+|------|------|------|
+| 证据充分度 | 85% | ... |
+| 一致性 | 90% | ... |
+| 完整性 | 80% | ... |
+| 确定性 | 75% | ... |
+
+### 风险等级: LOW/MEDIUM/HIGH/CRITICAL
+
+### 建议
+- ...
+```
+
+### 置信度级别
+
+| 级别 | 分数范围 | 说明 |
+|------|----------|------|
+| HIGH | ≥80% | 证据充分,可信度高 |
+| MEDIUM | 60-79% | 有一定依据,需进一步确认 |
+| LOW | 40-59% | 证据不足,建议谨慎 |
+| UNCERTAIN | <40% | 高度不确定,强烈建议就医 |
+
+---
+
+## 3. start_diagnosis_game
+
+### 功能
+启动交互式诊断模拟游戏,用户扮演医生进行问诊练习。
+
+### 参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| difficulty | int | 否 | 难度等级 (1=初级, 2=中级, 3=高级) |
+| case_type | str | 否 | 病例类型 (hiv_basic, hiv_opportunistic, random) |
+
+### 输出格式
+
+```markdown
+## 🏥 诊断模拟器 - 病例开始
+
+### 👤 患者信息
+**男性,32岁,程序员**
+
+### 💬 主诉
+> "医生,我最近一个月反复发热..."
+
+### 📋 当前阶段:问诊 (第1步/共4步)
+
+**请选择您要询问的内容:**
+
+[btn:询问发热详情] [btn:询问其他症状] [btn:询问既往病史]
+[btn:询问接触史] [btn:询问用药情况] [btn:进入体格检查]
+```
+
+### 游戏流程
+
+```
+问诊 → 体格检查 → 辅助检查 → 给出诊断
+```
+
+---
+
+## 4. diagnosis_action
+
+### 功能
+在诊断模拟游戏中执行具体动作。
+
+### 参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| case_id | str | 是 | 病例ID |
+| action_type | str | 是 | 动作类型 (ask/exam/test/diagnose) |
+| action_detail | str | 是 | 具体动作内容 |
+
+### 动作类型
+
+| 类型 | 说明 | 示例 |
+|------|------|------|
+| ask | 问诊 | 询问发热情况 |
+| exam | 体格检查 | 检查淋巴结 |
+| test | 辅助检查 | HIV抗体初筛 |
+| diagnose | 给出诊断 | 给出诊断结论 |
+
+---
+
+## 5. search_pathology_images
+
+### 功能
+搜索本地病理图片服务器中的图片。
+
+### 参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| keyword | str | 是 | 搜索关键词 |
+| count | int | 否 | 返回数量(默认6,最大9) |
+
+### 支持的关键词类别
+
+- HIV/AIDS/免疫 - 免疫病理学图片
+- 感染 - 感染性疾病图片
+- 心血管 - 心血管病理图片
+- 肺/呼吸 - 肺部病理图片
+- 肿瘤/癌 - 肿瘤病理图片
+- 神经/脑 - 神经系统病理图片
+- 胃肠/消化 - 消化系统病理图片
+
+### 输出格式
+
+```markdown
+## 🔍 病理图片搜索结果
+
+找到 5 张相关图片:
+
+| 序号 | 分类 | 文件名 | URL |
+|------|------|--------|-----|
+| 1 | Immunopathology | hiv_lymph_node.jpg | http://... |
+```
+
+---
+
+## 6. generate_medical_guide
+
+### 功能
+生成结构化的就医指南,包括科室推荐、检查项目、注意事项等。
+
+### 参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| condition | str | 是 | 病情描述 |
+| urgency | str | 否 | 紧急程度 (emergency/urgent/routine),默认urgent |
+| patient_info | str | 否 | 患者关键信息 |
+
+### 输出格式
+
+```markdown
+## 🏥 就医指南
+
+### 推荐科室
+| 优先级 | 科室 | 说明 |
+|--------|------|------|
+| 1 | 感染科 | ... |
+
+### 建议检查
+| 检查项目 | 目的 | 费用参考 |
+|----------|------|----------|
+| HIV抗体 | 初筛 | ¥50-100 |
+
+### 就诊流程
+[Mermaid流程图]
+
+### 注意事项
+- ...
+```
+
+---
+
+---
+
+## 7-15. 医学可视化工具
+
+以下工具均输出 **Mermaid 格式**,可在前端直接渲染。
+
+### 7. generate_knowledge_graph
+生成医学知识图谱,展示疾病、症状、治疗的关系。
+
+| 参数 | 说明 |
+|------|------|
+| topic | 主题(如"HIV感染") |
+| nodes | 节点列表,用\|分隔 |
+| relations | 关系列表,用\|分隔 |
+
+### 8. generate_diagnosis_flow
+生成诊断流程图,展示诊断步骤和决策点。
+
+| 参数 | 说明 |
+|------|------|
+| disease | 疾病名称 |
+| steps | 步骤列表,用\|分隔 |
+| decisions | 决策点列表 |
+
+### 9. generate_medical_chart
+生成统计图表(柱状图/折线图/饼图)。
+
+| 参数 | 说明 |
+|------|------|
+| chart_type | 图表类型 (bar/line/pie) |
+| title | 标题 |
+| data | 数据,格式"标签:值\|标签:值" |
+
+### 10. generate_radar_chart
+生成雷达图,用于多维度健康指标对比。
+
+| 参数 | 说明 |
+|------|------|
+| title | 标题 |
+| metrics | 指标列表 |
+| values | 数值列表 |
+
+### 11. generate_timeline
+生成时间线图,展示疾病发展或治疗计划。
+
+| 参数 | 说明 |
+|------|------|
+| title | 标题 |
+| events | 事件列表,格式"时间:描述\|时间:描述" |
+
+### 12. generate_gantt_chart
+生成甘特图,用于治疗疗程安排。
+
+| 参数 | 说明 |
+|------|------|
+| title | 标题 |
+| tasks | 任务列表 |
+
+### 13. generate_quadrant_chart
+生成象限图,用于风险评估和优先级分析。
+
+| 参数 | 说明 |
+|------|------|
+| title | 标题 |
+| x_axis | X轴标签 |
+| y_axis | Y轴标签 |
+| items | 项目列表 |
+
+### 14. generate_state_diagram
+生成状态转换图,展示疾病状态变化。
+
+| 参数 | 说明 |
+|------|------|
+| title | 标题 |
+| states | 状态列表 |
+| transitions | 转换列表 |
+
+### 15. generate_sankey_diagram
+生成桑基图,展示流量和转换关系。
+
+| 参数 | 说明 |
+|------|------|
+| title | 标题 |
+| flows | 流量列表 |
+
+---
+
+## 工具文件位置
+
+所有自定义工具定义在:
+
+```
+backend/tool_collection/mcp/local_mcp_service.py
+```
+
+使用 FastMCP 框架注册,通过 `@local_mcp_service.tool()` 装饰器定义。
diff --git a/pathology-ai/diagrams/cod_process.png b/pathology-ai/diagrams/cod_process.png
new file mode 100644
index 000000000..7da167fb4
Binary files /dev/null and b/pathology-ai/diagrams/cod_process.png differ
diff --git a/pathology-ai/diagrams/dataflow.png b/pathology-ai/diagrams/dataflow.png
new file mode 100644
index 000000000..1d825e141
Binary files /dev/null and b/pathology-ai/diagrams/dataflow.png differ
diff --git a/pathology-ai/diagrams/game_flow.png b/pathology-ai/diagrams/game_flow.png
new file mode 100644
index 000000000..e47c55b6a
Binary files /dev/null and b/pathology-ai/diagrams/game_flow.png differ
diff --git a/pathology-ai/diagrams/system_architecture.png b/pathology-ai/diagrams/system_architecture.png
new file mode 100644
index 000000000..37d822ebd
Binary files /dev/null and b/pathology-ai/diagrams/system_architecture.png differ
diff --git a/pathology-ai/diagrams/tools_architecture.png b/pathology-ai/diagrams/tools_architecture.png
new file mode 100644
index 000000000..545701273
Binary files /dev/null and b/pathology-ai/diagrams/tools_architecture.png differ
diff --git a/pathology-ai/frontend-improvements.md b/pathology-ai/frontend-improvements.md
new file mode 100644
index 000000000..6cbbfdf9a
--- /dev/null
+++ b/pathology-ai/frontend-improvements.md
@@ -0,0 +1,154 @@
+# 🎨 前端改进说明
+
+本文档详细介绍病理学AI助手中的前端优化和新增组件。
+
+## 新增组件
+
+### 1. PathologyImageGallery.tsx
+
+**位置**: `frontend/components/medical-visualization/PathologyImageGallery.tsx`
+
+**功能**: 病理图片画廊组件,用于展示和预览病理图片
+
+**特性**:
+- 网格布局展示图片
+- 点击图片放大预览
+- 支持图片分类标签
+- 响应式设计
+
+### 2. DiagnosisConfidenceCard.tsx
+
+**位置**: `frontend/components/medical-visualization/DiagnosisConfidenceCard.tsx`
+
+**功能**: 置信度评估卡片组件
+
+**特性**:
+- 显示总体置信度分数
+- 风险等级指示器 (LOW/MEDIUM/HIGH/CRITICAL)
+- 评估维度雷达图
+- 建议和警告显示
+
+### 3. SourceTag.tsx
+
+**位置**: `frontend/components/medical-visualization/SourceTag.tsx`
+
+**功能**: 来源标签组件,用于标注信息来源
+
+**特性**:
+- [内部] 标签 - 蓝色,表示来自本地知识库
+- [外部] 标签 - 绿色,表示来自互联网搜索
+- 悬停显示详细来源信息
+
+---
+
+## 修改的组件
+
+### 1. MedicalVisualizationPanel.tsx
+
+**位置**: `frontend/components/medical-visualization/MedicalVisualizationPanel.tsx`
+
+**修改内容**:
+- 移除HIV/AIDS硬编码文字
+- 改为通用病理学描述
+- 支持动态标题和描述
+
+**修改行**: 54-56, 97
+
+### 2. markdownRenderer.tsx
+
+**位置**: `frontend/components/ui/markdownRenderer.tsx`
+
+**修改内容**:
+- 新增 `ClickableOption` 组件
+- 解析 `[btn:xxx]` 格式为可点击按钮
+- 支持诊断游戏交互
+
+**新增代码位置**: 378-410行 (ClickableOption组件), 975-1045行 (processText函数)
+
+### 3. chatLeftSidebar.tsx
+
+**位置**: `frontend/app/[locale]/chat/components/chatLeftSidebar.tsx`
+
+**修改内容**:
+- 新增"清空所有对话"按钮
+- 新增删除确认对话框
+- 新增 `handleDeleteAllClick` 和 `confirmDeleteAll` 函数
+
+**修改行**: 10, 136-138, 209-227, 463-475, 507-541
+
+### 4. conversationService.ts
+
+**位置**: `frontend/services/conversationService.ts`
+
+**修改内容**:
+- 新增 `deleteAll` 方法用于批量删除对话
+
+**修改行**: 122-130
+
+### 5. index.ts (医学可视化组件导出)
+
+**位置**: `frontend/components/medical-visualization/index.ts`
+
+**修改内容**:
+- 添加新组件的导出语句
+
+---
+
+## 组件使用示例
+
+### PathologyImageGallery
+
+```tsx
+import { PathologyImageGallery } from '@/components/medical-visualization';
+
+
+```
+
+### DiagnosisConfidenceCard
+
+```tsx
+import { DiagnosisConfidenceCard } from '@/components/medical-visualization';
+
+
+```
+
+### SourceTag
+
+```tsx
+import { SourceTag } from '@/components/medical-visualization';
+
+ // 显示 [内部]
+ // 显示 [外部]
+```
+
+### 可点击按钮 (Markdown中)
+
+在AI回复中使用 `[btn:选项文字]` 格式,会自动渲染为可点击按钮:
+
+```markdown
+请选择下一步操作:
+
+[btn:询问发热情况] [btn:询问其他症状] [btn:进入体格检查]
+```
+
+---
+
+## 样式说明
+
+所有新增组件使用:
+- **TailwindCSS** 进行样式定义
+- **Lucide React** 图标库
+- **shadcn/ui** 基础组件
+
+遵循 Nexent 现有设计规范,保持视觉一致性。