Farm2Market / src /liveAgent.ts
pixel3user
Update Farm2Market demo frontend
e46c4bd
import { callVoiceTool, sendVoiceAssistantMessage } from './liveApi';
import type { LiveVoiceToolResponse } from './liveApi';
import type { DemoCard, DemoLocale, DemoTabId, MockAgentResult, ProgressStep, ProgressStepStatus, TraceItem } from './types';
type RunLiveAgentParams = {
tab: DemoTabId;
input: string;
locale: DemoLocale;
onWorkflowInit: (steps: ProgressStep[]) => void;
onStepStatus: (stepId: string, status: ProgressStepStatus, detail?: string) => void;
onTrace: (item: TraceItem) => void;
};
function nowLabel(): string {
return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
let sequence = 0;
function nextId(prefix: string): string {
sequence += 1;
return `${prefix}-${Date.now().toString(36)}-${sequence.toString(36)}`;
}
function trace(kind: TraceItem['kind'], title: string, detail: string, status: TraceItem['status'], payload?: Record<string, unknown>): TraceItem {
return {
id: nextId('trace'),
kind,
title,
detail,
status,
payload,
timestamp: nowLabel(),
};
}
function uiActions(ui: Record<string, unknown>): string[] {
const actions = Array.isArray(ui.actions) ? ui.actions : [];
return actions
.map((entry) => {
if (typeof entry !== 'object' || !entry) return '';
const label = (entry as { label?: unknown }).label;
return typeof label === 'string' ? label : '';
})
.filter(Boolean)
.slice(0, 4);
}
function toMetric(label: string, value: unknown): { label: string; value: string } {
return { label, value: typeof value === 'number' ? String(value) : typeof value === 'string' ? value : '-' };
}
function uiToCards(ui: Record<string, unknown> | undefined, locale: DemoLocale): DemoCard[] {
if (!ui) return [];
const kind = typeof ui.kind === 'string' ? ui.kind : '';
const title = typeof ui.title === 'string' ? ui.title : locale === 'zh-TW' ? '工具結果' : 'Tool Output';
const description = typeof ui.description === 'string' ? ui.description : undefined;
const actions = uiActions(ui);
if (kind === 'stats' || kind === 'marketplace_stats') {
const items = Array.isArray(ui.items) ? ui.items : [];
const metrics = items
.map((item) => {
if (typeof item !== 'object' || !item) return null;
const label = (item as { label?: unknown }).label;
const value = (item as { value?: unknown }).value;
return typeof label === 'string' ? toMetric(label, value) : null;
})
.filter((metric): metric is { label: string; value: string } => Boolean(metric))
.slice(0, 6);
return [{ title, subtitle: description, metrics, actions }];
}
if (kind === 'product_created' || kind === 'product_updated') {
const product = typeof ui.product === 'object' && ui.product ? (ui.product as Record<string, unknown>) : {};
const metrics = [
toMetric(locale === 'zh-TW' ? '價格' : 'Price', product.price),
toMetric(locale === 'zh-TW' ? '庫存' : 'Stock', product.stock),
toMetric(locale === 'zh-TW' ? '分類' : 'Category', product.category),
];
return [
{
title,
subtitle: typeof product.name === 'string' ? product.name : description,
metrics,
actions,
},
];
}
if (kind === 'product_list' || kind === 'store_list' || kind === 'marketing_list') {
const items = Array.isArray(ui.items) ? ui.items : [];
const cards = items.slice(0, 3).map((item, index) => {
if (typeof item !== 'object' || !item) {
return { title: `${title} ${index + 1}` } as DemoCard;
}
const row = item as Record<string, unknown>;
const rowTitle =
typeof row.name === 'string'
? row.name
: typeof row.title === 'string'
? row.title
: `${title} ${index + 1}`;
const metrics: Array<{ label: string; value: string }> = [];
if (row.price !== undefined) metrics.push(toMetric(locale === 'zh-TW' ? '價格' : 'Price', row.price));
if (row.stock !== undefined) metrics.push(toMetric(locale === 'zh-TW' ? '庫存' : 'Stock', row.stock));
if (row.location !== undefined) metrics.push(toMetric(locale === 'zh-TW' ? '地區' : 'Location', row.location));
if (row.type !== undefined) metrics.push(toMetric(locale === 'zh-TW' ? '類型' : 'Type', row.type));
return {
title: rowTitle,
subtitle: typeof row.description === 'string' ? row.description : undefined,
metrics: metrics.length ? metrics : undefined,
} as DemoCard;
});
return cards.length ? cards : [{ title, subtitle: description, actions }];
}
return [{ title, subtitle: description, actions }];
}
function assistantSummary(messages: Array<{ role?: unknown; text?: unknown }>, locale: DemoLocale): string {
const textList = messages
.filter((message) => message && message.role === 'assistant' && typeof message.text === 'string')
.map((message) => String(message.text).trim())
.filter(Boolean);
if (!textList.length) {
return locale === 'zh-TW' ? '工具流程已完成。' : 'Tool workflow finished.';
}
return textList[textList.length - 1];
}
export async function runLiveAgent(params: RunLiveAgentParams): Promise<MockAgentResult> {
const { tab, input, locale, onWorkflowInit, onStepStatus, onTrace } = params;
if (tab !== 'chat') {
throw new Error('Live agent is currently supported for chat tab only.');
}
const steps: ProgressStep[] = [
{
id: 'live-step-1',
label: locale === 'zh-TW' ? '呼叫語音助理後端' : 'Call voice assistant backend',
detail: locale === 'zh-TW' ? '傳送需求並等待回覆。' : 'Submitting request and waiting for response.',
status: 'pending',
},
{
id: 'live-step-2',
label: locale === 'zh-TW' ? '補充工具資料' : 'Fetch supplemental tool data',
detail: locale === 'zh-TW' ? '取得即時市集統計。' : 'Fetching live marketplace stats.',
status: 'pending',
},
{
id: 'live-step-3',
label: locale === 'zh-TW' ? '渲染回覆卡片' : 'Render response cards',
detail: locale === 'zh-TW' ? '整理訊息與結構化輸出。' : 'Formatting messages and structured payload.',
status: 'pending',
},
];
onWorkflowInit(steps);
onStepStatus(steps[0].id, 'in_progress', steps[0].detail);
onTrace(
trace(
'planner',
locale === 'zh-TW' ? '聊天請求' : 'Chat request',
locale === 'zh-TW' ? '正在呼叫 /api/voice/assistant。' : 'Calling /api/voice/assistant.',
'running'
)
);
const assistant = await sendVoiceAssistantMessage({
message: input,
locale,
pageRoute: '/hf-demo/chat',
pageContext: locale === 'zh-TW' ? 'Hugging Face Chat Agent Demo' : 'Hugging Face Chat Agent Demo',
});
onStepStatus(steps[0].id, 'completed', locale === 'zh-TW' ? '已收到助理回覆。' : 'Assistant response received.');
onTrace(
trace(
'planner',
locale === 'zh-TW' ? '聊天回覆' : 'Chat response',
locale === 'zh-TW' ? '語音助理回覆已到達。' : 'Voice assistant returned output.',
'ok',
{ messageCount: assistant.messages?.length ?? 0, uiKind: assistant.ui?.kind || null }
)
);
onStepStatus(steps[1].id, 'in_progress', steps[1].detail);
onTrace(
trace(
'tool',
locale === 'zh-TW' ? '補充工具查詢' : 'Supplemental tool query',
locale === 'zh-TW' ? '呼叫 marketplace_get_stats。' : 'Calling marketplace_get_stats.',
'running'
)
);
let statsPayload: LiveVoiceToolResponse | null = null;
try {
statsPayload = await callVoiceTool({
name: 'marketplace_get_stats',
locale,
});
onStepStatus(steps[1].id, 'completed', locale === 'zh-TW' ? '即時統計已取得。' : 'Live stats fetched.');
onTrace(
trace(
'tool',
locale === 'zh-TW' ? '統計工具結果' : 'Stats tool result',
locale === 'zh-TW' ? '市集統計已附加。' : 'Marketplace stats attached.',
'ok',
{ uiKind: statsPayload.ui?.kind || null }
)
);
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown live tool error';
onStepStatus(steps[1].id, 'error', message);
onTrace(
trace(
'tool',
locale === 'zh-TW' ? '統計工具失敗' : 'Stats tool failed',
message,
'error'
)
);
}
onStepStatus(steps[2].id, 'in_progress', steps[2].detail);
onTrace(
trace(
'renderer',
locale === 'zh-TW' ? '結果渲染' : 'Result rendering',
locale === 'zh-TW' ? '正在整理卡片與輸出。' : 'Formatting cards and payload.',
'running'
)
);
const cards = [
...uiToCards(assistant.ui as Record<string, unknown> | undefined, locale),
...uiToCards(statsPayload?.ui as Record<string, unknown> | undefined, locale),
];
const summary = assistantSummary(assistant.messages || [], locale);
const payload: Record<string, unknown> = {
assistant,
statsPayload,
source: 'live-backend',
};
onStepStatus(steps[2].id, 'completed', locale === 'zh-TW' ? '即時回覆已完成。' : 'Live response rendered.');
onTrace(
trace(
'renderer',
locale === 'zh-TW' ? '渲染完成' : 'Rendering complete',
locale === 'zh-TW' ? '回覆內容與載荷已就緒。' : 'Response and payload are ready.',
'ok',
{ cards: cards.length }
)
);
return {
summary,
cards,
payload,
};
}