feat(aia): Implement Protocol Agent MVP with reusable Agent framework

Sprint 1-3 Completed (Backend + Frontend):

Backend (Sprint 1-2):
- Implement 5-layer Agent framework (Query->Planner->Executor->Tools->Reflection)
- Create agent_schema with 6 tables (agent_definitions, stages, prompts, sessions, traces, reflexion_rules)
- Create protocol_schema with 2 tables (protocol_contexts, protocol_generations)
- Implement Protocol Agent core services (Orchestrator, ContextService, PromptBuilder)
- Integrate LLM service adapter (DeepSeek/Qwen/GPT-5/Claude)
- 6 API endpoints with full authentication
- 10/10 API tests passed

Frontend (Sprint 3):
- Add Protocol Agent entry in AgentHub (indigo theme card)
- Implement ProtocolAgentPage with 3-column layout
- Collapsible sidebar (Gemini style, 48px <-> 280px)
- StatePanel with 5 stage cards (scientific_question, pico, study_design, sample_size, endpoints)
- ChatArea with sync button and action cards integration
- 100% prototype design restoration (608 lines CSS)
- Detailed endpoints structure: baseline, exposure, outcomes, confounders

Features:
- 5-stage dialogue flow for research protocol design
- Conversation-driven interaction with sync-to-protocol button
- Real-time context state management
- One-click protocol generation button (UI ready, backend pending)

Database:
- agent_schema: 6 tables for reusable Agent framework
- protocol_schema: 2 tables for Protocol Agent
- Seed data: 1 agent + 5 stages + 9 prompts + 4 reflexion rules

Code Stats:
- Backend: 13 files, 4338 lines
- Frontend: 14 files, 2071 lines
- Total: 27 files, 6409 lines

Status: MVP core functionality completed, pending frontend-backend integration testing

Next: Sprint 4 - One-click protocol generation + Word export
This commit is contained in:
2026-01-24 17:29:24 +08:00
parent 61cdc97eeb
commit 96290d2f76
345 changed files with 13945 additions and 47 deletions

View File

@@ -0,0 +1,442 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Protocol Agent V2.0 - 产品原型</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- 引入 Lucide 图标 -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
body { font-family: 'Inter', 'Noto Sans SC', sans-serif; }
/* 模拟 Ant Design X 的 ultramodern 风格 */
.chat-bubble-ai {
background-color: #F3F4F6;
border-top-left-radius: 2px;
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
border-bottom-left-radius: 12px;
}
.chat-bubble-user {
background-color: #4F46E5; /* Indigo-600 */
color: white;
border-top-left-radius: 12px;
border-top-right-radius: 2px;
border-bottom-right-radius: 12px;
border-bottom-left-radius: 12px;
}
.reflexion-border {
border-left: 4px solid #9333EA; /* Purple-600 */
background: #F3E8FF; /* Purple-100 */
}
/* 状态面板动画 */
.flash-update {
animation: flash 1.5s ease-out;
}
@keyframes flash {
0% { background-color: #dbeafe; } /* blue-100 */
100% { background-color: transparent; }
}
/* 滚动条美化 */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
</style>
</head>
<body class="bg-gray-100 h-screen flex flex-col overflow-hidden">
<!-- Top Navigation -->
<header class="bg-white border-b border-gray-200 h-14 flex items-center justify-between px-4 shadow-sm z-10">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center text-white font-bold shadow-lg shadow-indigo-200">
<i data-lucide="bot" class="w-5 h-5"></i>
</div>
<div>
<h1 class="font-bold text-gray-800 text-sm">研究方案制定 Agent</h1>
<div class="flex items-center gap-1.5">
<span class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
<span class="text-xs text-gray-500">Orchestrator Online</span>
</div>
</div>
</div>
<div class="flex items-center gap-4">
<div class="hidden md:flex items-center gap-2 px-3 py-1.5 bg-gray-50 rounded-full border border-gray-200">
<span class="text-xs font-medium text-gray-500">当前阶段:</span>
<span class="text-xs font-bold text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded border border-indigo-100">Step 5: 样本量计算</span>
</div>
<button class="p-2 hover:bg-gray-100 rounded-full text-gray-600">
<i data-lucide="settings" class="w-5 h-5"></i>
</button>
</div>
</header>
<!-- Main Workspace -->
<main class="flex-1 flex overflow-hidden">
<!-- Left: Chat Interface (70%) -->
<section class="flex-1 flex flex-col bg-white relative">
<!-- Chat History -->
<div id="chat-container" class="flex-1 overflow-y-auto p-4 md:p-6 space-y-6 pb-20">
<!-- Welcome Message -->
<div class="flex gap-4">
<div class="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center shrink-0">
<i data-lucide="sparkles" class="w-4 h-4 text-indigo-600"></i>
</div>
<div class="space-y-1 max-w-[80%]">
<div class="text-xs text-gray-400 ml-1">Protocol Agent • 10:00 AM</div>
<div class="chat-bubble-ai p-4 text-sm text-gray-800 shadow-sm">
<p>您好!我是您的研究方案制定助手。</p>
<p class="mt-2">我已经记录了您关于<strong>“阿司匹林预防老年高血压患者中风”</strong>的科学问题和 PICO 信息。</p>
<p class="mt-2">根据当前的 RCT 设计,我们需要进行<strong>样本量计算</strong>以支撑伦理审查。您准备好开始了吗?</p>
</div>
</div>
</div>
<!-- User Message -->
<div class="flex gap-4 flex-row-reverse">
<div class="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center shrink-0">
<i data-lucide="user" class="w-4 h-4 text-gray-500"></i>
</div>
<div class="space-y-1 max-w-[80%]">
<div class="text-xs text-gray-400 text-right mr-1">User • 10:02 AM</div>
<div class="chat-bubble-user p-4 text-sm shadow-sm">
好的,帮我计算一下样本量。
</div>
</div>
</div>
<!-- AI Plan & Action Card (Deep Link) -->
<div class="flex gap-4">
<div class="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center shrink-0">
<i data-lucide="brain-circuit" class="w-4 h-4 text-indigo-600"></i>
</div>
<div class="space-y-1 max-w-[80%]">
<div class="text-xs text-gray-400 ml-1">Protocol Agent • 10:02 AM</div>
<!-- The Action Card Component -->
<div class="border border-indigo-200 bg-indigo-50/50 rounded-xl p-4 shadow-sm w-[380px]">
<div class="flex items-start gap-3 mb-3">
<div class="bg-indigo-100 p-2 rounded-lg">
<i data-lucide="calculator" class="w-5 h-5 text-indigo-600"></i>
</div>
<div>
<h3 class="font-bold text-gray-900 text-sm">建议执行:样本量计算</h3>
<p class="text-xs text-gray-500 mt-0.5">基于双侧两样本均数比较 (RCT)</p>
</div>
</div>
<!-- Prefilled Parameters Preview -->
<div class="bg-white rounded border border-indigo-100 p-2 mb-3 space-y-1">
<div class="flex justify-between text-xs">
<span class="text-gray-500">Alpha (α):</span>
<span class="font-mono font-medium">0.05</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-gray-500">Power (1-β):</span>
<span class="font-mono font-medium">0.80</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-gray-500">Effect Size:</span>
<span class="font-mono font-medium">预估 0.15 vs 0.10</span>
</div>
</div>
<button onclick="openToolModal()" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium py-2 rounded-lg transition flex items-center justify-center gap-2 shadow-sm shadow-indigo-200">
<span>🚀 前往计算工具</span>
<i data-lucide="external-link" class="w-3 h-3"></i>
</button>
<p class="text-[10px] text-center text-gray-400 mt-2">点击将打开工具面板,参数已自动预填</p>
</div>
</div>
</div>
<!-- Dynamic Content Will Be Appended Here -->
</div>
<!-- Input Area -->
<div class="p-4 border-t border-gray-100 bg-white absolute bottom-0 w-full">
<div class="relative">
<input type="text" placeholder="输入您的指令..." class="w-full pl-4 pr-12 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:bg-white transition text-sm">
<button class="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">
<i data-lucide="send" class="w-4 h-4"></i>
</button>
</div>
</div>
</section>
<!-- Right: Protocol State Panel (30%) -->
<aside class="w-[350px] bg-gray-50 border-l border-gray-200 flex flex-col hidden md:flex">
<div class="p-4 border-b border-gray-200 bg-white flex justify-between items-center">
<h2 class="font-bold text-gray-800 text-sm flex items-center gap-2">
<i data-lucide="file-text" class="w-4 h-4 text-gray-500"></i>
方案状态 (Context)
</h2>
<span class="text-[10px] bg-green-100 text-green-700 px-2 py-0.5 rounded-full font-medium">Synced</span>
</div>
<div class="flex-1 overflow-y-auto p-4 space-y-4">
<!-- Section 1: Scientific Question -->
<div class="bg-white p-3 rounded-lg border border-gray-200 shadow-sm">
<h3 class="text-xs font-bold text-gray-400 uppercase mb-2 tracking-wider">01 科学问题</h3>
<p class="text-xs text-gray-700 leading-relaxed font-medium">
阿司匹林预防老年高血压患者中风的疗效与安全性研究
</p>
</div>
<!-- Section 2: PICO -->
<div class="bg-white p-3 rounded-lg border border-gray-200 shadow-sm">
<div class="flex justify-between items-center mb-2">
<h3 class="text-xs font-bold text-gray-400 uppercase tracking-wider">02 PICO</h3>
<button class="text-[10px] text-indigo-600 hover:underline">编辑</button>
</div>
<div class="space-y-2">
<div class="flex gap-2">
<span class="bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded text-[10px] font-bold w-6 text-center">P</span>
<span class="text-xs text-gray-600">≥65岁原发性高血压患者</span>
</div>
<div class="flex gap-2">
<span class="bg-indigo-100 text-indigo-700 px-1.5 py-0.5 rounded text-[10px] font-bold w-6 text-center">I</span>
<span class="text-xs text-gray-600">阿司匹林肠溶片 100mg/d</span>
</div>
<div class="flex gap-2">
<span class="bg-orange-100 text-orange-700 px-1.5 py-0.5 rounded text-[10px] font-bold w-6 text-center">C</span>
<span class="text-xs text-gray-600">安慰剂</span>
</div>
<div class="flex gap-2">
<span class="bg-green-100 text-green-700 px-1.5 py-0.5 rounded text-[10px] font-bold w-6 text-center">O</span>
<span class="text-xs text-gray-600">5年内缺血性脑卒中发生率</span>
</div>
</div>
</div>
<!-- Section 3: Study Design -->
<div class="bg-white p-3 rounded-lg border border-gray-200 shadow-sm">
<h3 class="text-xs font-bold text-gray-400 uppercase mb-2 tracking-wider">03 研究设计</h3>
<div class="flex flex-wrap gap-2">
<span class="text-[10px] border border-gray-200 px-2 py-1 rounded bg-gray-50 text-gray-600">RCT</span>
<span class="text-[10px] border border-gray-200 px-2 py-1 rounded bg-gray-50 text-gray-600">双盲</span>
<span class="text-[10px] border border-gray-200 px-2 py-1 rounded bg-gray-50 text-gray-600">多中心</span>
</div>
</div>
<!-- Section 4: Sample Size (The Active One) -->
<div id="state-sample-size" class="bg-white p-3 rounded-lg border-2 border-indigo-100 shadow-sm transition-colors duration-500">
<h3 class="text-xs font-bold text-indigo-600 uppercase mb-2 tracking-wider flex justify-between">
04 样本量
<i data-lucide="loader-2" class="w-3 h-3 animate-spin"></i>
</h3>
<div class="flex items-center justify-between">
<span class="text-xs text-gray-500">总样本量 (N)</span>
<span id="n-value-display" class="text-lg font-bold text-gray-300 font-mono">待计算...</span>
</div>
</div>
</div>
</aside>
</main>
<!-- Modal: Simulated External Tool (Deep Link Destination) -->
<div id="tool-modal" class="fixed inset-0 bg-black/50 hidden z-50 flex items-center justify-center backdrop-blur-sm opacity-0 transition-opacity duration-300">
<div class="bg-white rounded-xl shadow-2xl w-[600px] overflow-hidden transform scale-95 transition-transform duration-300" id="tool-modal-content">
<!-- Tool Header -->
<div class="bg-gray-800 text-white p-4 flex justify-between items-center">
<div class="flex items-center gap-2">
<i data-lucide="calculator" class="w-5 h-5 text-indigo-400"></i>
<span class="font-bold">统计工具箱 v3.0 | 样本量计算器</span>
</div>
<button onclick="closeToolModal()" class="text-gray-400 hover:text-white">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<!-- Tool Body (Simulated Form) -->
<div class="p-6 bg-gray-50">
<div class="bg-blue-50 border border-blue-200 text-blue-800 text-xs p-3 rounded mb-4 flex gap-2">
<i data-lucide="info" class="w-4 h-4 shrink-0"></i>
已从 Protocol Agent 自动填充参数。修改参数后请点击“重新计算”。
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-xs font-bold text-gray-500 mb-1">检验类型</label>
<select class="w-full text-sm border-gray-300 rounded p-2 border">
<option>双侧两样本均数比较</option>
<option>卡方检验</option>
</select>
</div>
<div>
<label class="block text-xs font-bold text-gray-500 mb-1">检验水准 (α)</label>
<input type="number" value="0.05" class="w-full text-sm border-gray-300 rounded p-2 border">
</div>
<div>
<label class="block text-xs font-bold text-gray-500 mb-1">把握度 (1-β)</label>
<input type="number" value="0.80" class="w-full text-sm border-gray-300 rounded p-2 border">
</div>
<div>
<label class="block text-xs font-bold text-gray-500 mb-1">脱落率 (%)</label>
<input type="number" value="10" class="w-full text-sm border-gray-300 rounded p-2 border">
</div>
</div>
<!-- Effect Size Params -->
<div class="bg-white p-3 rounded border border-gray-200 mb-4">
<h4 class="text-xs font-bold text-gray-800 mb-2">效应量参数 (Effect Size)</h4>
<div class="flex gap-4">
<div class="flex-1">
<label class="block text-[10px] text-gray-400">实验组预估发生率</label>
<input type="number" value="0.10" class="w-full font-mono text-sm border-gray-300 rounded p-1 border">
</div>
<div class="flex-1">
<label class="block text-[10px] text-gray-400">对照组预估发生率</label>
<input type="number" value="0.15" class="w-full font-mono text-sm border-gray-300 rounded p-1 border">
</div>
</div>
</div>
<!-- Result Area -->
<div class="flex items-center justify-between bg-gray-100 p-4 rounded-lg mb-6">
<span class="text-sm font-bold text-gray-600">计算结果 (N):</span>
<span id="tool-result" class="text-2xl font-bold text-indigo-600 font-mono">---</span>
</div>
<!-- Actions -->
<div class="flex justify-end gap-3">
<button onclick="calculate()" class="px-4 py-2 bg-white border border-gray-300 text-gray-700 text-sm font-medium rounded hover:bg-gray-50">
试算
</button>
<button id="btn-sync" onclick="syncAndClose()" disabled class="px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2">
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
确认并同步到方案
</button>
</div>
</div>
</div>
</div>
<script>
// 初始化 Lucide 图标
lucide.createIcons();
// Modal 控制逻辑
const modal = document.getElementById('tool-modal');
const modalContent = document.getElementById('tool-modal-content');
const btnSync = document.getElementById('btn-sync');
const toolResult = document.getElementById('tool-result');
function openToolModal() {
modal.classList.remove('hidden');
// 简单的淡入动画
setTimeout(() => {
modal.classList.remove('opacity-0');
modalContent.classList.remove('scale-95');
}, 10);
}
function closeToolModal() {
modal.classList.add('opacity-0');
modalContent.classList.add('scale-95');
setTimeout(() => {
modal.classList.add('hidden');
}, 300);
}
// 模拟计算过程
function calculate() {
toolResult.innerText = "Calculing...";
setTimeout(() => {
toolResult.innerText = "386";
btnSync.disabled = false;
btnSync.classList.add('animate-pulse');
}, 600);
}
// 核心:同步回写逻辑 (Reflexion Loop)
function syncAndClose() {
closeToolModal();
// 1. 更新右侧状态面板 (State Panel Update)
const stateCard = document.getElementById('state-sample-size');
const nValueDisplay = document.getElementById('n-value-display');
// 视觉反馈:闪烁一下
stateCard.classList.add('flash-update');
stateCard.classList.remove('border-indigo-100');
stateCard.classList.add('border-green-500'); // 变成绿色边框
// 移除 Loading 图标
stateCard.querySelector('i').remove();
// 更新数值
setTimeout(() => {
nValueDisplay.innerText = "N = 386";
nValueDisplay.classList.remove('text-gray-300');
nValueDisplay.classList.add('text-indigo-600');
}, 500);
// 2. 更新聊天界面 (Chat Update with Reflexion)
const chatContainer = document.getElementById('chat-container');
// 插入系统消息
const systemMsgHTML = `
<div class="flex justify-center my-4">
<span class="text-[10px] bg-gray-100 text-gray-500 px-2 py-1 rounded-full border border-gray-200">
<i data-lucide="check-circle" class="w-3 h-3 inline mr-1 text-green-500"></i>
系统消息: 用户已同步样本量结果 N=386
</span>
</div>
`;
chatContainer.insertAdjacentHTML('beforeend', systemMsgHTML);
// 模拟 AI 思考 (Reflexion)
setTimeout(() => {
const aiReflexionHTML = `
<div class="flex gap-4 animate-fade-in-up">
<div class="w-8 h-8 rounded-full bg-purple-100 flex items-center justify-center shrink-0 border border-purple-200">
<i data-lucide="check-check" class="w-4 h-4 text-purple-600"></i>
</div>
<div class="space-y-1 max-w-[80%]">
<div class="text-xs text-purple-500 ml-1 font-bold">Reflexion Guard • 刚刚</div>
<div class="chat-bubble-ai p-4 text-sm text-gray-800 shadow-sm reflexion-border">
<p class="font-bold text-purple-700 text-xs mb-1 mb-2 flex items-center gap-1">
<i data-lucide="shield-check" class="w-3 h-3"></i>
质量校验通过
</p>
<p>我已校验您的计算结果:</p>
<ul class="list-disc list-inside mt-1 text-gray-600 text-xs space-y-1">
<li>N=386 满足 RCT 设计对统计效能 (Power=0.8) 的要求。</li>
<li>参数设置符合心血管预防研究的常规标准。</li>
</ul>
<p class="mt-3">数值已同步到方案中。接下来,建议我们进行<strong>第五步CRF (病例报告表) 设计</strong>。</p>
</div>
</div>
</div>
`;
chatContainer.insertAdjacentHTML('beforeend', aiReflexionHTML);
lucide.createIcons(); // 重新渲染新插入的图标
// 自动滚动到底部
chatContainer.scrollTop = chatContainer.scrollHeight;
}, 1000);
}
// 点击遮罩关闭
modal.addEventListener('click', (e) => {
if (e.target === modal) closeToolModal();
});
</script>
</body>
</html>