voyageai-cli 1.30.0 → 1.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +6 -0
- package/src/commands/chat.js +32 -11
- package/src/commands/export.js +124 -0
- package/src/commands/import.js +195 -0
- package/src/commands/index-workspace.js +239 -0
- package/src/commands/mcp-server.js +113 -3
- package/src/commands/playground.js +111 -3
- package/src/lib/export/contexts/benchmark-export.js +27 -0
- package/src/lib/export/contexts/chat-export.js +41 -0
- package/src/lib/export/contexts/explore-export.js +22 -0
- package/src/lib/export/contexts/search-export.js +54 -0
- package/src/lib/export/contexts/workflow-export.js +80 -0
- package/src/lib/export/formats/clipboard-export.js +29 -0
- package/src/lib/export/formats/csv-export.js +45 -0
- package/src/lib/export/formats/json-export.js +50 -0
- package/src/lib/export/formats/markdown-export.js +189 -0
- package/src/lib/export/formats/mermaid-export.js +274 -0
- package/src/lib/export/formats/pdf-export.js +117 -0
- package/src/lib/export/formats/png-export.js +96 -0
- package/src/lib/export/formats/svg-export.js +116 -0
- package/src/lib/export/index.js +175 -0
- package/src/lib/workflow.js +206 -27
- package/src/mcp/install.js +280 -7
- package/src/mcp/schemas/index.js +40 -0
- package/src/mcp/server.js +2 -0
- package/src/mcp/tools/workspace.js +463 -0
- package/src/playground/announcements.md +52 -5
- package/src/playground/index.html +11125 -7796
- package/src/playground/vendor/mermaid.min.js +2811 -0
- package/src/workflows/rag-chat.json +165 -0
- package/src/workflows/tests/consistency-check.happy-path.test.json +28 -0
- package/src/workflows/tests/consistency-check.missing-source.test.json +26 -0
- package/src/workflows/tests/cost-analysis.happy-path.test.json +28 -0
- package/src/workflows/tests/enrich-and-ingest.happy-path.test.json +38 -0
- package/src/workflows/tests/enrich-and-ingest.notify-fails.test.json +38 -0
- package/src/workflows/tests/intelligent-ingest.all-filtered.test.json +26 -0
- package/src/workflows/tests/intelligent-ingest.happy-path.test.json +28 -0
- package/src/workflows/tests/kb-health-report.custom-queries.test.json +24 -0
- package/src/workflows/tests/kb-health-report.happy-path.test.json +26 -0
- package/src/workflows/tests/multi-collection-search.happy-path.test.json +40 -0
- package/src/workflows/tests/multi-collection-search.one-empty.test.json +28 -0
- package/src/workflows/tests/rag-chat.happy-path.test.json +26 -0
- package/src/workflows/tests/rag-chat.no-relevant-results.test.json +25 -0
- package/src/workflows/tests/research-and-summarize.happy-path.test.json +33 -0
- package/src/workflows/tests/research-and-summarize.no-results.test.json +29 -0
- package/src/workflows/tests/search-with-fallback.empty-both.test.json +24 -0
- package/src/workflows/tests/search-with-fallback.fallback-branch.test.json +24 -0
- package/src/workflows/tests/search-with-fallback.happy-path.test.json +27 -0
- package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +34 -0
- package/src/workflows/tests/smart-ingest.happy-path.test.json +31 -0
- package/src/playground/assets/announcements/appstore.jpg +0 -0
- package/src/playground/assets/announcements/circuits.jpg +0 -0
- package/src/playground/assets/announcements/csvingest.jpg +0 -0
- package/src/playground/assets/announcements/green-wave.jpg +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Render normalized data as Markdown.
|
|
5
|
+
* The shape of `normalized` varies by context — the context module sets a `_context` key.
|
|
6
|
+
* @param {object} normalized
|
|
7
|
+
* @param {object} options
|
|
8
|
+
* @returns {{ content: string, mimeType: string }}
|
|
9
|
+
*/
|
|
10
|
+
function renderMarkdown(normalized, options = {}) {
|
|
11
|
+
const ctx = normalized._context;
|
|
12
|
+
let md;
|
|
13
|
+
switch (ctx) {
|
|
14
|
+
case 'workflow':
|
|
15
|
+
md = renderWorkflowMd(normalized, options);
|
|
16
|
+
break;
|
|
17
|
+
case 'search':
|
|
18
|
+
md = renderSearchMd(normalized, options);
|
|
19
|
+
break;
|
|
20
|
+
case 'chat':
|
|
21
|
+
md = renderChatMd(normalized, options);
|
|
22
|
+
break;
|
|
23
|
+
case 'benchmark':
|
|
24
|
+
md = renderBenchmarkMd(normalized, options);
|
|
25
|
+
break;
|
|
26
|
+
default:
|
|
27
|
+
md = '# Export\n\n```json\n' + JSON.stringify(normalized, null, 2) + '\n```\n';
|
|
28
|
+
}
|
|
29
|
+
return { content: md, mimeType: 'text/markdown' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Workflow ──
|
|
33
|
+
|
|
34
|
+
function renderWorkflowMd(n, opts) {
|
|
35
|
+
const lines = [];
|
|
36
|
+
lines.push(`# ${n.name || 'Workflow'}`);
|
|
37
|
+
lines.push('');
|
|
38
|
+
if (n.description) {
|
|
39
|
+
lines.push(n.description);
|
|
40
|
+
lines.push('');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Metadata
|
|
44
|
+
if (n.version) lines.push(`**Version:** ${n.version}`);
|
|
45
|
+
const tools = (n.steps || []).map((s) => s.tool).filter(Boolean);
|
|
46
|
+
const unique = [...new Set(tools)];
|
|
47
|
+
if (unique.length) lines.push(`**Tools used:** ${unique.join(', ')}`);
|
|
48
|
+
const layers = n._executionLayers || null;
|
|
49
|
+
if (layers) lines.push(`**Execution layers:** ${layers} (sequential)`);
|
|
50
|
+
lines.push('');
|
|
51
|
+
|
|
52
|
+
// Inputs table
|
|
53
|
+
if (n.inputs && Object.keys(n.inputs).length > 0) {
|
|
54
|
+
lines.push('## Inputs');
|
|
55
|
+
lines.push('');
|
|
56
|
+
lines.push('| Parameter | Type | Required | Default | Description |');
|
|
57
|
+
lines.push('|-----------|------|----------|---------|-------------|');
|
|
58
|
+
for (const [key, schema] of Object.entries(n.inputs)) {
|
|
59
|
+
const type = schema.type || 'string';
|
|
60
|
+
const req = schema.required ? 'Yes' : 'No';
|
|
61
|
+
const def = schema.default !== undefined ? String(schema.default) : '—';
|
|
62
|
+
const desc = schema.description || '';
|
|
63
|
+
lines.push(`| ${key} | ${type} | ${req} | ${def} | ${desc} |`);
|
|
64
|
+
}
|
|
65
|
+
lines.push('');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Steps
|
|
69
|
+
if (n.steps && n.steps.length > 0) {
|
|
70
|
+
lines.push('## Steps');
|
|
71
|
+
lines.push('');
|
|
72
|
+
const deps = n._dependencyMap || {};
|
|
73
|
+
n.steps.forEach((step, i) => {
|
|
74
|
+
const label = step.name || step.description || step.id;
|
|
75
|
+
lines.push(`### ${i + 1}. ${label}`);
|
|
76
|
+
if (step.tool) lines.push(`- **Tool:** ${step.tool}`);
|
|
77
|
+
const stepDeps = deps[step.id];
|
|
78
|
+
if (stepDeps && stepDeps.length > 0) {
|
|
79
|
+
lines.push(`- **Depends on:** ${stepDeps.join(', ')}`);
|
|
80
|
+
} else {
|
|
81
|
+
lines.push('- **Depends on:** (none — first step)');
|
|
82
|
+
}
|
|
83
|
+
if (step.description && step.description !== label) {
|
|
84
|
+
lines.push(`- **Configuration:** ${step.description}`);
|
|
85
|
+
}
|
|
86
|
+
lines.push('');
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Output
|
|
91
|
+
if (n.output) {
|
|
92
|
+
lines.push('## Output');
|
|
93
|
+
lines.push('');
|
|
94
|
+
lines.push(`Returns: \`${n.output}\``);
|
|
95
|
+
lines.push('');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Search Results ──
|
|
102
|
+
|
|
103
|
+
function renderSearchMd(n, opts) {
|
|
104
|
+
const lines = [];
|
|
105
|
+
lines.push('# Search Results');
|
|
106
|
+
lines.push('');
|
|
107
|
+
if (n.query) lines.push(`**Query:** ${n.query}`);
|
|
108
|
+
if (n.collection) lines.push(`**Collection:** ${n.collection}`);
|
|
109
|
+
if (n.model) lines.push(`**Model:** ${n.model}`);
|
|
110
|
+
const total = (n.results || []).length;
|
|
111
|
+
lines.push(`**Results:** ${total}`);
|
|
112
|
+
lines.push('');
|
|
113
|
+
|
|
114
|
+
(n.results || []).forEach((r, i) => {
|
|
115
|
+
lines.push(`### ${i + 1}. ${r.source || 'Result'} (score: ${r.score ?? '—'})`);
|
|
116
|
+
if (r.rerankedScore !== undefined) lines.push(`- **Reranked score:** ${r.rerankedScore}`);
|
|
117
|
+
const text = opts.includeFullText ? r.text : (r.text || '').slice(0, 200);
|
|
118
|
+
if (text) {
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push(`> ${text}`);
|
|
121
|
+
}
|
|
122
|
+
lines.push('');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return lines.join('\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Chat Session ──
|
|
129
|
+
|
|
130
|
+
function renderChatMd(n, opts) {
|
|
131
|
+
const lines = [];
|
|
132
|
+
const sessionId = n.sessionId || n.id || 'unknown';
|
|
133
|
+
lines.push(`# Chat Session: ${sessionId}`);
|
|
134
|
+
lines.push('');
|
|
135
|
+
if (n.startedAt) lines.push(`**Date:** ${n.startedAt}`);
|
|
136
|
+
if (n.provider && n.model) lines.push(`**Provider:** ${n.provider} (${n.model})`);
|
|
137
|
+
else if (n.model) lines.push(`**Model:** ${n.model}`);
|
|
138
|
+
if (n.collection) lines.push(`**Knowledge Base:** ${n.collection}`);
|
|
139
|
+
const turns = (n.turns || []).length;
|
|
140
|
+
lines.push(`**Turns:** ${turns}`);
|
|
141
|
+
lines.push('');
|
|
142
|
+
lines.push('---');
|
|
143
|
+
lines.push('');
|
|
144
|
+
|
|
145
|
+
(n.turns || []).forEach((turn) => {
|
|
146
|
+
const role = turn.role === 'user' ? 'User' : 'vai';
|
|
147
|
+
lines.push(`**${role}:**`);
|
|
148
|
+
lines.push(turn.content || '');
|
|
149
|
+
if (opts.includeSources !== false && turn.context && turn.context.length > 0) {
|
|
150
|
+
lines.push('');
|
|
151
|
+
lines.push('> **Sources:**');
|
|
152
|
+
turn.context.forEach((src, i) => {
|
|
153
|
+
const rel = src.score !== undefined ? ` (relevance: ${src.score})` : '';
|
|
154
|
+
lines.push(`> ${i + 1}. ${src.source || 'unknown'}${rel}`);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
lines.push('');
|
|
158
|
+
lines.push('---');
|
|
159
|
+
lines.push('');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return lines.join('\n');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Benchmark ──
|
|
166
|
+
|
|
167
|
+
function renderBenchmarkMd(n, opts) {
|
|
168
|
+
const lines = [];
|
|
169
|
+
lines.push('# Benchmark Results');
|
|
170
|
+
lines.push('');
|
|
171
|
+
if (n.name) lines.push(`**Benchmark:** ${n.name}`);
|
|
172
|
+
if (n.date) lines.push(`**Date:** ${n.date}`);
|
|
173
|
+
lines.push('');
|
|
174
|
+
|
|
175
|
+
const results = n.results || n.rows || [];
|
|
176
|
+
if (results.length > 0) {
|
|
177
|
+
const cols = Object.keys(results[0]);
|
|
178
|
+
lines.push('| ' + cols.join(' | ') + ' |');
|
|
179
|
+
lines.push('| ' + cols.map(() => '---').join(' | ') + ' |');
|
|
180
|
+
results.forEach((row) => {
|
|
181
|
+
lines.push('| ' + cols.map((c) => String(row[c] ?? '')).join(' | ') + ' |');
|
|
182
|
+
});
|
|
183
|
+
lines.push('');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return lines.join('\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { renderMarkdown };
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
VAI_TOOLS,
|
|
5
|
+
CONTROL_FLOW_TOOLS,
|
|
6
|
+
PROCESSING_TOOLS,
|
|
7
|
+
INTEGRATION_TOOLS,
|
|
8
|
+
buildDependencyGraph,
|
|
9
|
+
} = require('../../workflow');
|
|
10
|
+
|
|
11
|
+
// ════════════════════════════════════════════════════════════════════
|
|
12
|
+
// Tool → Category mapping
|
|
13
|
+
// ════════════════════════════════════════════════════════════════════
|
|
14
|
+
|
|
15
|
+
function getToolCategory(tool) {
|
|
16
|
+
if (!tool) return 'utility';
|
|
17
|
+
if (tool === 'rerank') return 'retrieval';
|
|
18
|
+
if (['query', 'search'].includes(tool)) return 'retrieval';
|
|
19
|
+
if (['embed', 'similarity'].includes(tool)) return 'embedding';
|
|
20
|
+
if (tool === 'generate') return 'generation';
|
|
21
|
+
if (tool === 'ingest') return 'ingestion';
|
|
22
|
+
if (CONTROL_FLOW_TOOLS.has(tool)) return 'control';
|
|
23
|
+
if (PROCESSING_TOOLS.has(tool)) return 'utility';
|
|
24
|
+
if (INTEGRATION_TOOLS.has(tool)) return 'utility';
|
|
25
|
+
return 'utility';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ════════════════════════════════════════════════════════════════════
|
|
29
|
+
// Emoji per category
|
|
30
|
+
// ════════════════════════════════════════════════════════════════════
|
|
31
|
+
|
|
32
|
+
const CATEGORY_EMOJI = {
|
|
33
|
+
retrieval: '🔍',
|
|
34
|
+
embedding: '🧬',
|
|
35
|
+
generation: '✨',
|
|
36
|
+
control: '⚙️',
|
|
37
|
+
utility: '🔧',
|
|
38
|
+
ingestion: '📥',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ════════════════════════════════════════════════════════════════════
|
|
42
|
+
// Mermaid node shape per category (spec Section 7.1)
|
|
43
|
+
// ════════════════════════════════════════════════════════════════════
|
|
44
|
+
|
|
45
|
+
function wrapNodeShape(id, label, category, isConditional) {
|
|
46
|
+
if (isConditional) return `${id}{"${label}"}`;
|
|
47
|
+
switch (category) {
|
|
48
|
+
case 'retrieval': return `${id}(["${label}"])`; // stadium
|
|
49
|
+
case 'embedding': return `${id}("${label}")`; // rounded
|
|
50
|
+
case 'generation': return `${id}[/"${label}"/]`; // trapezoid
|
|
51
|
+
case 'control': return `${id}{"${label}"}`; // diamond
|
|
52
|
+
case 'ingestion': return `${id}[["${label}"]]`; // subroutine
|
|
53
|
+
case 'utility':
|
|
54
|
+
default: return `${id}("${label}")`; // rounded
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ════════════════════════════════════════════════════════════════════
|
|
59
|
+
// Style class definitions (spec Section 7.2)
|
|
60
|
+
// ════════════════════════════════════════════════════════════════════
|
|
61
|
+
|
|
62
|
+
const CLASS_DEFS = {
|
|
63
|
+
retrieval: 'classDef retrieval fill:#00838F,stroke:#00D4AA,color:#fff',
|
|
64
|
+
embedding: 'classDef embedding fill:#4A148C,stroke:#CE93D8,color:#fff',
|
|
65
|
+
generation: 'classDef generation fill:#6A1B9A,stroke:#CE93D8,color:#fff',
|
|
66
|
+
control: 'classDef control fill:#424242,stroke:#9E9E9E,color:#fff',
|
|
67
|
+
utility: 'classDef utility fill:#E65100,stroke:#FFB74D,color:#fff',
|
|
68
|
+
ingestion: 'classDef ingestion fill:#1B5E20,stroke:#66BB6A,color:#fff',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ════════════════════════════════════════════════════════════════════
|
|
72
|
+
// Main conversion
|
|
73
|
+
// ════════════════════════════════════════════════════════════════════
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convert a workflow definition to Mermaid flowchart syntax.
|
|
77
|
+
*
|
|
78
|
+
* @param {object} workflow - Parsed workflow JSON
|
|
79
|
+
* @param {object} [options]
|
|
80
|
+
* @param {string} [options.theme='dark']
|
|
81
|
+
* @param {string} [options.direction='TD']
|
|
82
|
+
* @param {boolean} [options.includeStepIds=false]
|
|
83
|
+
* @param {boolean} [options.includeToolEmoji=true]
|
|
84
|
+
* @param {boolean} [options.colorCoded=true]
|
|
85
|
+
* @param {boolean} [options.includeParallelism=false]
|
|
86
|
+
* @returns {string} Mermaid diagram source
|
|
87
|
+
*/
|
|
88
|
+
function workflowToMermaid(workflow, options = {}) {
|
|
89
|
+
const {
|
|
90
|
+
theme = 'dark',
|
|
91
|
+
direction = 'TD',
|
|
92
|
+
includeStepIds = false,
|
|
93
|
+
includeToolEmoji = true,
|
|
94
|
+
colorCoded = true,
|
|
95
|
+
includeParallelism = false,
|
|
96
|
+
} = options;
|
|
97
|
+
|
|
98
|
+
const steps = workflow.steps || [];
|
|
99
|
+
if (steps.length === 0) return '';
|
|
100
|
+
|
|
101
|
+
const lines = [];
|
|
102
|
+
|
|
103
|
+
// Init directive
|
|
104
|
+
lines.push(`%%{init: {'theme': '${theme}', 'themeVariables': { 'primaryColor': '#00D4AA' }}}%%`);
|
|
105
|
+
lines.push(`graph ${direction}`);
|
|
106
|
+
|
|
107
|
+
// Class definitions
|
|
108
|
+
if (colorCoded) {
|
|
109
|
+
const usedCategories = new Set(steps.map((s) => getToolCategory(s.tool)));
|
|
110
|
+
for (const cat of usedCategories) {
|
|
111
|
+
if (CLASS_DEFS[cat]) lines.push(` ${CLASS_DEFS[cat]}`);
|
|
112
|
+
}
|
|
113
|
+
lines.push('');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Build dependency info — buildDependencyGraph returns Map<string, Set<string>>
|
|
117
|
+
const depGraphMap = buildDependencyGraph(steps);
|
|
118
|
+
// Convert to plain object with arrays for easier iteration
|
|
119
|
+
const depGraph = {};
|
|
120
|
+
for (const [id, deps] of depGraphMap) {
|
|
121
|
+
depGraph[id] = [...deps];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Build execution layers if needed (for subgraph grouping)
|
|
125
|
+
let layers = null;
|
|
126
|
+
if (includeParallelism) {
|
|
127
|
+
layers = buildLayers(steps, depGraph);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Node definitions
|
|
131
|
+
if (layers && includeParallelism) {
|
|
132
|
+
layers.forEach((layerStepIds, i) => {
|
|
133
|
+
if (layerStepIds.length > 1) {
|
|
134
|
+
lines.push(` subgraph "Layer ${i + 1} (parallel)"`);
|
|
135
|
+
for (const sid of layerStepIds) {
|
|
136
|
+
const step = steps.find((s) => s.id === sid);
|
|
137
|
+
lines.push(` ${buildNode(step, { includeStepIds, includeToolEmoji })}`);
|
|
138
|
+
}
|
|
139
|
+
lines.push(' end');
|
|
140
|
+
} else if (layerStepIds.length === 1) {
|
|
141
|
+
const step = steps.find((s) => s.id === layerStepIds[0]);
|
|
142
|
+
lines.push(` ${buildNode(step, { includeStepIds, includeToolEmoji })}`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
for (const step of steps) {
|
|
147
|
+
lines.push(` ${buildNode(step, { includeStepIds, includeToolEmoji })}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
lines.push('');
|
|
152
|
+
|
|
153
|
+
// Edges
|
|
154
|
+
const stepIds = new Set(steps.map((s) => s.id));
|
|
155
|
+
for (const step of steps) {
|
|
156
|
+
const deps = depGraph[step.id] || [];
|
|
157
|
+
|
|
158
|
+
// Also detect implicit deps from condition/forEach that reference step IDs
|
|
159
|
+
const implicitDeps = new Set();
|
|
160
|
+
if (step.condition) {
|
|
161
|
+
for (const sid of stepIds) {
|
|
162
|
+
if (step.condition.includes(sid) && sid !== step.id && !deps.includes(sid)) {
|
|
163
|
+
implicitDeps.add(sid);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (step.forEach) {
|
|
168
|
+
for (const sid of stepIds) {
|
|
169
|
+
if (step.forEach.includes(sid) && sid !== step.id && !deps.includes(sid)) {
|
|
170
|
+
implicitDeps.add(sid);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const dep of deps) {
|
|
176
|
+
const hasCondition = step.condition && step.condition.includes(dep);
|
|
177
|
+
const arrow = hasCondition ? '-.->' : '-->';
|
|
178
|
+
lines.push(` ${dep} ${arrow} ${step.id}`);
|
|
179
|
+
}
|
|
180
|
+
for (const dep of implicitDeps) {
|
|
181
|
+
lines.push(` ${dep} -.-> ${step.id}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Apply class assignments
|
|
186
|
+
if (colorCoded) {
|
|
187
|
+
lines.push('');
|
|
188
|
+
for (const step of steps) {
|
|
189
|
+
const cat = getToolCategory(step.tool);
|
|
190
|
+
lines.push(` class ${step.id} ${cat}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return lines.join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Build a single node definition string.
|
|
199
|
+
*/
|
|
200
|
+
function buildNode(step, opts) {
|
|
201
|
+
const category = getToolCategory(step.tool);
|
|
202
|
+
const isConditional = !!step.condition;
|
|
203
|
+
const isForEach = !!step.forEach;
|
|
204
|
+
|
|
205
|
+
let label = step.name || step.description || step.id;
|
|
206
|
+
if (opts.includeToolEmoji && CATEGORY_EMOJI[category]) {
|
|
207
|
+
label = `${CATEGORY_EMOJI[category]} ${label}`;
|
|
208
|
+
}
|
|
209
|
+
if (opts.includeStepIds) {
|
|
210
|
+
label = `${label} (${step.id})`;
|
|
211
|
+
}
|
|
212
|
+
if (isConditional) {
|
|
213
|
+
const condText = step.condition.length > 30
|
|
214
|
+
? step.condition.slice(0, 27) + '...'
|
|
215
|
+
: step.condition;
|
|
216
|
+
label = `${label}<br/><small>condition: ${condText}</small>`;
|
|
217
|
+
}
|
|
218
|
+
if (isForEach) {
|
|
219
|
+
label = `${label}<br/><small>∀ item in ${step.forEach}</small>`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return wrapNodeShape(step.id, label, category, isConditional);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Build execution layers via topological sort / breadth-first layering.
|
|
227
|
+
*/
|
|
228
|
+
function buildLayers(steps, depGraph) {
|
|
229
|
+
const inDegree = {};
|
|
230
|
+
const stepIds = steps.map((s) => s.id);
|
|
231
|
+
for (const id of stepIds) inDegree[id] = 0;
|
|
232
|
+
for (const [id, deps] of Object.entries(depGraph)) {
|
|
233
|
+
inDegree[id] = (deps || []).length;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const layers = [];
|
|
237
|
+
const remaining = new Set(stepIds);
|
|
238
|
+
|
|
239
|
+
while (remaining.size > 0) {
|
|
240
|
+
const layer = [];
|
|
241
|
+
for (const id of remaining) {
|
|
242
|
+
if ((inDegree[id] || 0) === 0) layer.push(id);
|
|
243
|
+
}
|
|
244
|
+
if (layer.length === 0) break; // cycle guard
|
|
245
|
+
layers.push(layer);
|
|
246
|
+
for (const id of layer) {
|
|
247
|
+
remaining.delete(id);
|
|
248
|
+
// Decrement in-degree of dependents
|
|
249
|
+
for (const [depId, deps] of Object.entries(depGraph)) {
|
|
250
|
+
if (remaining.has(depId) && deps.includes(id)) {
|
|
251
|
+
inDegree[depId]--;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return layers;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Render as Mermaid format for the export system.
|
|
262
|
+
* @param {object} normalized - workflow normalized data
|
|
263
|
+
* @param {object} options
|
|
264
|
+
* @returns {{ content: string, mimeType: string }}
|
|
265
|
+
*/
|
|
266
|
+
function renderMermaid(normalized, options = {}) {
|
|
267
|
+
const mermaid = workflowToMermaid(normalized, options);
|
|
268
|
+
return {
|
|
269
|
+
content: mermaid,
|
|
270
|
+
mimeType: 'text/x-mermaid',
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = { workflowToMermaid, renderMermaid, getToolCategory, buildLayers };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Render a chat session (or any markdown content) as PDF using Playwright.
|
|
5
|
+
*
|
|
6
|
+
* @param {object} normalized
|
|
7
|
+
* @param {object} options
|
|
8
|
+
* @param {string} [options.theme='dark'] - 'dark' | 'light'
|
|
9
|
+
* @returns {Promise<{ content: Buffer, mimeType: string }>}
|
|
10
|
+
*/
|
|
11
|
+
async function renderPdf(normalized, options = {}) {
|
|
12
|
+
if (normalized._context !== 'chat') {
|
|
13
|
+
throw new Error(`PDF export currently supported for chat sessions only (got: ${normalized._context})`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const html = buildChatHtml(normalized, options);
|
|
17
|
+
const pdf = await htmlToPdf(html, options);
|
|
18
|
+
return { content: pdf, mimeType: 'application/pdf' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build styled HTML from a chat session for PDF rendering.
|
|
23
|
+
*/
|
|
24
|
+
function buildChatHtml(chat, options = {}) {
|
|
25
|
+
const theme = options.theme || 'dark';
|
|
26
|
+
const isDark = theme === 'dark';
|
|
27
|
+
const bg = isDark ? '#1e1e1e' : '#ffffff';
|
|
28
|
+
const fg = isDark ? '#e0e0e0' : '#1a1a1a';
|
|
29
|
+
const userBg = isDark ? '#2a2a2a' : '#f5f5f5';
|
|
30
|
+
const asstBg = isDark ? '#1a2a3a' : '#e8f4f8';
|
|
31
|
+
const accent = '#00D4AA';
|
|
32
|
+
|
|
33
|
+
const turns = (chat.turns || []).map((t) => {
|
|
34
|
+
const isUser = t.role === 'user';
|
|
35
|
+
const roleName = isUser ? 'User' : 'vai';
|
|
36
|
+
const msgBg = isUser ? userBg : asstBg;
|
|
37
|
+
const content = escapeHtml(t.content || '').replace(/\n/g, '<br/>');
|
|
38
|
+
|
|
39
|
+
let sources = '';
|
|
40
|
+
if (t.context && t.context.length > 0 && options.includeSources !== false) {
|
|
41
|
+
const srcItems = t.context.map((s, i) => {
|
|
42
|
+
const rel = s.score !== undefined ? ` (relevance: ${s.score})` : '';
|
|
43
|
+
return `<li style="font-size:12px;color:#888;">${escapeHtml(s.source || 'unknown')}${rel}</li>`;
|
|
44
|
+
}).join('');
|
|
45
|
+
sources = `<ul style="margin:4px 0 0 16px;padding:0;">${srcItems}</ul>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let meta = '';
|
|
49
|
+
if (options.includeMetadata && t.metadata) {
|
|
50
|
+
const parts = [];
|
|
51
|
+
if (t.metadata.tokensUsed) parts.push(`${t.metadata.tokensUsed} tokens`);
|
|
52
|
+
if (t.metadata.retrievalTimeMs) parts.push(`retrieval: ${t.metadata.retrievalTimeMs}ms`);
|
|
53
|
+
if (t.metadata.generationTimeMs) parts.push(`generation: ${t.metadata.generationTimeMs}ms`);
|
|
54
|
+
if (parts.length) {
|
|
55
|
+
meta = `<div style="font-size:11px;color:#666;margin-top:4px;">${parts.join(' · ')}</div>`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return `<div style="background:${msgBg};padding:12px 16px;border-radius:8px;margin:8px 0;">
|
|
60
|
+
<div style="font-weight:bold;color:${accent};margin-bottom:6px;">${roleName}</div>
|
|
61
|
+
<div>${content}</div>
|
|
62
|
+
${sources}${meta}
|
|
63
|
+
</div>`;
|
|
64
|
+
}).join('');
|
|
65
|
+
|
|
66
|
+
const sessionId = chat.sessionId || chat.id || '';
|
|
67
|
+
const headerParts = [];
|
|
68
|
+
if (chat.startedAt) headerParts.push(`Date: ${chat.startedAt}`);
|
|
69
|
+
if (chat.provider && chat.model) headerParts.push(`Provider: ${chat.provider} (${chat.model})`);
|
|
70
|
+
else if (chat.model) headerParts.push(`Model: ${chat.model}`);
|
|
71
|
+
if (chat.collection) headerParts.push(`Knowledge Base: ${chat.collection}`);
|
|
72
|
+
headerParts.push(`Turns: ${(chat.turns || []).length}`);
|
|
73
|
+
|
|
74
|
+
return `<!DOCTYPE html>
|
|
75
|
+
<html><head><meta charset="utf-8">
|
|
76
|
+
<style>
|
|
77
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: ${bg}; color: ${fg}; margin: 40px; line-height: 1.6; }
|
|
78
|
+
h1 { color: ${accent}; font-size: 24px; margin-bottom: 4px; }
|
|
79
|
+
.meta { font-size: 13px; color: #888; margin-bottom: 24px; }
|
|
80
|
+
.footer { font-size: 11px; color: #666; text-align: center; margin-top: 40px; border-top: 1px solid #333; padding-top: 12px; }
|
|
81
|
+
</style>
|
|
82
|
+
</head><body>
|
|
83
|
+
<h1>Chat Session: ${escapeHtml(sessionId)}</h1>
|
|
84
|
+
<div class="meta">${headerParts.join(' · ')}</div>
|
|
85
|
+
<hr style="border:none;border-top:1px solid #333;margin:16px 0;">
|
|
86
|
+
${turns}
|
|
87
|
+
<div class="footer">Generated by vai · voyageai-cli</div>
|
|
88
|
+
</body></html>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Convert HTML to PDF using Playwright.
|
|
93
|
+
*/
|
|
94
|
+
async function htmlToPdf(html, options = {}) {
|
|
95
|
+
const { chromium } = require('playwright');
|
|
96
|
+
const browser = await chromium.launch({ headless: true });
|
|
97
|
+
try {
|
|
98
|
+
const page = await browser.newPage();
|
|
99
|
+
await page.setContent(html, { waitUntil: 'networkidle' });
|
|
100
|
+
|
|
101
|
+
const pdf = await page.pdf({
|
|
102
|
+
format: 'A4',
|
|
103
|
+
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
|
|
104
|
+
printBackground: true,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return pdf;
|
|
108
|
+
} finally {
|
|
109
|
+
await browser.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function escapeHtml(str) {
|
|
114
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { renderPdf, buildChatHtml, htmlToPdf };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Render PNG from Mermaid diagram via Playwright.
|
|
5
|
+
*
|
|
6
|
+
* @param {object} normalized
|
|
7
|
+
* @param {object} options
|
|
8
|
+
* @param {string} [options.resolution='2x'] - '1x' | '2x' | '3x'
|
|
9
|
+
* @param {string} [options.background='transparent'] - 'transparent' | 'dark' | 'light'
|
|
10
|
+
* @param {boolean} [options.includeWatermark=true]
|
|
11
|
+
* @param {boolean} [options.fitToContent=true]
|
|
12
|
+
* @returns {Promise<{ content: Buffer, mimeType: string }>}
|
|
13
|
+
*/
|
|
14
|
+
async function renderPng(normalized, options = {}) {
|
|
15
|
+
// If raw PNG buffer provided (from Electron canvas capture)
|
|
16
|
+
if (normalized._pngBuffer) {
|
|
17
|
+
return { content: normalized._pngBuffer, mimeType: 'image/png' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { workflowToMermaid } = require('./mermaid-export');
|
|
21
|
+
|
|
22
|
+
if (normalized._context !== 'workflow' && normalized._context !== 'benchmark') {
|
|
23
|
+
throw new Error(`PNG export not supported for context: ${normalized._context}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const mermaidSrc = normalized._context === 'workflow'
|
|
27
|
+
? workflowToMermaid(normalized, options)
|
|
28
|
+
: null;
|
|
29
|
+
|
|
30
|
+
if (!mermaidSrc) {
|
|
31
|
+
throw new Error('No Mermaid source available for PNG rendering');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const png = await renderMermaidToPng(mermaidSrc, options);
|
|
35
|
+
return { content: png, mimeType: 'image/png' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Render Mermaid syntax to PNG using Playwright.
|
|
40
|
+
*/
|
|
41
|
+
async function renderMermaidToPng(mermaidSrc, options = {}) {
|
|
42
|
+
const { chromium } = require('playwright');
|
|
43
|
+
const resolution = options.resolution || '2x';
|
|
44
|
+
const scale = parseInt(resolution) || 2;
|
|
45
|
+
const background = options.background || 'transparent';
|
|
46
|
+
const bgColor = background === 'dark' ? '#1e1e1e' : background === 'light' ? '#ffffff' : 'transparent';
|
|
47
|
+
|
|
48
|
+
const browser = await chromium.launch({ headless: true });
|
|
49
|
+
try {
|
|
50
|
+
const page = await browser.newPage({
|
|
51
|
+
deviceScaleFactor: scale,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const html = `<!DOCTYPE html>
|
|
55
|
+
<html><head>
|
|
56
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
57
|
+
</head><body style="margin:0;padding:16px;background:${bgColor};">
|
|
58
|
+
<pre class="mermaid">${escapeHtml(mermaidSrc)}</pre>
|
|
59
|
+
<script>mermaid.initialize({ startOnLoad: true, theme: '${options.theme || 'dark'}' });</script>
|
|
60
|
+
</body></html>`;
|
|
61
|
+
|
|
62
|
+
await page.setContent(html, { waitUntil: 'networkidle' });
|
|
63
|
+
await page.waitForSelector('svg', { timeout: 10000 });
|
|
64
|
+
|
|
65
|
+
const fitToContent = options.fitToContent !== false;
|
|
66
|
+
|
|
67
|
+
let screenshotOpts = { type: 'png' };
|
|
68
|
+
if (background === 'transparent') {
|
|
69
|
+
screenshotOpts.omitBackground = true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (fitToContent) {
|
|
73
|
+
const svgEl = await page.$('svg');
|
|
74
|
+
const box = await svgEl.boundingBox();
|
|
75
|
+
if (box) {
|
|
76
|
+
screenshotOpts.clip = {
|
|
77
|
+
x: Math.max(0, box.x - 8),
|
|
78
|
+
y: Math.max(0, box.y - 8),
|
|
79
|
+
width: box.width + 16,
|
|
80
|
+
height: box.height + 16,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const buffer = await page.screenshot(screenshotOpts);
|
|
86
|
+
return buffer;
|
|
87
|
+
} finally {
|
|
88
|
+
await browser.close();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function escapeHtml(str) {
|
|
93
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { renderPng, renderMermaidToPng };
|