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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user