living-ai-documentation 2.0.0 → 2.3.0
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/dist/src/frontend/agents.js +288 -0
- package/dist/src/frontend/config.js +3 -2
- package/dist/src/frontend/diagram/color-picker.js +72 -0
- package/dist/src/frontend/diagram/defaults-modal.js +302 -0
- package/dist/src/frontend/diagram/edge-panel.js +110 -11
- package/dist/src/frontend/diagram/main.js +5 -8
- package/dist/src/frontend/diagram/network.js +16 -7
- package/dist/src/frontend/diagram/node-panel.js +104 -2
- package/dist/src/frontend/diagram.html +52 -29
- package/dist/src/frontend/i18n/en.json +43 -1
- package/dist/src/frontend/i18n/fr.json +43 -1
- package/dist/src/frontend/index.html +156 -33
- package/dist/src/frontend/workspace/app.js +439 -67
- package/dist/src/frontend/workspace/app.js.map +1 -1
- package/dist/src/frontend/workspace/app.ts +582 -84
- package/dist/src/frontend/workspace/index.html +232 -82
- package/dist/src/frontend/workspace/persistence.js +61 -0
- package/dist/src/frontend/workspace/persistence.js.map +1 -1
- package/dist/src/frontend/workspace/persistence.ts +111 -0
- package/dist/src/frontend/workspace/styles.css +293 -34
- package/dist/src/lib/config.d.ts +23 -0
- package/dist/src/lib/config.d.ts.map +1 -1
- package/dist/src/lib/config.js +16 -0
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/routes/config.d.ts.map +1 -1
- package/dist/src/routes/config.js +13 -0
- package/dist/src/routes/config.js.map +1 -1
- package/dist/src/routes/workspace.d.ts.map +1 -1
- package/dist/src/routes/workspace.js +528 -6
- package/dist/src/routes/workspace.js.map +1 -1
- package/package.json +2 -2
|
@@ -7,11 +7,14 @@ exports.workspaceRouter = workspaceRouter;
|
|
|
7
7
|
const express_1 = require("express");
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const config_1 = require("../lib/config");
|
|
10
11
|
const WORKSPACE_VERSION = 1;
|
|
11
12
|
const MAX_WORKSPACE_ENTITIES = 1000;
|
|
12
13
|
const MAX_LABEL_LENGTH = 160;
|
|
13
14
|
const MAX_TEXT_FIELD_LENGTH = 4000;
|
|
14
15
|
const MAX_WORKSPACE_FILE_BYTES = 1024 * 1024;
|
|
16
|
+
const WORKSPACE_PROVIDER_ROOT = path_1.default.join('AI', 'WORKSPACE');
|
|
17
|
+
const DEFAULT_LLM_TEST_TIMEOUT_MS = 5000;
|
|
15
18
|
function workspaceFilePath(docsPath) {
|
|
16
19
|
return path_1.default.join(docsPath, '.workspace');
|
|
17
20
|
}
|
|
@@ -41,7 +44,7 @@ function writeWorkspaceState(docsPath, state) {
|
|
|
41
44
|
fs_1.default.writeFileSync(tmpPath, JSON.stringify(state, null, 2), 'utf-8');
|
|
42
45
|
fs_1.default.renameSync(tmpPath, filePath);
|
|
43
46
|
}
|
|
44
|
-
function sanitizeWorkspaceState(input) {
|
|
47
|
+
function sanitizeWorkspaceState(input, docsPath) {
|
|
45
48
|
if (!isRecord(input)) {
|
|
46
49
|
throw new Error('workspace payload must be an object');
|
|
47
50
|
}
|
|
@@ -55,12 +58,16 @@ function sanitizeWorkspaceState(input) {
|
|
|
55
58
|
const updatedAt = typeof input.updatedAt === 'string' && input.updatedAt.trim()
|
|
56
59
|
? input.updatedAt
|
|
57
60
|
: new Date().toISOString();
|
|
58
|
-
|
|
61
|
+
const state = {
|
|
59
62
|
version: WORKSPACE_VERSION,
|
|
60
63
|
updatedAt,
|
|
61
64
|
entities: entitiesInput.map(sanitizeEntity),
|
|
62
65
|
camera: sanitizeCamera(input.camera),
|
|
63
66
|
};
|
|
67
|
+
if (docsPath) {
|
|
68
|
+
ensureProviderWorkspaceFolders(docsPath, state);
|
|
69
|
+
}
|
|
70
|
+
return state;
|
|
64
71
|
}
|
|
65
72
|
function sanitizeEntity(input) {
|
|
66
73
|
if (!isRecord(input)) {
|
|
@@ -85,14 +92,91 @@ function sanitizeEntity(input) {
|
|
|
85
92
|
function sanitizeConfig(input) {
|
|
86
93
|
const config = isRecord(input) ? input : {};
|
|
87
94
|
return {
|
|
88
|
-
environment: safeOptionalString(config.environment, 'local', MAX_LABEL_LENGTH),
|
|
89
95
|
endpoint: safeOptionalString(config.endpoint, '', MAX_TEXT_FIELD_LENGTH),
|
|
90
96
|
token: safeOptionalString(config.token, '', MAX_TEXT_FIELD_LENGTH),
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
model: safeOptionalString(config.model, '', MAX_LABEL_LENGTH),
|
|
98
|
+
timeout: clamp(safeFiniteNumber(config.timeout, 180), 1, 600),
|
|
99
|
+
systemPrompt: safeOptionalString(config.systemPrompt, '', MAX_TEXT_FIELD_LENGTH),
|
|
100
|
+
requiresUserInput: config.requiresUserInput === true,
|
|
101
|
+
userInputDescription: safeOptionalString(config.userInputDescription, '', MAX_TEXT_FIELD_LENGTH),
|
|
102
|
+
expectedOutputMarker: safeOptionalString(config.expectedOutputMarker, '', MAX_LABEL_LENGTH),
|
|
93
103
|
description: safeOptionalString(config.description, '', MAX_TEXT_FIELD_LENGTH),
|
|
104
|
+
workspaceFolder: sanitizeWorkspaceFolder(safeOptionalString(config.workspaceFolder, '', MAX_TEXT_FIELD_LENGTH)),
|
|
94
105
|
};
|
|
95
106
|
}
|
|
107
|
+
function ensureProviderWorkspaceFolders(docsPath, state) {
|
|
108
|
+
const workspaceRoot = path_1.default.resolve(docsPath, WORKSPACE_PROVIDER_ROOT);
|
|
109
|
+
fs_1.default.mkdirSync(workspaceRoot, { recursive: true });
|
|
110
|
+
const reservedFolders = new Set();
|
|
111
|
+
for (const entity of state.entities) {
|
|
112
|
+
if (entity.kind !== 'agent') {
|
|
113
|
+
entity.config.workspaceFolder = '';
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const existingFolder = sanitizeWorkspaceFolder(entity.config.workspaceFolder);
|
|
117
|
+
const labelFolder = toPosixPath(path_1.default.join(WORKSPACE_PROVIDER_ROOT, slugify(entity.label)));
|
|
118
|
+
// Rename existing folder if label slug changed
|
|
119
|
+
if (existingFolder && existingFolder !== labelFolder) {
|
|
120
|
+
const absoluteExisting = path_1.default.resolve(docsPath, existingFolder);
|
|
121
|
+
if (fs_1.default.existsSync(absoluteExisting)) {
|
|
122
|
+
const uniqueLabel = uniqueWorkspaceFolder(labelFolder, reservedFolders);
|
|
123
|
+
const absoluteNew = path_1.default.resolve(docsPath, uniqueLabel);
|
|
124
|
+
if (!absoluteNew.startsWith(`${workspaceRoot}${path_1.default.sep}`)) {
|
|
125
|
+
throw new Error('provider workspace folder escapes AI/WORKSPACE');
|
|
126
|
+
}
|
|
127
|
+
fs_1.default.renameSync(absoluteExisting, absoluteNew);
|
|
128
|
+
entity.config.workspaceFolder = toPosixPath(uniqueLabel);
|
|
129
|
+
reservedFolders.add(entity.config.workspaceFolder);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const baseFolder = existingFolder || labelFolder;
|
|
134
|
+
const uniqueFolder = uniqueWorkspaceFolder(baseFolder, reservedFolders);
|
|
135
|
+
const absoluteFolder = path_1.default.resolve(docsPath, uniqueFolder);
|
|
136
|
+
if (!absoluteFolder.startsWith(`${workspaceRoot}${path_1.default.sep}`) && absoluteFolder !== workspaceRoot) {
|
|
137
|
+
throw new Error('provider workspace folder escapes AI/WORKSPACE');
|
|
138
|
+
}
|
|
139
|
+
fs_1.default.mkdirSync(absoluteFolder, { recursive: true });
|
|
140
|
+
entity.config.workspaceFolder = toPosixPath(uniqueFolder);
|
|
141
|
+
reservedFolders.add(entity.config.workspaceFolder);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function sanitizeWorkspaceFolder(folder) {
|
|
145
|
+
if (!folder) {
|
|
146
|
+
return '';
|
|
147
|
+
}
|
|
148
|
+
const normalized = toPosixPath(path_1.default.normalize(folder));
|
|
149
|
+
const prefix = toPosixPath(WORKSPACE_PROVIDER_ROOT);
|
|
150
|
+
if (normalized.startsWith('../') ||
|
|
151
|
+
path_1.default.isAbsolute(normalized) ||
|
|
152
|
+
normalized === prefix ||
|
|
153
|
+
!normalized.startsWith(`${prefix}/`)) {
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
return normalized;
|
|
157
|
+
}
|
|
158
|
+
function uniqueWorkspaceFolder(folder, reservedFolders) {
|
|
159
|
+
const parsed = path_1.default.posix.parse(toPosixPath(folder));
|
|
160
|
+
let candidate = toPosixPath(folder);
|
|
161
|
+
let index = 2;
|
|
162
|
+
while (reservedFolders.has(candidate)) {
|
|
163
|
+
candidate = path_1.default.posix.join(parsed.dir, `${parsed.name}_${index}`);
|
|
164
|
+
index += 1;
|
|
165
|
+
}
|
|
166
|
+
return candidate;
|
|
167
|
+
}
|
|
168
|
+
function slugify(value) {
|
|
169
|
+
const slug = value
|
|
170
|
+
.normalize('NFKD')
|
|
171
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
172
|
+
.toLowerCase()
|
|
173
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
174
|
+
.replace(/^_+|_+$/g, '');
|
|
175
|
+
return slug || 'llm_provider';
|
|
176
|
+
}
|
|
177
|
+
function toPosixPath(value) {
|
|
178
|
+
return value.split(path_1.default.sep).join('/');
|
|
179
|
+
}
|
|
96
180
|
function sanitizeCamera(input) {
|
|
97
181
|
const camera = isRecord(input) ? input : {};
|
|
98
182
|
return {
|
|
@@ -125,6 +209,326 @@ function clamp(value, min, max) {
|
|
|
125
209
|
function isRecord(input) {
|
|
126
210
|
return typeof input === 'object' && input !== null && !Array.isArray(input);
|
|
127
211
|
}
|
|
212
|
+
function llmModelsUrl(endpoint) {
|
|
213
|
+
const url = new URL(endpoint);
|
|
214
|
+
const cleanPath = url.pathname.replace(/\/+$/, '');
|
|
215
|
+
if (cleanPath.endsWith('/models')) {
|
|
216
|
+
url.pathname = cleanPath;
|
|
217
|
+
}
|
|
218
|
+
else if (cleanPath.endsWith('/v1')) {
|
|
219
|
+
url.pathname = `${cleanPath}/models`;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
url.pathname = `${cleanPath}/v1/models`;
|
|
223
|
+
}
|
|
224
|
+
return url;
|
|
225
|
+
}
|
|
226
|
+
async function testLlmConnection(input) {
|
|
227
|
+
if (typeof input.endpoint !== 'string' || !input.endpoint.trim()) {
|
|
228
|
+
throw new Error('endpoint is required');
|
|
229
|
+
}
|
|
230
|
+
const model = typeof input.model === 'string' ? input.model.trim() : '';
|
|
231
|
+
const timeoutSeconds = clamp(safeFiniteNumber(input.timeout, 5), 1, 30);
|
|
232
|
+
const controller = new AbortController();
|
|
233
|
+
const timeout = setTimeout(() => controller.abort(), timeoutSeconds * 1000 || DEFAULT_LLM_TEST_TIMEOUT_MS);
|
|
234
|
+
const headers = { Accept: 'application/json', 'Content-Type': 'application/json' };
|
|
235
|
+
if (typeof input.token === 'string' && input.token.trim()) {
|
|
236
|
+
headers.Authorization = `Bearer ${input.token.trim()}`;
|
|
237
|
+
}
|
|
238
|
+
if (model) {
|
|
239
|
+
const base = input.endpoint.trim();
|
|
240
|
+
const chatUrl = new URL(base.endsWith('/v1') ? `${base}/chat/completions` : `${base}/v1/chat/completions`);
|
|
241
|
+
try {
|
|
242
|
+
const response = await fetch(chatUrl, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers,
|
|
245
|
+
signal: controller.signal,
|
|
246
|
+
body: JSON.stringify({
|
|
247
|
+
model,
|
|
248
|
+
messages: [{ role: 'user', content: 'ping' }],
|
|
249
|
+
max_tokens: 1,
|
|
250
|
+
}),
|
|
251
|
+
});
|
|
252
|
+
const text = await response.text();
|
|
253
|
+
let replyModel = null;
|
|
254
|
+
try {
|
|
255
|
+
const body = JSON.parse(text);
|
|
256
|
+
if (isRecord(body) && typeof body.model === 'string') {
|
|
257
|
+
replyModel = body.model;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch { /* ignore */ }
|
|
261
|
+
return {
|
|
262
|
+
ok: response.ok,
|
|
263
|
+
status: response.status,
|
|
264
|
+
url: chatUrl.toString(),
|
|
265
|
+
detail: response.ok
|
|
266
|
+
? `Model ${replyModel ?? model} responded`
|
|
267
|
+
: `Chat completion failed (${response.status})`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
clearTimeout(timeout);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const url = llmModelsUrl(input.endpoint.trim());
|
|
275
|
+
try {
|
|
276
|
+
const response = await fetch(url, {
|
|
277
|
+
method: 'GET',
|
|
278
|
+
headers,
|
|
279
|
+
signal: controller.signal,
|
|
280
|
+
});
|
|
281
|
+
const text = await response.text();
|
|
282
|
+
let modelCount = null;
|
|
283
|
+
try {
|
|
284
|
+
const body = JSON.parse(text);
|
|
285
|
+
if (isRecord(body) && Array.isArray(body.data)) {
|
|
286
|
+
modelCount = body.data.length;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch { /* ignore */ }
|
|
290
|
+
return {
|
|
291
|
+
ok: response.ok,
|
|
292
|
+
status: response.status,
|
|
293
|
+
url: url.toString(),
|
|
294
|
+
detail: response.ok
|
|
295
|
+
? modelCount !== null
|
|
296
|
+
? `Connection OK (${modelCount} model${modelCount === 1 ? '' : 's'})`
|
|
297
|
+
: `Connection OK (${response.status})`
|
|
298
|
+
: `Connection failed (${response.status})`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
clearTimeout(timeout);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async function callMcp(endpoint, method, params) {
|
|
306
|
+
const response = await fetch(endpoint, {
|
|
307
|
+
method: 'POST',
|
|
308
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream' },
|
|
309
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
|
|
310
|
+
});
|
|
311
|
+
const text = await response.text();
|
|
312
|
+
// Handle SSE (data: ...) or plain JSON
|
|
313
|
+
for (const line of text.split('\n')) {
|
|
314
|
+
const trimmed = line.startsWith('data:') ? line.slice(5).trim() : line.trim();
|
|
315
|
+
if (!trimmed || trimmed === '[DONE]')
|
|
316
|
+
continue;
|
|
317
|
+
try {
|
|
318
|
+
const parsed = JSON.parse(trimmed);
|
|
319
|
+
if (isRecord(parsed) && isRecord(parsed.result))
|
|
320
|
+
return parsed.result;
|
|
321
|
+
if (isRecord(parsed) && parsed.error)
|
|
322
|
+
throw new Error(String(parsed.error.message ?? parsed.error));
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
throw new Error('No valid MCP response');
|
|
329
|
+
}
|
|
330
|
+
async function runAgent(config) {
|
|
331
|
+
if (!config.endpoint.trim())
|
|
332
|
+
throw new Error('endpoint is required');
|
|
333
|
+
if (!config.model.trim())
|
|
334
|
+
throw new Error('model is required');
|
|
335
|
+
if (!config.systemPrompt.trim())
|
|
336
|
+
throw new Error('systemPrompt is required');
|
|
337
|
+
const timeoutSeconds = clamp(config.timeout, 1, 600);
|
|
338
|
+
const base = config.endpoint.trim();
|
|
339
|
+
const chatUrl = new URL(base.endsWith('/v1') ? `${base}/chat/completions` : `${base}/v1/chat/completions`);
|
|
340
|
+
const llmHeaders = { Accept: 'application/json', 'Content-Type': 'application/json' };
|
|
341
|
+
if (config.token.trim()) {
|
|
342
|
+
llmHeaders.Authorization = `Bearer ${config.token.trim()}`;
|
|
343
|
+
}
|
|
344
|
+
let mcpTools = [];
|
|
345
|
+
if (config.mcpEndpoint) {
|
|
346
|
+
try {
|
|
347
|
+
const toolsRes = await callMcp(config.mcpEndpoint, 'tools/list', {});
|
|
348
|
+
if (isRecord(toolsRes) && Array.isArray(toolsRes.tools)) {
|
|
349
|
+
mcpTools = toolsRes.tools.map((tool) => {
|
|
350
|
+
if (!isRecord(tool))
|
|
351
|
+
return null;
|
|
352
|
+
return {
|
|
353
|
+
type: 'function',
|
|
354
|
+
function: {
|
|
355
|
+
name: tool.name,
|
|
356
|
+
description: tool.description ?? '',
|
|
357
|
+
parameters: tool.inputSchema ?? { type: 'object', properties: {} },
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}).filter(Boolean);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
mcpTools = [];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const messages = [{ role: 'system', content: config.systemPrompt.trim() }];
|
|
368
|
+
const expectedOutputMarker = config.expectedOutputMarker.trim();
|
|
369
|
+
if (config.userInput.trim()) {
|
|
370
|
+
messages.push({ role: 'user', content: config.userInput.trim() });
|
|
371
|
+
}
|
|
372
|
+
const controller = new AbortController();
|
|
373
|
+
const timer = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
|
|
374
|
+
let toolsSupported = mcpTools.length > 0;
|
|
375
|
+
try {
|
|
376
|
+
for (let turn = 0; turn < 5; turn += 1) {
|
|
377
|
+
const llmBody = { model: config.model.trim(), messages };
|
|
378
|
+
if (toolsSupported && mcpTools.length > 0)
|
|
379
|
+
llmBody.tools = mcpTools;
|
|
380
|
+
const response = await fetch(chatUrl, {
|
|
381
|
+
method: 'POST',
|
|
382
|
+
headers: llmHeaders,
|
|
383
|
+
signal: controller.signal,
|
|
384
|
+
body: JSON.stringify(llmBody),
|
|
385
|
+
});
|
|
386
|
+
// Model doesn't support tool use — retry once without tools
|
|
387
|
+
if (!response.ok && response.status === 400 && toolsSupported) {
|
|
388
|
+
toolsSupported = false;
|
|
389
|
+
// Inject tool descriptions as context in the system message
|
|
390
|
+
const toolDescriptions = mcpTools
|
|
391
|
+
.map((t) => {
|
|
392
|
+
if (!isRecord(t) || !isRecord(t.function))
|
|
393
|
+
return '';
|
|
394
|
+
return `- ${t.function.name}: ${t.function.description ?? ''}`;
|
|
395
|
+
})
|
|
396
|
+
.filter(Boolean)
|
|
397
|
+
.join('\n');
|
|
398
|
+
if (toolDescriptions && messages.length > 0 && isRecord(messages[0]) && messages[0].role === 'system') {
|
|
399
|
+
messages[0].content =
|
|
400
|
+
`${messages[0].content}\n\nAvailable MCP tools (call them by name in your response):\n${toolDescriptions}`;
|
|
401
|
+
}
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (!response.ok) {
|
|
405
|
+
throw new Error(`LLM error (${response.status})`);
|
|
406
|
+
}
|
|
407
|
+
const parsed = JSON.parse(await response.text());
|
|
408
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.choices) || !parsed.choices.length)
|
|
409
|
+
break;
|
|
410
|
+
const choice = parsed.choices[0];
|
|
411
|
+
if (!isRecord(choice) || !isRecord(choice.message))
|
|
412
|
+
break;
|
|
413
|
+
const msg = choice.message;
|
|
414
|
+
messages.push(msg);
|
|
415
|
+
if (Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0 && config.mcpEndpoint) {
|
|
416
|
+
for (const toolCall of msg.tool_calls) {
|
|
417
|
+
if (!isRecord(toolCall) || !isRecord(toolCall.function))
|
|
418
|
+
continue;
|
|
419
|
+
const fnName = typeof toolCall.function.name === 'string' ? toolCall.function.name : '';
|
|
420
|
+
const fnArgs = typeof toolCall.function.arguments === 'string'
|
|
421
|
+
? JSON.parse(toolCall.function.arguments)
|
|
422
|
+
: (toolCall.function.arguments ?? {});
|
|
423
|
+
let toolResult = '';
|
|
424
|
+
try {
|
|
425
|
+
const mcpResult = await callMcp(config.mcpEndpoint, 'tools/call', { name: fnName, arguments: fnArgs });
|
|
426
|
+
if (isRecord(mcpResult) && Array.isArray(mcpResult.content)) {
|
|
427
|
+
toolResult = mcpResult.content
|
|
428
|
+
.filter((content) => isRecord(content) && typeof content.text === 'string')
|
|
429
|
+
.map((content) => content.text)
|
|
430
|
+
.join('\n');
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
toolResult = JSON.stringify(mcpResult);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
toolResult = `Tool error: ${error instanceof Error ? error.message : String(error)}`;
|
|
438
|
+
}
|
|
439
|
+
messages.push({ role: 'tool', tool_call_id: toolCall.id ?? '', content: toolResult });
|
|
440
|
+
}
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (typeof msg.content === 'string' && msg.content.trim()) {
|
|
444
|
+
const content = msg.content.trim();
|
|
445
|
+
if (!expectedOutputMarker || content.includes(expectedOutputMarker)) {
|
|
446
|
+
return content;
|
|
447
|
+
}
|
|
448
|
+
messages.push({
|
|
449
|
+
role: 'user',
|
|
450
|
+
content: `Your previous answer did not include the required output marker "${expectedOutputMarker}". ` +
|
|
451
|
+
`Return the final answer now, include "${expectedOutputMarker}", and do not explain the correction.`,
|
|
452
|
+
});
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
clearTimeout(timer);
|
|
460
|
+
}
|
|
461
|
+
throw new Error(expectedOutputMarker
|
|
462
|
+
? `Agent response did not include required output marker: ${expectedOutputMarker}`
|
|
463
|
+
: 'Agent did not produce a response');
|
|
464
|
+
}
|
|
465
|
+
function agentRunMarkdown(args) {
|
|
466
|
+
const status = args.ok ? 'Success' : 'Failed';
|
|
467
|
+
const userInput = args.userInput.trim() || '_No user input._';
|
|
468
|
+
return `---\n**date:** ${new Date().toISOString()}\n**status:** ${status}\n**description:** Resultat d'execution de l'agent ${args.agent.label} via ${args.provider.label}.\n**tags:** agent, run-agent, workspace, llm, ${slugify(args.agent.label)}\n---\n\n# ${args.title}\n\n## Execution\n\n- Agent: ${args.agent.label}\n- Provider: ${args.provider.label}\n- Model: ${args.provider.config.model}\n- Status: ${status}\n\n## User input\n\n${userInput}\n\n## Response\n\n${args.content}\n`;
|
|
469
|
+
}
|
|
470
|
+
function createAgentRunDocument(docsPath, agent, provider, ok, userInput, content) {
|
|
471
|
+
const folder = sanitizeWorkspaceFolder(agent.config.workspaceFolder);
|
|
472
|
+
if (!folder)
|
|
473
|
+
throw new Error('agent workspace folder is missing');
|
|
474
|
+
const docsRoot = path_1.default.resolve(docsPath);
|
|
475
|
+
const targetDir = path_1.default.resolve(docsPath, folder);
|
|
476
|
+
if (!targetDir.startsWith(`${docsRoot}${path_1.default.sep}`) && targetDir !== docsRoot) {
|
|
477
|
+
throw new Error('agent workspace folder escapes docs folder');
|
|
478
|
+
}
|
|
479
|
+
fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
480
|
+
const title = `Run - ${agent.label}`;
|
|
481
|
+
const baseFilename = buildWorkspaceFilenameForDocs(docsPath, title, 'AGENT');
|
|
482
|
+
const baseName = baseFilename.replace(/\.md$/, '');
|
|
483
|
+
let filename = baseFilename;
|
|
484
|
+
let filePath = path_1.default.join(targetDir, filename);
|
|
485
|
+
let suffix = 2;
|
|
486
|
+
while (fs_1.default.existsSync(filePath)) {
|
|
487
|
+
filename = `${baseName}_${suffix}.md`;
|
|
488
|
+
filePath = path_1.default.join(targetDir, filename);
|
|
489
|
+
suffix += 1;
|
|
490
|
+
}
|
|
491
|
+
fs_1.default.writeFileSync(filePath, agentRunMarkdown({ title, agent, provider, ok, userInput, content }), 'utf-8');
|
|
492
|
+
const relativePath = path_1.default.relative(docsPath, filePath);
|
|
493
|
+
return {
|
|
494
|
+
id: encodeURIComponent(relativePath.slice(0, -3)),
|
|
495
|
+
filename: relativePath,
|
|
496
|
+
title,
|
|
497
|
+
ok,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function findWorkspaceAgentRunContext(state, agentId) {
|
|
501
|
+
const agent = state.entities.find((entity) => entity.id === agentId && entity.kind === 'agent');
|
|
502
|
+
if (!agent) {
|
|
503
|
+
throw new Error('agent not found');
|
|
504
|
+
}
|
|
505
|
+
const provider = state.entities.find((entity) => entity.id === agent.parentId && entity.kind === 'llm');
|
|
506
|
+
if (!provider) {
|
|
507
|
+
throw new Error('agent parent LLM provider not found');
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
agent,
|
|
511
|
+
provider,
|
|
512
|
+
mcp: state.entities.find((entity) => entity.kind === 'mcp') ?? null,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function buildWorkspaceFilenameForDocs(docsPath, title, category) {
|
|
516
|
+
const { filenamePattern } = (0, config_1.readConfig)(docsPath);
|
|
517
|
+
const now = new Date();
|
|
518
|
+
const yyyy = String(now.getFullYear());
|
|
519
|
+
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
|
520
|
+
const dd = String(now.getDate()).padStart(2, '0');
|
|
521
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
522
|
+
const min = String(now.getMinutes()).padStart(2, '0');
|
|
523
|
+
return (filenamePattern || 'YYYY_MM_DD_HH_mm_[Category]_title')
|
|
524
|
+
.replace('YYYY', yyyy)
|
|
525
|
+
.replace('MM', mm)
|
|
526
|
+
.replace('DD', dd)
|
|
527
|
+
.replace('HH', hh)
|
|
528
|
+
.replace('mm', min)
|
|
529
|
+
.replace(/\[Category\]/i, `[${category}]`)
|
|
530
|
+
.replace(/(?<![a-z0-9])(?:title_words|title)(?![a-z0-9])/i, slugify(title)) + '.md';
|
|
531
|
+
}
|
|
128
532
|
function workspaceRouter(docsPath) {
|
|
129
533
|
const router = (0, express_1.Router)();
|
|
130
534
|
router.get('/', (_req, res) => {
|
|
@@ -140,7 +544,7 @@ function workspaceRouter(docsPath) {
|
|
|
140
544
|
const state = sanitizeWorkspaceState({
|
|
141
545
|
...req.body,
|
|
142
546
|
updatedAt: new Date().toISOString(),
|
|
143
|
-
});
|
|
547
|
+
}, docsPath);
|
|
144
548
|
writeWorkspaceState(docsPath, state);
|
|
145
549
|
res.json(state);
|
|
146
550
|
}
|
|
@@ -149,6 +553,124 @@ function workspaceRouter(docsPath) {
|
|
|
149
553
|
res.status(400).json({ error: message });
|
|
150
554
|
}
|
|
151
555
|
});
|
|
556
|
+
router.post('/list-models', async (req, res) => {
|
|
557
|
+
try {
|
|
558
|
+
const body = req.body;
|
|
559
|
+
if (typeof body.endpoint !== 'string' || !body.endpoint.trim()) {
|
|
560
|
+
res.status(400).json({ ok: false, error: 'endpoint is required' });
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const url = llmModelsUrl(body.endpoint.trim());
|
|
564
|
+
const headers = { Accept: 'application/json' };
|
|
565
|
+
if (typeof body.token === 'string' && body.token.trim()) {
|
|
566
|
+
headers.Authorization = `Bearer ${body.token.trim()}`;
|
|
567
|
+
}
|
|
568
|
+
const controller = new AbortController();
|
|
569
|
+
const timeout = setTimeout(() => controller.abort(), DEFAULT_LLM_TEST_TIMEOUT_MS);
|
|
570
|
+
try {
|
|
571
|
+
const response = await fetch(url, { method: 'GET', headers, signal: controller.signal });
|
|
572
|
+
const text = await response.text();
|
|
573
|
+
let models = [];
|
|
574
|
+
try {
|
|
575
|
+
const parsed = JSON.parse(text);
|
|
576
|
+
if (isRecord(parsed) && Array.isArray(parsed.data)) {
|
|
577
|
+
models = parsed.data
|
|
578
|
+
.map((m) => (isRecord(m) && typeof m.id === 'string' ? m.id : null))
|
|
579
|
+
.filter((id) => id !== null);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
catch { /* ignore */ }
|
|
583
|
+
res.json({ ok: response.ok, models });
|
|
584
|
+
}
|
|
585
|
+
finally {
|
|
586
|
+
clearTimeout(timeout);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
const message = error instanceof Error ? error.message : 'Failed to list models';
|
|
591
|
+
res.status(400).json({ ok: false, error: message });
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
router.post('/run-agent-document', async (req, res) => {
|
|
595
|
+
try {
|
|
596
|
+
const body = req.body;
|
|
597
|
+
if (typeof body.agentId !== 'string' || !body.agentId.trim()) {
|
|
598
|
+
res.status(400).json({ ok: false, error: 'agentId is required' });
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const state = readWorkspaceState(docsPath);
|
|
602
|
+
ensureProviderWorkspaceFolders(docsPath, state);
|
|
603
|
+
writeWorkspaceState(docsPath, state);
|
|
604
|
+
const { agent, provider, mcp } = findWorkspaceAgentRunContext(state, body.agentId.trim());
|
|
605
|
+
const userInput = typeof body.userInput === 'string' ? body.userInput.trim() : '';
|
|
606
|
+
try {
|
|
607
|
+
const content = await runAgent({
|
|
608
|
+
endpoint: provider.config.endpoint,
|
|
609
|
+
token: provider.config.token,
|
|
610
|
+
model: agent.config.model || provider.config.model,
|
|
611
|
+
systemPrompt: agent.config.systemPrompt,
|
|
612
|
+
userInput,
|
|
613
|
+
timeout: agent.config.timeout || provider.config.timeout,
|
|
614
|
+
mcpEndpoint: mcp?.config.endpoint ?? '',
|
|
615
|
+
expectedOutputMarker: agent.config.expectedOutputMarker,
|
|
616
|
+
});
|
|
617
|
+
const document = createAgentRunDocument(docsPath, agent, provider, true, userInput, content);
|
|
618
|
+
res.json({ ok: true, content, document });
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
const message = error instanceof Error ? error.message : 'Agent run failed';
|
|
622
|
+
const document = createAgentRunDocument(docsPath, agent, provider, false, userInput, message);
|
|
623
|
+
res.status(502).json({ ok: false, error: message, document });
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
const message = error instanceof Error ? error.message : 'Agent run failed';
|
|
628
|
+
const status = message === 'agent not found' ? 404 : 400;
|
|
629
|
+
res.status(status).json({ ok: false, error: message });
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
router.post('/run-agent', async (req, res) => {
|
|
633
|
+
try {
|
|
634
|
+
const body = req.body;
|
|
635
|
+
if (typeof body.endpoint !== 'string' || !body.endpoint.trim()) {
|
|
636
|
+
res.status(400).json({ ok: false, error: 'endpoint is required' });
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (typeof body.model !== 'string' || !body.model.trim()) {
|
|
640
|
+
res.status(400).json({ ok: false, error: 'model is required' });
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (typeof body.systemPrompt !== 'string' || !body.systemPrompt.trim()) {
|
|
644
|
+
res.status(400).json({ ok: false, error: 'systemPrompt is required' });
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const content = await runAgent({
|
|
648
|
+
endpoint: body.endpoint,
|
|
649
|
+
token: typeof body.token === 'string' ? body.token : '',
|
|
650
|
+
model: body.model,
|
|
651
|
+
systemPrompt: body.systemPrompt,
|
|
652
|
+
userInput: typeof body.userInput === 'string' ? body.userInput : '',
|
|
653
|
+
timeout: clamp(safeFiniteNumber(body.timeout, 180), 1, 600),
|
|
654
|
+
mcpEndpoint: typeof body.mcpEndpoint === 'string' ? body.mcpEndpoint : '',
|
|
655
|
+
expectedOutputMarker: typeof body.expectedOutputMarker === 'string' ? body.expectedOutputMarker : '',
|
|
656
|
+
});
|
|
657
|
+
res.json({ ok: true, content });
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
const message = error instanceof Error ? error.message : 'Agent run failed';
|
|
661
|
+
res.status(400).json({ ok: false, error: message });
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
router.post('/test-llm', async (req, res) => {
|
|
665
|
+
try {
|
|
666
|
+
const result = await testLlmConnection(req.body);
|
|
667
|
+
res.status(result.ok ? 200 : 502).json(result);
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
const message = error instanceof Error ? error.message : 'Failed to test LLM connection';
|
|
671
|
+
res.status(400).json({ ok: false, error: message });
|
|
672
|
+
}
|
|
673
|
+
});
|
|
152
674
|
return router;
|
|
153
675
|
}
|
|
154
676
|
//# sourceMappingURL=workspace.js.map
|