|
@@ -73,6 +73,26 @@ interface ChatPayload {
|
|
|
message: string;
|
|
message: string;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ─── n8n intelligent-response schema ────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+interface ChatVisualDataset {
|
|
|
|
|
+ label: string;
|
|
|
|
|
+ data: number[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface ChatVisualData {
|
|
|
|
|
+ type: string;
|
|
|
|
|
+ title: string;
|
|
|
|
|
+ labels: string[];
|
|
|
|
|
+ datasets: ChatVisualDataset[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface ChatResponse {
|
|
|
|
|
+ text: string;
|
|
|
|
|
+ has_visuals: boolean;
|
|
|
|
|
+ visual_data: ChatVisualData | null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
interface HistoryDeletePayload {
|
|
interface HistoryDeletePayload {
|
|
|
archiveId: string;
|
|
archiveId: string;
|
|
|
}
|
|
}
|
|
@@ -95,6 +115,53 @@ interface EdgeResultPayload {
|
|
|
|
|
|
|
|
const N8N_WEBHOOK_URL = process.env['N8N_WEBHOOK_URL'] ?? '';
|
|
const N8N_WEBHOOK_URL = process.env['N8N_WEBHOOK_URL'] ?? '';
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * n8n's AI agent node is only allowed to emit text — the enforced JSON schema
|
|
|
|
|
+ * (text/has_visuals/visual_data) arrives as a *string* inside `output` (or
|
|
|
|
|
+ * `text`/`message`, depending on node config), sometimes fenced in ```json.
|
|
|
|
|
+ * This normalizes whatever shape comes back into a strict ChatResponse.
|
|
|
|
|
+ */
|
|
|
|
|
+function normalizeChatResponse(raw: any): ChatResponse {
|
|
|
|
|
+ const node = Array.isArray(raw) ? raw[0] : raw;
|
|
|
|
|
+
|
|
|
|
|
+ const candidate =
|
|
|
|
|
+ node && typeof node === 'object'
|
|
|
|
|
+ ? (node.text !== undefined || node.has_visuals !== undefined
|
|
|
|
|
+ ? node
|
|
|
|
|
+ : (node.output ?? node.message ?? node))
|
|
|
|
|
+ : node;
|
|
|
|
|
+
|
|
|
|
|
+ let parsed: any = candidate;
|
|
|
|
|
+ if (typeof candidate === 'string') {
|
|
|
|
|
+ const unfenced = candidate.trim().replace(/^```json\s*|```$/g, '').trim();
|
|
|
|
|
+ try {
|
|
|
|
|
+ parsed = JSON.parse(unfenced);
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ parsed = { text: candidate };
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!parsed || typeof parsed !== 'object') {
|
|
|
|
|
+ parsed = { text: String(parsed ?? '') };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const visualData: ChatVisualData | null =
|
|
|
|
|
+ parsed.has_visuals && parsed.visual_data
|
|
|
|
|
+ ? {
|
|
|
|
|
+ type: String(parsed.visual_data.type ?? 'bar'),
|
|
|
|
|
+ title: String(parsed.visual_data.title ?? ''),
|
|
|
|
|
+ labels: Array.isArray(parsed.visual_data.labels) ? parsed.visual_data.labels : [],
|
|
|
|
|
+ datasets: Array.isArray(parsed.visual_data.datasets) ? parsed.visual_data.datasets : [],
|
|
|
|
|
+ }
|
|
|
|
|
+ : null;
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ text: String(parsed.text ?? parsed.output ?? parsed.message ?? ''),
|
|
|
|
|
+ has_visuals: Boolean(parsed.has_visuals && visualData),
|
|
|
|
|
+ visual_data: visualData,
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ─── Gateway ──────────────────────────────────────────────────────────────────
|
|
// ─── Gateway ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
@WebSocketGateway({
|
|
@WebSocketGateway({
|
|
@@ -297,7 +364,7 @@ export class VisionGateway
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const raw = await response.json();
|
|
const raw = await response.json();
|
|
|
- const chatResult = Array.isArray(raw) ? raw[0] : raw;
|
|
|
|
|
|
|
+ const chatResult = normalizeChatResponse(raw);
|
|
|
reply(chatResult);
|
|
reply(chatResult);
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|