nothumanallowed 15.1.4 → 15.1.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/package.json +1 -1
- package/src/constants.mjs +1 -1
- package/src/server/routes/studio.mjs +229 -27
- package/src/services/tool-executor.mjs +63 -9
- package/src/services/web-tools.mjs +611 -284
- package/src/ui-dist/assets/{index-B9Bd4Mrx.js → index-Chhxajp8.js} +92 -68
- package/src/ui-dist/index.html +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "15.1.
|
|
3
|
+
"version": "15.1.5",
|
|
4
4
|
"description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/constants.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
7
|
|
|
8
|
-
export const VERSION = '15.
|
|
8
|
+
export const VERSION = '15.1.5';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -9,7 +9,7 @@ import { sendJSON, sendError, parseBody } from '../index.mjs';
|
|
|
9
9
|
import { loadConfig } from '../../config.mjs';
|
|
10
10
|
import { NHA_DIR, AGENTS_DIR } from '../../constants.mjs';
|
|
11
11
|
import { callLLM, callLLMStream, parseAgentFile } from '../../services/llm.mjs';
|
|
12
|
-
import { webSearch, fetchUrl } from '../../services/web-tools.mjs';
|
|
12
|
+
import { webSearch, fetchUrl, fetchUrlRich, headProbe } from '../../services/web-tools.mjs';
|
|
13
13
|
|
|
14
14
|
// Agents that get web data pre-fetched before their LLM call
|
|
15
15
|
const WEB_TOOL_AGENTS = new Set([
|
|
@@ -94,20 +94,65 @@ async function runWebSearch(query) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
|
-
* Fetch a URL and return
|
|
97
|
+
* Fetch a URL and return a structured result. Caller decides how to format it.
|
|
98
|
+
* Returns { ok, url, content?, title?, metadata?, code?, reason? }.
|
|
98
99
|
*/
|
|
99
100
|
async function runFetchUrl(url) {
|
|
100
101
|
try {
|
|
101
|
-
const result = await
|
|
102
|
-
if (result.error)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
const result = await fetchUrlRich(url, { maxChars: 16_000 });
|
|
103
|
+
if (result.error) {
|
|
104
|
+
return { ok: false, url, code: result.code || 'FETCH_FAILED', reason: result.message || 'unknown' };
|
|
105
|
+
}
|
|
106
|
+
const meta = result.metadata || {};
|
|
107
|
+
const ogBlock = Object.keys(meta.og || {}).length
|
|
108
|
+
? `OpenGraph: ${Object.entries(meta.og).slice(0, 8).map(([k, v]) => `${k}=${String(v).slice(0, 200)}`).join(' | ')}\n`
|
|
109
|
+
: '';
|
|
110
|
+
const ldBlock = (meta.jsonLd || []).length
|
|
111
|
+
? `Schema.org types: ${meta.jsonLd.slice(0, 6).map(j => j.type).join(', ')}\n`
|
|
112
|
+
: '';
|
|
113
|
+
const headingsBlock = meta.headings && (meta.headings.h1?.length || meta.headings.h2?.length)
|
|
114
|
+
? `Headings H1: ${(meta.headings.h1 || []).slice(0, 5).join(' | ')}\n` +
|
|
115
|
+
(meta.headings.h2?.length ? `Headings H2: ${meta.headings.h2.slice(0, 8).join(' | ')}\n` : '')
|
|
116
|
+
: '';
|
|
117
|
+
const descBlock = meta.description ? `Description: ${meta.description}\n` : '';
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
url: result.url || url,
|
|
121
|
+
title: result.title || '',
|
|
122
|
+
content: result.body || '',
|
|
123
|
+
metadata: meta,
|
|
124
|
+
preamble: `${result.title ? `Title: ${result.title}\n` : ''}${descBlock}${ogBlock}${ldBlock}${headingsBlock}`.trim(),
|
|
125
|
+
};
|
|
106
126
|
} catch (e) {
|
|
107
|
-
return
|
|
127
|
+
return { ok: false, url, code: 'EXCEPTION', reason: e.message || String(e) };
|
|
108
128
|
}
|
|
109
129
|
}
|
|
110
130
|
|
|
131
|
+
/** Extract HTTP/HTTPS URLs from a free-form string, deduplicated, max 6. */
|
|
132
|
+
function extractUrlsFromText(text) {
|
|
133
|
+
const urls = [...(text || '').matchAll(/https?:\/\/[^\s,)<>"'`]+/gi)].map(m => m[0].replace(/[.,;:!?)\]]+$/, ''));
|
|
134
|
+
return [...new Set(urls)].slice(0, 6);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Strip transient status markers, internal think/tool blocks, and propagation
|
|
139
|
+
* noise before the text is injected into another agent's prompt or surfaced
|
|
140
|
+
* to the client. Parity with LegionX `CommunicationStream.stripTags`.
|
|
141
|
+
*/
|
|
142
|
+
function sanitizeAgentOutput(text) {
|
|
143
|
+
if (!text) return '';
|
|
144
|
+
return String(text)
|
|
145
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, '')
|
|
146
|
+
.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
|
|
147
|
+
.replace(/<tool[^>]*>[\s\S]*?<\/tool>/gi, '')
|
|
148
|
+
.replace(/<scratch[^>]*>[\s\S]*?<\/scratch>/gi, '')
|
|
149
|
+
.replace(/<reasoning>[\s\S]*?<\/reasoning>/gi, '')
|
|
150
|
+
.replace(/\[(?:Searching|Fetching|GET|POST|HEAD|Raccolta dati web|Parlamento)[^\]]*\]\s*/gi, '')
|
|
151
|
+
.replace(/|/g, '') // zero-width / BOM
|
|
152
|
+
.replace(/\r\n/g, '\n')
|
|
153
|
+
.trim();
|
|
154
|
+
}
|
|
155
|
+
|
|
111
156
|
/**
|
|
112
157
|
* Extract search queries from a task string.
|
|
113
158
|
* Returns up to 3 queries covering different angles.
|
|
@@ -257,8 +302,24 @@ Select the optimal agent pipeline. Output ONLY the JSON.`;
|
|
|
257
302
|
} catch {}
|
|
258
303
|
}
|
|
259
304
|
|
|
260
|
-
const contextBlock = context ? `\n\n## CONTEXT FROM PREVIOUS STEPS:\n${context.slice(0, 12000)}` : '';
|
|
261
|
-
|
|
305
|
+
const contextBlock = context ? `\n\n## CONTEXT FROM PREVIOUS STEPS:\n${sanitizeAgentOutput(context).slice(0, 12000)}` : '';
|
|
306
|
+
|
|
307
|
+
// Cross-reading block — when other agents have already produced proposals,
|
|
308
|
+
// we inject them with explicit deliberation instructions so the model
|
|
309
|
+
// engages with them (agree/disagree) rather than restating its own view.
|
|
310
|
+
const proposalContextBlock = body.proposalContext ? `
|
|
311
|
+
|
|
312
|
+
## ALTRE PROPOSTE GIÀ COMPLETATE (CROSS-READING)
|
|
313
|
+
|
|
314
|
+
I seguenti output sono proposte di altri agenti che hanno lavorato sullo stesso task. Leggile attentamente prima di rispondere.
|
|
315
|
+
|
|
316
|
+
${sanitizeAgentOutput(body.proposalContext).slice(0, 12000)}
|
|
317
|
+
|
|
318
|
+
### ISTRUZIONI DI DELIBERAZIONE (obbligatorie)
|
|
319
|
+
1. **Identifica i punti in cui CONCORDI** con un agente precedente → marca con \`[✓ CONCORDA CON: NomeAgente]\` e cita il punto specifico.
|
|
320
|
+
2. **Identifica i punti in cui DISSENTI** → marca con \`[✗ DISACCORDO CON: NomeAgente]\` e spiega il motivo con evidenze concrete (dai dati fetched, non opinioni).
|
|
321
|
+
3. **NON ripetere ciò che hanno già detto bene** — aggiungi il TUO contributo specifico dalla TUA specializzazione.
|
|
322
|
+
4. **Se i dati raccolti contraddicono un agente precedente**, segnalalo esplicitamente.` : '';
|
|
262
323
|
|
|
263
324
|
const formatInstructions = `\n\nFORMATTING RULES (CRITICAL — your output will be rendered as HTML):
|
|
264
325
|
- Use MARKDOWN TABLES with | pipes | for ALL tabular data. Example:
|
|
@@ -284,27 +345,62 @@ Select the optimal agent pipeline. Output ONLY the JSON.`;
|
|
|
284
345
|
const userMessage = stepDef?.prompt || task;
|
|
285
346
|
|
|
286
347
|
const useWebTools = WEB_TOOL_AGENTS.has(agent);
|
|
348
|
+
const isPrimaryResearch = !!body.isPrimaryResearch || !!stepDef?.isPrimaryResearch;
|
|
287
349
|
let webDataBlock = '';
|
|
350
|
+
const fetchOutcomes = { ok: 0, failed: 0, attempted: 0, failures: [] };
|
|
288
351
|
|
|
289
|
-
//
|
|
290
|
-
const taskUrls =
|
|
291
|
-
if (taskUrls.length > 0
|
|
292
|
-
sse({ token:
|
|
352
|
+
// ── Pre-fetch URLs declared in the task ─────────────────────────
|
|
353
|
+
const taskUrls = extractUrlsFromText(task);
|
|
354
|
+
if (taskUrls.length > 0) {
|
|
355
|
+
sse({ token: `[Fetching ${taskUrls.length} URL(s) from task...]` });
|
|
293
356
|
const urlFetches = await Promise.all(
|
|
294
|
-
taskUrls.slice(0,
|
|
295
|
-
sse({ token: `[
|
|
296
|
-
|
|
357
|
+
taskUrls.slice(0, 4).map(async (url) => {
|
|
358
|
+
sse({ token: `[GET ${url}]` });
|
|
359
|
+
const r = await runFetchUrl(url);
|
|
360
|
+
fetchOutcomes.attempted++;
|
|
361
|
+
if (r.ok) fetchOutcomes.ok++;
|
|
362
|
+
else { fetchOutcomes.failed++; fetchOutcomes.failures.push({ url: r.url, code: r.code, reason: r.reason }); }
|
|
363
|
+
return r;
|
|
297
364
|
})
|
|
298
365
|
);
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
366
|
+
|
|
367
|
+
const okFetches = urlFetches.filter(f => f.ok);
|
|
368
|
+
const failedFetches = urlFetches.filter(f => !f.ok);
|
|
369
|
+
|
|
370
|
+
if (okFetches.length > 0) {
|
|
371
|
+
const okBlock = okFetches.map(f => {
|
|
372
|
+
const preamble = f.preamble ? `${f.preamble}\n\n` : '';
|
|
373
|
+
const content = (f.content || '').slice(0, 12_000);
|
|
374
|
+
return `### Content from ${f.url}\n${preamble}${content}`;
|
|
375
|
+
}).join('\n\n---\n\n');
|
|
376
|
+
webDataBlock = `\n\n## FETCHED WEBSITE CONTENT (THIS IS THE GROUND TRUTH — base your analysis EXCLUSIVELY on this. Do NOT invent products, sectors, brands, or facts that are not in this block.):\n\n${okBlock}`;
|
|
302
377
|
}
|
|
378
|
+
|
|
379
|
+
if (failedFetches.length > 0) {
|
|
380
|
+
const failBlock = failedFetches.map(f => `- ${f.url} → ${f.code}: ${f.reason}`).join('\n');
|
|
381
|
+
webDataBlock += `\n\n## ❌ URL NON RAGGIUNGIBILI (NON inventare contenuto su questi siti — scrivi esplicitamente "fonte non disponibile" nelle sezioni che li riguardano):\n${failBlock}`;
|
|
382
|
+
sse({ data_unavailable: failedFetches.map(f => ({ url: f.url, code: f.code, reason: f.reason })) });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Fail-fast: primary research agent and ALL declared URLs failed → abort.
|
|
386
|
+
if (isPrimaryResearch && okFetches.length === 0 && taskUrls.length > 0) {
|
|
387
|
+
clearInterval(keepalive);
|
|
388
|
+
sse({
|
|
389
|
+
aborted: true,
|
|
390
|
+
reason: 'PRIMARY_RESEARCH_FAILED',
|
|
391
|
+
message: 'Tutti gli URL del task sono irraggiungibili. Pipeline interrotta per evitare contenuto inventato.',
|
|
392
|
+
failures: fetchOutcomes.failures,
|
|
393
|
+
});
|
|
394
|
+
res.write('data: [DONE]\n\n');
|
|
395
|
+
res.end();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
303
399
|
sse({ token: '\n' });
|
|
304
400
|
}
|
|
305
401
|
|
|
306
|
-
//
|
|
307
|
-
if (useWebTools
|
|
402
|
+
// ── Web-search agents: search-driven data collection ────────────
|
|
403
|
+
if (useWebTools) {
|
|
308
404
|
sse({ token: '[Raccolta dati web...]' });
|
|
309
405
|
|
|
310
406
|
const queries = extractSearchQueries(task, stepDef?.prompt);
|
|
@@ -324,7 +420,11 @@ Select the optimal agent pipeline. Output ONLY the JSON.`;
|
|
|
324
420
|
const fetchResults = await Promise.all(
|
|
325
421
|
urlMatches.map(async (url) => {
|
|
326
422
|
sse({ token: `[Fetching: ${url}]` });
|
|
327
|
-
|
|
423
|
+
const r = await runFetchUrl(url);
|
|
424
|
+
fetchOutcomes.attempted++;
|
|
425
|
+
if (r.ok) fetchOutcomes.ok++;
|
|
426
|
+
else fetchOutcomes.failed++;
|
|
427
|
+
return r;
|
|
328
428
|
})
|
|
329
429
|
);
|
|
330
430
|
|
|
@@ -332,11 +432,19 @@ Select the optimal agent pipeline. Output ONLY the JSON.`;
|
|
|
332
432
|
.map((s) => `### Search: "${s.query}"\n${s.result}`)
|
|
333
433
|
.join('\n\n---\n\n');
|
|
334
434
|
|
|
335
|
-
const fetchBlock = fetchResults
|
|
336
|
-
|
|
435
|
+
const fetchBlock = fetchResults
|
|
436
|
+
.filter(f => f.ok)
|
|
437
|
+
.map(f => `### Full content: ${f.url}\n${f.preamble ? f.preamble + '\n\n' : ''}${(f.content || '').slice(0, 8_000)}`)
|
|
438
|
+
.join('\n\n---\n\n');
|
|
439
|
+
|
|
440
|
+
const failedFetches = fetchResults.filter(f => !f.ok);
|
|
441
|
+
const failBlock = failedFetches.length > 0
|
|
442
|
+
? `\n\n## URL non raggiungibili durante la ricerca:\n${failedFetches.map(f => `- ${f.url} → ${f.code}`).join('\n')}`
|
|
337
443
|
: '';
|
|
338
444
|
|
|
339
|
-
|
|
445
|
+
const collected = `${searchBlock}${fetchBlock ? '\n\n---\n\n' + fetchBlock : ''}${failBlock}`;
|
|
446
|
+
webDataBlock = (webDataBlock || '') +
|
|
447
|
+
`\n\n## REAL-TIME WEB DATA (use ONLY this data — do NOT invent prices, figures, brands, sectors):\n\n${collected}`;
|
|
340
448
|
|
|
341
449
|
sse({ token: '\n' });
|
|
342
450
|
}
|
|
@@ -355,7 +463,17 @@ Select the optimal agent pipeline. Output ONLY the JSON.`;
|
|
|
355
463
|
}, { max_tokens: 16384 });
|
|
356
464
|
|
|
357
465
|
clearInterval(keepalive);
|
|
358
|
-
|
|
466
|
+
// Strip internal think/tool blocks before emitting the final output —
|
|
467
|
+
// the streamed tokens may contain them. Quality is computed on the clean
|
|
468
|
+
// text so think-blocks don't inflate output_length artificially.
|
|
469
|
+
const cleanOutput = sanitizeAgentOutput(output);
|
|
470
|
+
const dataQuality = computeDataQuality({
|
|
471
|
+
fetchOutcomes,
|
|
472
|
+
output: cleanOutput,
|
|
473
|
+
taskUrls,
|
|
474
|
+
useWebTools,
|
|
475
|
+
});
|
|
476
|
+
sse({ done: true, output: cleanOutput, tokensOut, dataQuality, fetchOutcomes });
|
|
359
477
|
res.write('data: [DONE]\n\n');
|
|
360
478
|
res.end();
|
|
361
479
|
} catch (e) {
|
|
@@ -365,6 +483,30 @@ Select the optimal agent pipeline. Output ONLY the JSON.`;
|
|
|
365
483
|
}
|
|
366
484
|
});
|
|
367
485
|
|
|
486
|
+
// ── /api/studio/smoke-test — Pre-run reachability check ──────────────
|
|
487
|
+
router.post('/api/studio/smoke-test', async (req, res) => {
|
|
488
|
+
try {
|
|
489
|
+
const body = await parseBody(req);
|
|
490
|
+
let urls = Array.isArray(body.urls) ? body.urls : [];
|
|
491
|
+
if (!urls.length && typeof body.task === 'string') urls = extractUrlsFromText(body.task);
|
|
492
|
+
if (!urls.length) return sendJSON(res, 200, { probes: [], allOk: true });
|
|
493
|
+
|
|
494
|
+
const probes = await Promise.all(
|
|
495
|
+
urls.slice(0, 8).map(async (url) => {
|
|
496
|
+
try {
|
|
497
|
+
const r = await headProbe(url);
|
|
498
|
+
return { url, ok: r.ok, status: r.status, reason: r.reason || '', finalUrl: r.finalUrl };
|
|
499
|
+
} catch (e) {
|
|
500
|
+
return { url, ok: false, status: 0, reason: e.message || String(e) };
|
|
501
|
+
}
|
|
502
|
+
})
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
const allOk = probes.every(p => p.ok);
|
|
506
|
+
sendJSON(res, 200, { probes, allOk });
|
|
507
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
508
|
+
});
|
|
509
|
+
|
|
368
510
|
// ── /api/studio/deliberate — Parliament Geth Consensus ───────────────
|
|
369
511
|
router.post('/api/studio/deliberate', async (req, res) => {
|
|
370
512
|
const body = await parseBody(req);
|
|
@@ -470,6 +612,66 @@ Select the optimal agent pipeline. Output ONLY the JSON.`;
|
|
|
470
612
|
});
|
|
471
613
|
}
|
|
472
614
|
|
|
615
|
+
// ── Data-quality scoring (replaces fake sentiment "confidence") ────────
|
|
616
|
+
/**
|
|
617
|
+
* Compute a 0..1 data-quality score from observable signals.
|
|
618
|
+
*
|
|
619
|
+
* Signals (weighted):
|
|
620
|
+
* - fetch_success_ratio: fraction of attempted fetches that succeeded (0.45)
|
|
621
|
+
* - declared_urls_ok: primary URLs from the task were reachable (0.25)
|
|
622
|
+
* - output_length: produced a substantive response (>= 600 chars) (0.10)
|
|
623
|
+
* - no_error_strings: output free of "fetch failed", "search failed" (0.10)
|
|
624
|
+
* - structured_evidence: output cites numbers, dates, or quoted strings (0.10)
|
|
625
|
+
*
|
|
626
|
+
* Returns { score: number, label: 'high'|'medium'|'low', signals: {...} }.
|
|
627
|
+
*/
|
|
628
|
+
export function computeDataQuality({ fetchOutcomes, output = '', taskUrls = [], useWebTools = false }) {
|
|
629
|
+
const out = String(output || '');
|
|
630
|
+
const signals = {};
|
|
631
|
+
|
|
632
|
+
const totalAttempted = fetchOutcomes?.attempted || 0;
|
|
633
|
+
const totalOk = fetchOutcomes?.ok || 0;
|
|
634
|
+
if (totalAttempted > 0) {
|
|
635
|
+
signals.fetch_success_ratio = totalOk / totalAttempted;
|
|
636
|
+
} else if (useWebTools || taskUrls.length > 0) {
|
|
637
|
+
signals.fetch_success_ratio = 0;
|
|
638
|
+
} else {
|
|
639
|
+
signals.fetch_success_ratio = 1; // No fetches expected → neutral
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const declaredFailures = (fetchOutcomes?.failures || []).filter(f => taskUrls.includes(f.url)).length;
|
|
643
|
+
if (taskUrls.length === 0) {
|
|
644
|
+
signals.declared_urls_ok = 1;
|
|
645
|
+
} else {
|
|
646
|
+
signals.declared_urls_ok = Math.max(0, 1 - declaredFailures / taskUrls.length);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
signals.output_length = Math.min(1, out.length / 1_200);
|
|
650
|
+
|
|
651
|
+
const errorMarkers = /fetch failed|search failed|errore 500|errore 404|http_500|http_404|http_429|http_4\d{2}|timeout|network error/gi;
|
|
652
|
+
const errorCount = (out.match(errorMarkers) || []).length;
|
|
653
|
+
signals.no_error_strings = Math.max(0, 1 - errorCount / 5);
|
|
654
|
+
|
|
655
|
+
const hasNumbers = /\b\d{1,4}(?:[.,]\d+)?(?:\s*(%|€|\$|£|¥|kg|cm|mm|km|°c))?/i.test(out);
|
|
656
|
+
const hasDates = /\b(19|20)\d{2}\b|\b\d{1,2}[\/\-.]\d{1,2}[\/\-.](19|20)?\d{2}\b/.test(out);
|
|
657
|
+
const hasQuotes = /"[^"]{8,}"|"[^"]{8,}"/.test(out);
|
|
658
|
+
const structScore = (hasNumbers ? 0.5 : 0) + (hasDates ? 0.25 : 0) + (hasQuotes ? 0.25 : 0);
|
|
659
|
+
signals.structured_evidence = Math.min(1, structScore);
|
|
660
|
+
|
|
661
|
+
const score =
|
|
662
|
+
0.45 * signals.fetch_success_ratio +
|
|
663
|
+
0.25 * signals.declared_urls_ok +
|
|
664
|
+
0.10 * signals.output_length +
|
|
665
|
+
0.10 * signals.no_error_strings +
|
|
666
|
+
0.10 * signals.structured_evidence;
|
|
667
|
+
|
|
668
|
+
let label = 'low';
|
|
669
|
+
if (score >= 0.70) label = 'high';
|
|
670
|
+
else if (score >= 0.50) label = 'medium';
|
|
671
|
+
|
|
672
|
+
return { score: Math.round(score * 100) / 100, label, signals };
|
|
673
|
+
}
|
|
674
|
+
|
|
473
675
|
// ── Keyword fallback planner (used when LLM is unavailable) ────────────
|
|
474
676
|
|
|
475
677
|
function buildKeywordFallback(task, sanitizedTask, hasPdf, pdfName, it) {
|
|
@@ -2029,15 +2029,45 @@ export async function executeTool(action, params, config) {
|
|
|
2029
2029
|
const url = params.url;
|
|
2030
2030
|
if (!url) return 'A URL is required.';
|
|
2031
2031
|
|
|
2032
|
-
|
|
2033
|
-
|
|
2032
|
+
// Use the rich variant so we get OpenGraph / JSON-LD / headings.
|
|
2033
|
+
// The structured preamble dramatically reduces hallucination on sites
|
|
2034
|
+
// where the body alone is ambiguous (e.g. SPA shells, B2B catalogs).
|
|
2035
|
+
const result = await wt.fetchUrlRich(url);
|
|
2036
|
+
if (result.error) {
|
|
2037
|
+
return `Fetch error (${result.code || 'UNKNOWN'}): ${result.message}. ` +
|
|
2038
|
+
`Try web_search to find an alternative source, or browser_open for JS-rendered pages.`;
|
|
2039
|
+
}
|
|
2034
2040
|
|
|
2041
|
+
const meta = result.metadata || {};
|
|
2035
2042
|
const lines = [];
|
|
2036
2043
|
if (result.title) lines.push(`Title: ${result.title}`);
|
|
2037
2044
|
lines.push(`URL: ${result.url || url}`);
|
|
2038
|
-
lines.push(`Status: ${result.status}`);
|
|
2045
|
+
lines.push(`Status: ${result.status} Bytes: ${result.bytes} Attempts: ${result.attempts}`);
|
|
2046
|
+
if (meta.description) lines.push(`Description: ${meta.description}`);
|
|
2047
|
+
if (meta.lang) lines.push(`Lang: ${meta.lang}`);
|
|
2048
|
+
if (meta.canonical) lines.push(`Canonical: ${meta.canonical}`);
|
|
2049
|
+
|
|
2050
|
+
const ogKeys = Object.keys(meta.og || {});
|
|
2051
|
+
if (ogKeys.length) {
|
|
2052
|
+
const ogPairs = ogKeys.slice(0, 8).map(k => `${k}=${String(meta.og[k]).slice(0, 240)}`);
|
|
2053
|
+
lines.push(`OpenGraph: ${ogPairs.join(' | ')}`);
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
const ldTypes = (meta.jsonLd || []).map(j => j.type).filter(Boolean);
|
|
2057
|
+
if (ldTypes.length) {
|
|
2058
|
+
lines.push(`Schema.org types found: ${[...new Set(ldTypes)].join(', ')}`);
|
|
2059
|
+
const ldNames = (meta.jsonLd || []).map(j => j.name).filter(Boolean).slice(0, 5);
|
|
2060
|
+
if (ldNames.length) lines.push(`Schema.org entities: ${ldNames.join(' | ')}`);
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
if (meta.headings) {
|
|
2064
|
+
if (meta.headings.h1?.length) lines.push(`H1: ${meta.headings.h1.slice(0, 3).join(' | ')}`);
|
|
2065
|
+
if (meta.headings.h2?.length) lines.push(`H2: ${meta.headings.h2.slice(0, 6).join(' | ')}`);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2039
2068
|
if (result.truncated) lines.push('[Content was truncated due to size limits]');
|
|
2040
2069
|
lines.push('');
|
|
2070
|
+
lines.push('--- MAIN CONTENT ---');
|
|
2041
2071
|
lines.push(result.body);
|
|
2042
2072
|
|
|
2043
2073
|
return lines.join('\n');
|
|
@@ -2047,13 +2077,37 @@ export async function executeTool(action, params, config) {
|
|
|
2047
2077
|
case 'get_weather': {
|
|
2048
2078
|
const location = (params.location || '').trim();
|
|
2049
2079
|
if (!location) return 'A location is required (e.g. "Rome", "Viterbo, Italy").';
|
|
2080
|
+
const encodedLoc = encodeURIComponent(location);
|
|
2081
|
+
const WTTR_UA = 'Mozilla/5.0 (compatible; nha-weather/1.0; +https://nothumanallowed.com)';
|
|
2082
|
+
const fetchWithRetry = async (attempts = 3) => {
|
|
2083
|
+
let lastErr = null;
|
|
2084
|
+
for (let i = 0; i < attempts; i++) {
|
|
2085
|
+
try {
|
|
2086
|
+
const r = await fetch(`https://wttr.in/${encodedLoc}?format=j1`, {
|
|
2087
|
+
headers: { 'User-Agent': WTTR_UA, 'Accept': 'application/json' },
|
|
2088
|
+
signal: AbortSignal.timeout(10_000),
|
|
2089
|
+
});
|
|
2090
|
+
if (r.ok) return r;
|
|
2091
|
+
// 429/5xx → retry; 4xx other → don't retry
|
|
2092
|
+
if (r.status === 429 || (r.status >= 500 && r.status < 600)) {
|
|
2093
|
+
await new Promise(rs => setTimeout(rs, 600 * (i + 1) + Math.random() * 400));
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
return r;
|
|
2097
|
+
} catch (e) {
|
|
2098
|
+
lastErr = e;
|
|
2099
|
+
if (i < attempts - 1) await new Promise(rs => setTimeout(rs, 600 * (i + 1)));
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
if (lastErr) throw lastErr;
|
|
2103
|
+
return null;
|
|
2104
|
+
};
|
|
2050
2105
|
try {
|
|
2051
|
-
const
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
}
|
|
2056
|
-
if (!wttrRes.ok) return `Weather service returned ${wttrRes.status} for "${location}". Try a different location name.`;
|
|
2106
|
+
const wttrRes = await fetchWithRetry(3);
|
|
2107
|
+
if (!wttrRes || !wttrRes.ok) {
|
|
2108
|
+
const status = wttrRes ? wttrRes.status : 'NET_ERR';
|
|
2109
|
+
return `Weather service returned ${status} for "${location}". Try a more specific location (e.g. "Rome, Italy") or use web_search("weather ${location}") as fallback.`;
|
|
2110
|
+
}
|
|
2057
2111
|
const w = await wttrRes.json();
|
|
2058
2112
|
const cur = w.current_condition?.[0];
|
|
2059
2113
|
const area = w.nearest_area?.[0];
|