yaml-flow 5.2.1 → 5.2.5
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/board-livecards-server-runtime.js +48 -9
- package/browser/live-cards.js +55 -9
- package/dist/cli/board-live-cards-cli.cjs +9 -6
- package/dist/cli/board-live-cards-cli.cjs.map +1 -1
- package/dist/cli/board-live-cards-cli.js +9 -6
- package/dist/cli/board-live-cards-cli.js.map +1 -1
- package/examples/example-board/demo-chat-handler.js +111 -362
- package/examples/example-board/demo-server.js +6 -0
- package/examples/example-board/demo-shell-browser.html +3 -3
- package/examples/example-board/demo-shell-with-server.html +4 -4
- package/package.json +1 -1
|
@@ -1,81 +1,101 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* demo-chat-handler.js — LLM-based chat handler for example-board.
|
|
1
|
+
/**
|
|
2
|
+
* demo-chat-handler.js - Chat handler for example-board.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
4
|
+
* Invoked by reusable-server-runtime after a user message is persisted:
|
|
6
5
|
* node demo-chat-handler.js --boardId <id> --cardId <id> --extraEncJson <base64json>
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* extraEncJson decodes to:
|
|
8
|
+
* boardSetupRoot — absolute path to board root (parent of runtime/, surface/, runtime-out/)
|
|
9
|
+
* boardRuntimeDir — relative subdir: 'runtime'
|
|
10
|
+
* runtimeStatusDir— relative subdir: 'runtime-out'
|
|
11
|
+
* cardsDir — relative subdir: 'surface/tmp-cards'
|
|
12
|
+
* chatDir — absolute path to the card's chats directory
|
|
13
|
+
* lastChatFile — filename of the just-written user message, e.g. '001_user.txt'
|
|
9
14
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* It reads the full conversation history from chatDir, builds a grounded system prompt
|
|
13
|
-
* (scoped to the card and board), and calls the LLM directly (Copilot CLI).
|
|
14
|
-
* Copilot is invoked from boardDir (cwd), so it naturally has access to board files.
|
|
15
|
-
*
|
|
16
|
-
* The LLM is the sole decision-maker. No rule-based fallback is used here — if the LLM
|
|
17
|
-
* is unavailable, the handler writes a short error acknowledgment so the user isn't left
|
|
18
|
-
* with a silent failure.
|
|
15
|
+
* Invokes copilot_wrapper.bat with a prompt built from conversation history.
|
|
16
|
+
* Session dir is per-card: os.tmpdir()/demo-chat-handler-sessions/<boardId>_<cardId>
|
|
19
17
|
*/
|
|
20
18
|
|
|
21
|
-
import * as fs
|
|
19
|
+
import * as fs from 'node:fs';
|
|
22
20
|
import * as path from 'node:path';
|
|
23
|
-
import
|
|
21
|
+
import * as os from 'node:os';
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
24
|
|
|
25
|
-
const
|
|
25
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
26
|
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Args
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const args = process.argv.slice(2);
|
|
27
31
|
function getArg(name) {
|
|
28
32
|
const idx = args.indexOf(name);
|
|
29
33
|
return idx !== -1 && args[idx + 1] !== undefined ? args[idx + 1] : null;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
const boardId
|
|
33
|
-
const cardId
|
|
34
|
-
const extraStr
|
|
36
|
+
const boardId = getArg('--boardId') || '';
|
|
37
|
+
const cardId = getArg('--cardId') || '';
|
|
38
|
+
const extraStr = getArg('--extraEncJson') || '';
|
|
39
|
+
const cleanOnExit = getArg('--cleanOnExit') || '';
|
|
35
40
|
|
|
36
41
|
let extra = {};
|
|
37
|
-
try {
|
|
38
|
-
|
|
39
|
-
} catch {
|
|
40
|
-
console.error('[demo-chat-handler] could not parse --extraEncJson');
|
|
41
|
-
process.exit(0);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const { chatDir, boardDir, lastChatFile } = extra;
|
|
42
|
+
try { extra = JSON.parse(Buffer.from(extraStr, 'base64').toString('utf-8')); }
|
|
43
|
+
catch { console.error('[demo-chat-handler] bad --extraEncJson'); process.exit(0); }
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const { boardSetupRoot, boardRuntimeDir, runtimeStatusDir, cardsDir, chatDir, lastChatFile } = extra;
|
|
46
|
+
if (!boardSetupRoot || !chatDir || !lastChatFile) {
|
|
47
|
+
console.error('[demo-chat-handler] missing boardSetupRoot/chatDir/lastChatFile');
|
|
48
48
|
process.exit(0);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// Resolve absolute paths from the structured extra fields
|
|
52
|
+
const boardRuntimeDirAbs = path.join(boardSetupRoot, boardRuntimeDir || 'runtime');
|
|
53
|
+
const runtimeStatusDirAbs = path.join(boardSetupRoot, runtimeStatusDir || 'runtime-out');
|
|
54
|
+
const cardsDirAbs = path.join(boardSetupRoot, cardsDir || path.join('surface', 'tmp-cards'));
|
|
55
|
+
const chatDirAbs = chatDir;
|
|
56
|
+
|
|
51
57
|
// ---------------------------------------------------------------------------
|
|
52
|
-
// Read
|
|
58
|
+
// Read conversation history
|
|
53
59
|
// ---------------------------------------------------------------------------
|
|
54
|
-
function
|
|
55
|
-
let files;
|
|
60
|
+
function readHistory(dir) {
|
|
56
61
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
return fs.readdirSync(dir)
|
|
63
|
+
.filter(f => /^\d+[-_](user|assistant)\.txt$/i.test(f))
|
|
64
|
+
.sort()
|
|
65
|
+
.map(f => {
|
|
66
|
+
const role = /user/i.test(f) ? 'User' : 'Assistant';
|
|
67
|
+
let text = '';
|
|
68
|
+
try { text = fs.readFileSync(path.join(dir, f), 'utf-8').trim(); } catch {}
|
|
69
|
+
return role + ': ' + text;
|
|
70
|
+
});
|
|
71
|
+
} catch { return []; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Build prompt
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
function buildPrompt(cId, bId, history, responseFileRel) {
|
|
78
|
+
const cardSetupDirRel = path.join(cardsDir, cId).replace(/\\/g, '/');
|
|
79
|
+
const runtimeDirRel = boardRuntimeDir || 'runtime';
|
|
80
|
+
const statusDirRel = runtimeStatusDir || 'runtime-out';
|
|
81
|
+
const chatDirRel = path.relative(boardSetupRoot, chatDir).replace(/\\/g, '/');
|
|
82
|
+
const lastQueryFileRel = path.join(chatDirRel, lastChatFile).replace(/\\/g, '/');
|
|
83
|
+
|
|
84
|
+
const contextBlock = [
|
|
85
|
+
'We are currently doing a three way orchestration.',
|
|
86
|
+
'You are the responder who has context of the cards in ' + cardSetupDirRel + ',',
|
|
87
|
+
'card runtime statuses in ' + runtimeDirRel + ',',
|
|
88
|
+
'and computed outputs in ' + statusDirRel + '.',
|
|
89
|
+
'I am just a mediator passing on the query.',
|
|
90
|
+
'The user sees the data available in cards which is rendered, and the status from ' + statusDirRel + '.',
|
|
91
|
+
'Everything else is internal detail not to be exposed to the user.',
|
|
92
|
+
'The conversation history can be found in ' + chatDirRel + ' and the last query is in ' + lastQueryFileRel + '.',
|
|
93
|
+
'Write your response to the user in ' + responseFileRel + ' (relative to your working directory).',
|
|
94
|
+
'Give me only a bare minimum log line on what you did — the response in ' + responseFileRel + ' is what the user will see.',
|
|
95
|
+
].join(' ');
|
|
69
96
|
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
// Build prompt: system instruction + conversation turns
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
function buildPrompt(bId, cId, history) {
|
|
74
97
|
return [
|
|
75
|
-
|
|
76
|
-
'Help the user understand and act on the data shown in this card.',
|
|
77
|
-
'Be concise — this is an inline card chat, not a full conversation window.',
|
|
78
|
-
'Ground answers in the card\'s data context. Ask one short question if the intent is ambiguous.',
|
|
98
|
+
contextBlock,
|
|
79
99
|
'',
|
|
80
100
|
...history,
|
|
81
101
|
'Assistant:',
|
|
@@ -83,327 +103,56 @@ function buildPrompt(bId, cId, history) {
|
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
// ---------------------------------------------------------------------------
|
|
86
|
-
//
|
|
106
|
+
// Invoke copilot_wrapper.bat
|
|
87
107
|
// ---------------------------------------------------------------------------
|
|
88
|
-
function
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
/^Requests\b/i.test(lines[lines.length - 2]) &&
|
|
95
|
-
/^Tokens\b/i.test(lines[lines.length - 1])
|
|
96
|
-
) {
|
|
97
|
-
lines.splice(lines.length - 3, 3);
|
|
98
|
-
}
|
|
99
|
-
while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop();
|
|
100
|
-
return lines.join('\n');
|
|
101
|
-
}
|
|
108
|
+
function runWrapper(prompt, sessionDir, workingDir) {
|
|
109
|
+
const wrapperPath = path.join(__dirname, 'scripts', 'copilot_wrapper.bat');
|
|
110
|
+
const tmpBase = os.tmpdir();
|
|
111
|
+
const ts = Date.now();
|
|
112
|
+
const outFile = path.join(tmpBase, 'dch-out-' + cardId + '-' + ts + '.txt');
|
|
113
|
+
const promptFile = path.join(tmpBase, 'dch-prompt-' + cardId + '-' + ts + '.txt');
|
|
102
114
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (envBin && fs.existsSync(envBin)) return envBin;
|
|
106
|
-
if (process.platform === 'win32') {
|
|
107
|
-
try {
|
|
108
|
-
const out = execFileSync('where.exe', ['copilot'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
109
|
-
const candidates = out.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
110
|
-
return candidates.find(p => /\.(cmd|exe|bat)$/i.test(p)) ?? candidates[0] ?? 'copilot';
|
|
111
|
-
} catch {}
|
|
112
|
-
} else {
|
|
113
|
-
try {
|
|
114
|
-
const out = execFileSync('which', ['copilot'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
115
|
-
return out.split(/\r?\n/).map(s => s.trim()).find(Boolean) ?? 'copilot';
|
|
116
|
-
} catch {}
|
|
117
|
-
}
|
|
118
|
-
return 'copilot';
|
|
119
|
-
}
|
|
115
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
116
|
+
fs.writeFileSync(promptFile, prompt, 'utf-8');
|
|
120
117
|
|
|
121
|
-
function runCopilotPrompt(prompt, cwd) {
|
|
122
|
-
const copilotBin = resolveCopilotExecutable();
|
|
123
|
-
const opts = {
|
|
124
|
-
input: String(prompt),
|
|
125
|
-
encoding: 'utf-8',
|
|
126
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
127
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
128
|
-
timeout: 60000,
|
|
129
|
-
...(cwd ? { cwd } : {}),
|
|
130
|
-
};
|
|
131
118
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
const msg = [directErr?.stderr?.trim?.(), String(directErr?.message ?? directErr)].filter(Boolean).join(' | ');
|
|
147
|
-
throw new Error(msg || 'copilot invocation failed');
|
|
119
|
+
spawnSync('cmd.exe', [
|
|
120
|
+
'/c', wrapperPath,
|
|
121
|
+
outFile,
|
|
122
|
+
sessionDir,
|
|
123
|
+
workingDir,
|
|
124
|
+
'@' + promptFile,
|
|
125
|
+
'raw',
|
|
126
|
+
'demo-chat',
|
|
127
|
+
], { stdio: 'inherit', timeout: 120000 });
|
|
128
|
+
|
|
129
|
+
return fs.existsSync(outFile) ? fs.readFileSync(outFile, 'utf-8').trim() : '';
|
|
130
|
+
} finally {
|
|
131
|
+
try { fs.unlinkSync(promptFile); } catch {}
|
|
132
|
+
try { fs.unlinkSync(outFile); } catch {}
|
|
148
133
|
}
|
|
149
134
|
}
|
|
150
135
|
|
|
151
136
|
// ---------------------------------------------------------------------------
|
|
152
137
|
// Main
|
|
153
138
|
// ---------------------------------------------------------------------------
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
let response = '';
|
|
159
|
-
try {
|
|
160
|
-
response = stripCopilotFooter(runCopilotPrompt(prompt, cwd)).trim();
|
|
161
|
-
} catch (err) {
|
|
162
|
-
const lastUser = [...history].reverse().find(l => l.startsWith('User:')) ?? '';
|
|
163
|
-
response = `Sorry, I could not reach the LLM right now. (${String(err?.message ?? err).slice(0, 120)})`;
|
|
164
|
-
console.error(`[demo-chat-handler] LLM call failed: ${err?.message ?? err}`);
|
|
165
|
-
}
|
|
139
|
+
const serialMatch = String(lastChatFile).match(/^(\d+)/);
|
|
140
|
+
const nextSerial = serialMatch ? parseInt(serialMatch[1], 10) + 1 : 1;
|
|
141
|
+
const nextName = String(nextSerial).padStart(3, '0') + '-assistant.txt';
|
|
142
|
+
const responseFileRel = path.relative(boardSetupRoot, path.join(chatDir, nextName)).replace(/\\/g, '/');
|
|
166
143
|
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
const nextPath = path.join(chatDir, nextName);
|
|
144
|
+
const history = readHistory(chatDirAbs);
|
|
145
|
+
const sessionDir = path.join(os.tmpdir(), 'demo-chat-handler-sessions', boardId + '_' + cardId);
|
|
146
|
+
const workingDir = boardSetupRoot;
|
|
147
|
+
const prompt = buildPrompt(cardId, boardId, history, responseFileRel);
|
|
172
148
|
|
|
173
149
|
try {
|
|
174
|
-
|
|
175
|
-
console.log(
|
|
150
|
+
runWrapper(prompt, sessionDir, workingDir);
|
|
151
|
+
console.log('[demo-chat-handler] cardId="' + cardId + '" copilot invoked, response expected at ' + responseFileRel);
|
|
176
152
|
} catch (err) {
|
|
177
|
-
console.error(
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
* node demo-chat-handler.js --boardId <id> --cardId <id> --extraEncJson <base64json>
|
|
182
|
-
*
|
|
183
|
-
* --extraEncJson decodes to: { chatDir: "<abs>", boardDir: "<abs>", lastChatFile: "<filename>" }
|
|
184
|
-
*
|
|
185
|
-
* Responsibilities:
|
|
186
|
-
* 1. Read the full conversation history from chatDir (all *_user.txt / *-assistant.txt files).
|
|
187
|
-
* 2. Read the current card state from boardDir/board-graph.json (card_data, fetched_sources,
|
|
188
|
-
* computed_values for cardId) to use as grounding context.
|
|
189
|
-
* 3. Build a system prompt that situates the LLM as an assistant for this specific card,
|
|
190
|
-
* including the card's current data as context.
|
|
191
|
-
* 4. Send the conversation + context to the LLM (Copilot via CLI).
|
|
192
|
-
* 5. Write the response as <nextSerial>-assistant.txt to chatDir.
|
|
193
|
-
*
|
|
194
|
-
* Design principle:
|
|
195
|
-
* The chat is always scoped to the card where the chat button is embedded.
|
|
196
|
-
* The card's current state (card_data, computed_values, fetched_sources) is the primary
|
|
197
|
-
* grounding context. The LLM should help the user understand, explore, or act on that card's
|
|
198
|
-
* data — not give generic answers disconnected from the card's content.
|
|
199
|
-
*
|
|
200
|
-
* The system prompt should encourage the LLM to:
|
|
201
|
-
* - Reference the card's actual values when answering
|
|
202
|
-
* - Ask clarifying questions if the user's intent is ambiguous
|
|
203
|
-
* - Suggest next steps relevant to the card's domain
|
|
204
|
-
* - Be concise (the chat is embedded in a card, not a full chat window)
|
|
205
|
-
*/
|
|
206
|
-
|
|
207
|
-
import * as fs from 'node:fs';
|
|
208
|
-
import * as path from 'node:path';
|
|
209
|
-
import { execFileSync } from 'node:child_process';
|
|
210
|
-
|
|
211
|
-
const args = process.argv.slice(2);
|
|
212
|
-
|
|
213
|
-
function getArg(name) {
|
|
214
|
-
const idx = args.indexOf(name);
|
|
215
|
-
return idx !== -1 && args[idx + 1] !== undefined ? args[idx + 1] : null;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const boardId = getArg('--boardId') || '';
|
|
219
|
-
const cardId = getArg('--cardId') || '';
|
|
220
|
-
const extraStr = getArg('--extraEncJson') || '';
|
|
221
|
-
|
|
222
|
-
let extra = {};
|
|
223
|
-
try {
|
|
224
|
-
extra = JSON.parse(Buffer.from(extraStr, 'base64').toString('utf-8'));
|
|
225
|
-
} catch {
|
|
226
|
-
console.error('[demo-chat-handler] could not parse --extraEncJson');
|
|
227
|
-
process.exit(0);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const { chatDir, boardDir, lastChatFile } = extra;
|
|
231
|
-
|
|
232
|
-
if (!chatDir || !lastChatFile) {
|
|
233
|
-
console.error('[demo-chat-handler] --extraEncJson must contain chatDir and lastChatFile');
|
|
234
|
-
process.exit(0);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// ---------------------------------------------------------------------------
|
|
238
|
-
// 1. Read full conversation history from chatDir
|
|
239
|
-
// ---------------------------------------------------------------------------
|
|
240
|
-
function readConversationHistory(dir) {
|
|
241
|
-
let files;
|
|
242
|
-
try {
|
|
243
|
-
files = fs.readdirSync(dir).filter(f => /^\d+[-_](user|assistant)\.txt$/i.test(f));
|
|
244
|
-
files.sort();
|
|
245
|
-
} catch {
|
|
246
|
-
return [];
|
|
153
|
+
console.error('[demo-chat-handler] wrapper failed: ' + (err?.message ?? err));
|
|
154
|
+
} finally {
|
|
155
|
+
if (cleanOnExit) {
|
|
156
|
+
try { fs.unlinkSync(cleanOnExit); } catch {}
|
|
247
157
|
}
|
|
248
|
-
|
|
249
|
-
const role = /user/i.test(f) ? 'user' : 'assistant';
|
|
250
|
-
let text = '';
|
|
251
|
-
try { text = fs.readFileSync(path.join(dir, f), 'utf-8').trim(); } catch {}
|
|
252
|
-
return { role, text };
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// ---------------------------------------------------------------------------
|
|
257
|
-
// 2. Read card state from board-graph.json
|
|
258
|
-
// ---------------------------------------------------------------------------
|
|
259
|
-
function readCardState(bDir, cId) {
|
|
260
|
-
if (!bDir) return null;
|
|
261
|
-
try {
|
|
262
|
-
const boardGraph = JSON.parse(fs.readFileSync(path.join(bDir, 'board-graph.json'), 'utf-8'));
|
|
263
|
-
// board-graph.json wraps a LiveGraph snapshot; cards live under graph.nodes
|
|
264
|
-
const nodes = boardGraph?.graph?.nodes ?? boardGraph?.nodes ?? {};
|
|
265
|
-
return nodes[cId] ?? null;
|
|
266
|
-
} catch {
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// ---------------------------------------------------------------------------
|
|
272
|
-
// 3. Build system prompt grounded in the card's current state
|
|
273
|
-
// ---------------------------------------------------------------------------
|
|
274
|
-
function buildSystemPrompt(cId, cardState) {
|
|
275
|
-
const lines = [
|
|
276
|
-
`You are a helpful assistant embedded inside a live card (id: "${cId}") on a data dashboard.`,
|
|
277
|
-
'Your role is to help the user understand, interpret, and act on the data shown in this card.',
|
|
278
|
-
'Always ground your answers in the card\'s actual current values. Be concise — this is an embedded card chat, not a full conversation window.',
|
|
279
|
-
'If the user\'s question is ambiguous, ask one short clarifying question.',
|
|
280
|
-
'Suggest relevant next steps or insights when appropriate.',
|
|
281
|
-
'',
|
|
282
|
-
'--- Current card state ---',
|
|
283
|
-
];
|
|
284
|
-
|
|
285
|
-
if (cardState) {
|
|
286
|
-
if (cardState.card_data && Object.keys(cardState.card_data).length > 0) {
|
|
287
|
-
lines.push('card_data: ' + JSON.stringify(cardState.card_data, null, 2));
|
|
288
|
-
}
|
|
289
|
-
if (cardState.computed_values && Object.keys(cardState.computed_values).length > 0) {
|
|
290
|
-
lines.push('computed_values: ' + JSON.stringify(cardState.computed_values, null, 2));
|
|
291
|
-
}
|
|
292
|
-
if (cardState.fetched_sources && Object.keys(cardState.fetched_sources).length > 0) {
|
|
293
|
-
lines.push('fetched_sources: ' + JSON.stringify(cardState.fetched_sources, null, 2));
|
|
294
|
-
}
|
|
295
|
-
} else {
|
|
296
|
-
lines.push('(card state not available)');
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
lines.push('--- End card state ---');
|
|
300
|
-
return lines.join('\n');
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// ---------------------------------------------------------------------------
|
|
304
|
-
// 4. Build the full prompt (system + conversation turns)
|
|
305
|
-
// ---------------------------------------------------------------------------
|
|
306
|
-
function buildPrompt(systemPrompt, history) {
|
|
307
|
-
const parts = [systemPrompt, ''];
|
|
308
|
-
for (const turn of history) {
|
|
309
|
-
parts.push(`${turn.role === 'user' ? 'User' : 'Assistant'}: ${turn.text}`);
|
|
310
|
-
}
|
|
311
|
-
parts.push('Assistant:');
|
|
312
|
-
return parts.join('\n');
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// ---------------------------------------------------------------------------
|
|
316
|
-
// 5. Call LLM (Copilot CLI)
|
|
317
|
-
// ---------------------------------------------------------------------------
|
|
318
|
-
function resolveCopilotExecutable() {
|
|
319
|
-
const envBin = process.env.COPILOT_BIN;
|
|
320
|
-
if (envBin && fs.existsSync(envBin)) return envBin;
|
|
321
|
-
|
|
322
|
-
if (process.platform === 'win32') {
|
|
323
|
-
try {
|
|
324
|
-
const out = execFileSync('where.exe', ['copilot'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
325
|
-
const candidates = out.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
326
|
-
return candidates.find(p => /\.(cmd|exe|bat)$/i.test(p)) ?? candidates[0] ?? 'copilot';
|
|
327
|
-
} catch {}
|
|
328
|
-
} else {
|
|
329
|
-
try {
|
|
330
|
-
const out = execFileSync('which', ['copilot'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
331
|
-
return out.split(/\r?\n/).map(s => s.trim()).find(Boolean) ?? 'copilot';
|
|
332
|
-
} catch {}
|
|
333
|
-
}
|
|
334
|
-
return 'copilot';
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function stripCopilotFooter(rawText) {
|
|
338
|
-
const lines = String(rawText ?? '').split(/\r?\n/);
|
|
339
|
-
while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop();
|
|
340
|
-
if (
|
|
341
|
-
lines.length >= 3 &&
|
|
342
|
-
/^Changes\b/i.test(lines[lines.length - 3]) &&
|
|
343
|
-
/^Requests\b/i.test(lines[lines.length - 2]) &&
|
|
344
|
-
/^Tokens\b/i.test(lines[lines.length - 1])
|
|
345
|
-
) {
|
|
346
|
-
lines.splice(lines.length - 3, 3);
|
|
347
|
-
}
|
|
348
|
-
while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop();
|
|
349
|
-
return lines.join('\n');
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function runLLM(prompt) {
|
|
353
|
-
const copilotBin = resolveCopilotExecutable();
|
|
354
|
-
try {
|
|
355
|
-
const raw = execFileSync(copilotBin, ['--allow-all'], {
|
|
356
|
-
input: String(prompt),
|
|
357
|
-
encoding: 'utf-8',
|
|
358
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
359
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
360
|
-
timeout: 60000,
|
|
361
|
-
});
|
|
362
|
-
return stripCopilotFooter(raw).trim();
|
|
363
|
-
} catch (err) {
|
|
364
|
-
if (process.platform === 'win32') {
|
|
365
|
-
try {
|
|
366
|
-
const raw = execFileSync('cmd.exe', ['/d', '/c', 'copilot --allow-all'], {
|
|
367
|
-
input: String(prompt),
|
|
368
|
-
encoding: 'utf-8',
|
|
369
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
370
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
371
|
-
timeout: 60000,
|
|
372
|
-
});
|
|
373
|
-
return stripCopilotFooter(raw).trim();
|
|
374
|
-
} catch {}
|
|
375
|
-
}
|
|
376
|
-
throw err;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// ---------------------------------------------------------------------------
|
|
381
|
-
// Main
|
|
382
|
-
// ---------------------------------------------------------------------------
|
|
383
|
-
const history = readConversationHistory(chatDir);
|
|
384
|
-
const cardState = readCardState(boardDir, cardId);
|
|
385
|
-
const systemPmt = buildSystemPrompt(cardId, cardState);
|
|
386
|
-
const fullPrompt = buildPrompt(systemPmt, history);
|
|
387
|
-
|
|
388
|
-
let response = '';
|
|
389
|
-
try {
|
|
390
|
-
response = runLLM(fullPrompt);
|
|
391
|
-
} catch (err) {
|
|
392
|
-
// Fallback: acknowledge the message so the user sees something
|
|
393
|
-
const lastUserMsg = [...history].reverse().find(t => t.role === 'user')?.text ?? '';
|
|
394
|
-
response = `I received your message ("${lastUserMsg.slice(0, 80)}") but could not reach the LLM right now. Please try again.`;
|
|
395
|
-
console.error(`[demo-chat-handler] LLM call failed: ${err && err.message || err}`);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Derive next serial and write assistant response
|
|
399
|
-
const serialMatch = String(lastChatFile).match(/^(\d+)/);
|
|
400
|
-
const nextSerial = serialMatch ? parseInt(serialMatch[1], 10) + 1 : 1;
|
|
401
|
-
const nextName = `${String(nextSerial).padStart(3, '0')}-assistant.txt`;
|
|
402
|
-
const nextPath = path.join(chatDir, nextName);
|
|
403
|
-
|
|
404
|
-
try {
|
|
405
|
-
fs.writeFileSync(nextPath, response + '\n', 'utf-8');
|
|
406
|
-
console.log(`[demo-chat-handler] boardId="${boardId}" cardId="${cardId}" wrote response → ${nextPath}`);
|
|
407
|
-
} catch (err) {
|
|
408
|
-
console.error(`[demo-chat-handler] write failed: ${err.message}`);
|
|
409
|
-
}
|
|
158
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import http from 'node:http';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
import { createRequire } from 'node:module';
|
|
8
9
|
|
|
@@ -90,6 +91,11 @@ function resetRuntime() {
|
|
|
90
91
|
fs.rmSync(setupDir, { recursive: true, force: true });
|
|
91
92
|
console.log(`[demo-server] reset: wiped ${setupDir}`);
|
|
92
93
|
}
|
|
94
|
+
const chatSessions = path.join(os.tmpdir(), 'demo-chat-handler-sessions');
|
|
95
|
+
if (fs.existsSync(chatSessions)) {
|
|
96
|
+
fs.rmSync(chatSessions, { recursive: true, force: true });
|
|
97
|
+
console.log(`[demo-server] reset: wiped ${chatSessions}`);
|
|
98
|
+
}
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
if (RESET_ON_START) {
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
<title>Example Board Demo (Browser Runtime)</title>
|
|
7
7
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
8
8
|
<script src="https://cdn.jsdelivr.net/npm/jsonata/jsonata.min.js"></script>
|
|
9
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.
|
|
10
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.
|
|
11
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.
|
|
9
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.5/browser/card-compute.js"></script>
|
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.5/browser/live-cards.js"></script>
|
|
11
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.5/browser/board-livegraph-engine.js"></script>
|
|
12
12
|
</head>
|
|
13
13
|
<body class="bg-light">
|
|
14
14
|
<div class="container-fluid py-3">
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
</style>
|
|
17
17
|
<script src="https://cdn.jsdelivr.net/npm/jsonata/jsonata.min.js"></script>
|
|
18
18
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
19
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.
|
|
20
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.
|
|
21
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.
|
|
22
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.
|
|
19
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.5/browser/card-compute.js"></script>
|
|
20
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.5/browser/live-cards.js"></script>
|
|
21
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.5/browser/board-livegraph-engine.js"></script>
|
|
22
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@5.2.5/browser/board-livecards-runtime-client.js"></script>
|
|
23
23
|
</head>
|
|
24
24
|
<body class="bg-light">
|
|
25
25
|
<div class="container-fluid py-3">
|