feat: Add Expert Transformation Agent with multi-expert perspective system

- Backend: Add expert transformation router with 3-step SSE pipeline
  - Step 0: Generate diverse expert team (random domains)
  - Step 1: Each expert generates keywords for attributes
  - Step 2: Batch generate descriptions for expert keywords
- Backend: Add simplified prompts for reliable JSON output
- Frontend: Add TransformationPanel with React Flow visualization
- Frontend: Add TransformationInputPanel for expert configuration
  - Expert count (2-8), keywords per expert (1-3)
  - Custom expert domains support
- Frontend: Add expert keyword nodes with expert badges
- Frontend: Improve description card layout (wider cards, more spacing)
- Frontend: Add fallback for missing descriptions with visual indicators

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-03 16:26:17 +08:00
parent 1ed1dab78f
commit 534fdbbcc4
25 changed files with 3114 additions and 27 deletions

View File

@@ -5,7 +5,12 @@ import type {
Step0Result,
CategoryDefinition,
DynamicStep1Result,
DAGStreamAnalyzeResponse
DAGStreamAnalyzeResponse,
TransformationRequest,
TransformationCategoryResult,
ExpertTransformationRequest,
ExpertTransformationCategoryResult,
ExpertProfile
} from '../types';
// 自動使用當前瀏覽器的 hostname支援遠端存取
@@ -114,3 +119,183 @@ export async function getModels(): Promise<ModelListResponse> {
return response.json();
}
// ===== Transformation Agent API =====
export interface TransformationSSECallbacks {
onKeywordStart?: () => void;
onKeywordComplete?: (keywords: string[]) => void;
onDescriptionStart?: () => void;
onDescriptionComplete?: (count: number) => void;
onDone?: (result: TransformationCategoryResult) => void;
onError?: (error: string) => void;
}
export async function transformCategoryStream(
request: TransformationRequest,
callbacks: TransformationSSECallbacks
): Promise<void> {
const response = await fetch(`${API_BASE_URL}/transformation/category`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('No response body');
}
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 解析 SSE 事件
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
for (const chunk of lines) {
if (!chunk.trim()) continue;
const eventMatch = chunk.match(/event: (\w+)/);
const dataMatch = chunk.match(/data: (.+)/s);
if (eventMatch && dataMatch) {
const eventType = eventMatch[1];
try {
const eventData = JSON.parse(dataMatch[1]);
switch (eventType) {
case 'keyword_start':
callbacks.onKeywordStart?.();
break;
case 'keyword_complete':
callbacks.onKeywordComplete?.(eventData.keywords);
break;
case 'description_start':
callbacks.onDescriptionStart?.();
break;
case 'description_complete':
callbacks.onDescriptionComplete?.(eventData.count);
break;
case 'done':
callbacks.onDone?.(eventData.result);
break;
case 'error':
callbacks.onError?.(eventData.error);
break;
}
} catch (e) {
console.error('Failed to parse SSE event:', e, chunk);
}
}
}
}
}
// ===== Expert Transformation Agent API =====
export interface ExpertTransformationSSECallbacks {
onExpertStart?: () => void;
onExpertComplete?: (experts: ExpertProfile[]) => void;
onKeywordStart?: () => void;
onKeywordProgress?: (data: { attribute: string; count: number }) => void;
onKeywordComplete?: (totalKeywords: number) => void;
onDescriptionStart?: () => void;
onDescriptionComplete?: (count: number) => void;
onDone?: (data: { result: ExpertTransformationCategoryResult; experts: ExpertProfile[] }) => void;
onError?: (error: string) => void;
}
export async function expertTransformCategoryStream(
request: ExpertTransformationRequest,
callbacks: ExpertTransformationSSECallbacks
): Promise<void> {
const response = await fetch(`${API_BASE_URL}/expert-transformation/category`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('No response body');
}
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 解析 SSE 事件
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
for (const chunk of lines) {
if (!chunk.trim()) continue;
const eventMatch = chunk.match(/event: (\w+)/);
const dataMatch = chunk.match(/data: (.+)/s);
if (eventMatch && dataMatch) {
const eventType = eventMatch[1];
try {
const eventData = JSON.parse(dataMatch[1]);
switch (eventType) {
case 'expert_start':
callbacks.onExpertStart?.();
break;
case 'expert_complete':
callbacks.onExpertComplete?.(eventData.experts);
break;
case 'keyword_start':
callbacks.onKeywordStart?.();
break;
case 'keyword_progress':
callbacks.onKeywordProgress?.(eventData);
break;
case 'keyword_complete':
callbacks.onKeywordComplete?.(eventData.total_keywords);
break;
case 'description_start':
callbacks.onDescriptionStart?.();
break;
case 'description_complete':
callbacks.onDescriptionComplete?.(eventData.count);
break;
case 'done':
callbacks.onDone?.(eventData);
break;
case 'error':
callbacks.onError?.(eventData.error);
break;
}
} catch (e) {
console.error('Failed to parse SSE event:', e, chunk);
}
}
}
}
}