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,8 @@
/**
* Protocol Agent Hooks Export
*/
export { useProtocolContext } from './useProtocolContext';
export { useProtocolConversations } from './useProtocolConversations';

View File

@@ -0,0 +1,79 @@
/**
* useProtocolContext Hook
* 管理Protocol Agent的上下文状态
*/
import { useState, useEffect, useCallback } from 'react';
import type { ProtocolContext } from '../types';
import { getAccessToken } from '../../../../framework/auth/api';
const API_BASE = '/api/v1/aia/protocol-agent';
export function useProtocolContext(conversationId?: string) {
const [context, setContext] = useState<ProtocolContext | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
/**
* 获取上下文状态
*/
const fetchContext = useCallback(async () => {
if (!conversationId) {
setContext(null);
return;
}
setLoading(true);
setError(null);
try {
const token = getAccessToken();
const response = await fetch(`${API_BASE}/context/${conversationId}`, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (response.status === 404) {
// 上下文不存在,返回空
setContext(null);
return;
}
if (!response.ok) {
throw new Error(`Failed to fetch context: ${response.statusText}`);
}
const result = await response.json();
if (result.success) {
setContext(result.data);
}
} catch (err) {
console.error('[useProtocolContext] fetchContext error:', err);
setError(err instanceof Error ? err.message : 'Failed to load context');
} finally {
setLoading(false);
}
}, [conversationId]);
/**
* 刷新上下文
*/
const refreshContext = useCallback(() => {
fetchContext();
}, [fetchContext]);
// 初次加载和conversationId变化时获取上下文
useEffect(() => {
fetchContext();
}, [fetchContext]);
return {
context,
loading,
error,
refreshContext,
};
}

View File

@@ -0,0 +1,144 @@
/**
* useProtocolConversations Hook
* 管理Protocol Agent的会话列表
*/
import { useState, useEffect, useCallback } from 'react';
import type { ProtocolConversation } from '../types';
import { getAccessToken } from '../../../../framework/auth/api';
const API_BASE = '/api/v1/aia';
export function useProtocolConversations(initialConversationId?: string) {
const [conversations, setConversations] = useState<ProtocolConversation[]>([]);
const [currentConversation, setCurrentConversation] = useState<ProtocolConversation | null>(null);
const [loading, setLoading] = useState(false);
/**
* 获取会话列表
*/
const fetchConversations = useCallback(async () => {
setLoading(true);
try {
const token = getAccessToken();
const response = await fetch(`${API_BASE}/conversations`, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error('Failed to fetch conversations');
}
const result = await response.json();
const allConversations = result.data || result;
// 过滤出Protocol Agent的对话agentId为PROTOCOL_AGENT
const protocolConversations = Array.isArray(allConversations)
? allConversations.filter((conv: any) =>
conv.agentId === 'PROTOCOL_AGENT' || conv.agent_id === 'PROTOCOL_AGENT'
)
: [];
setConversations(protocolConversations);
// 如果有initialConversationId设置为当前对话
if (initialConversationId) {
const current = protocolConversations.find((c: any) => c.id === initialConversationId);
if (current) {
setCurrentConversation(current);
}
}
} catch (err) {
console.error('[useProtocolConversations] fetchConversations error:', err);
} finally {
setLoading(false);
}
}, [initialConversationId]);
/**
* 创建新对话
*/
const createConversation = useCallback(async (): Promise<ProtocolConversation | null> => {
try {
const token = getAccessToken();
const response = await fetch(`${API_BASE}/conversations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
agentId: 'PROTOCOL_AGENT',
title: `研究方案-${new Date().toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`,
}),
});
if (!response.ok) {
throw new Error('Failed to create conversation');
}
const result = await response.json();
const newConv = result.data || result;
setConversations(prev => [newConv, ...prev]);
setCurrentConversation(newConv);
return newConv;
} catch (err) {
console.error('[useProtocolConversations] createConversation error:', err);
return null;
}
}, []);
/**
* 选择对话
*/
const selectConversation = useCallback((id: string) => {
const conv = conversations.find(c => c.id === id);
if (conv) {
setCurrentConversation(conv);
}
}, [conversations]);
/**
* 删除对话
*/
const deleteConversation = useCallback(async (id: string) => {
try {
const token = getAccessToken();
await fetch(`${API_BASE}/conversations/${id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
},
});
setConversations(prev => prev.filter(c => c.id !== id));
if (currentConversation?.id === id) {
setCurrentConversation(null);
}
} catch (err) {
console.error('[useProtocolConversations] deleteConversation error:', err);
}
}, [currentConversation]);
// 初次加载
useEffect(() => {
fetchConversations();
}, [fetchConversations]);
return {
conversations,
currentConversation,
loading,
createConversation,
selectConversation,
deleteConversation,
refreshConversations: fetchConversations,
};
}