nothumanallowed 15.1.60 → 15.1.62
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "15.1.
|
|
3
|
+
"version": "15.1.62",
|
|
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.1.
|
|
8
|
+
export const VERSION = '15.1.62';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -1045,7 +1045,15 @@ class TelegramResponder {
|
|
|
1045
1045
|
// appointments of May". By running the calendar tool server-side, the
|
|
1046
1046
|
// user always sees REAL data — never fabricated.
|
|
1047
1047
|
this._lastDirectAuditChatId = chatId;
|
|
1048
|
-
|
|
1048
|
+
// Run the per-domain direct-action dispatcher. First match wins; falls
|
|
1049
|
+
// through to LLM if no handler claims the message.
|
|
1050
|
+
const directFresh =
|
|
1051
|
+
await this._tryDirectFreshCalendarAction(cleanText, this.config) ||
|
|
1052
|
+
await this._tryDirectFreshEmailAction(cleanText, this.config) ||
|
|
1053
|
+
await this._tryDirectFreshTaskAction(cleanText, this.config) ||
|
|
1054
|
+
await this._tryDirectFreshNoteAction(cleanText, this.config) ||
|
|
1055
|
+
await this._tryDirectFreshReminderAction(cleanText, this.config) ||
|
|
1056
|
+
await this._tryDirectFreshSlackAction(cleanText, this.config);
|
|
1049
1057
|
if (directFresh) {
|
|
1050
1058
|
this.log(`[Telegram] ${fromUser}: direct-fresh ${directFresh.action} → ${directFresh.success ? 'OK' : 'FAIL'}`);
|
|
1051
1059
|
const personaName = this.config.responder?.telegram?.botName || this.config.responder?.botName || '';
|
|
@@ -1407,6 +1415,254 @@ class TelegramResponder {
|
|
|
1407
1415
|
return `${parseInt(m[3], 10)} ${months[parseInt(m[2], 10) - 1]} ${m[1]}`;
|
|
1408
1416
|
}
|
|
1409
1417
|
|
|
1418
|
+
/** Add N minutes to an ISO datetime string. Returns ISO. */
|
|
1419
|
+
_addMinutesIso(isoStart, minutes) {
|
|
1420
|
+
try {
|
|
1421
|
+
const d = new Date(isoStart);
|
|
1422
|
+
d.setMinutes(d.getMinutes() + minutes);
|
|
1423
|
+
return d.toISOString();
|
|
1424
|
+
} catch { return isoStart; }
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* NLU only — extract structured calendar-create params from a natural
|
|
1429
|
+
* language message using a tiny LLM call. We pin output to strict JSON and
|
|
1430
|
+
* accept the result only if the title + start datetime are present.
|
|
1431
|
+
* The LLM NEVER executes anything — it only parses.
|
|
1432
|
+
*/
|
|
1433
|
+
async _nluExtractCalendarCreate(userMessage, config) {
|
|
1434
|
+
const todayIso = new Date().toISOString().slice(0, 10);
|
|
1435
|
+
const sysPrompt =
|
|
1436
|
+
`You extract calendar event parameters from natural language.\n` +
|
|
1437
|
+
`Today is ${todayIso}. Output ONLY a JSON object with these keys:\n` +
|
|
1438
|
+
` - title (string, the event subject, NOT including verbs like "fissa"/"crea")\n` +
|
|
1439
|
+
` - start (string, ISO datetime "YYYY-MM-DDTHH:MM:00" in local time)\n` +
|
|
1440
|
+
` - end (string, ISO datetime same format; if duration unknown, leave null and default 60 min will be applied)\n` +
|
|
1441
|
+
` - description (string, optional notes)\n` +
|
|
1442
|
+
`Rules: if a field is missing, use null. If the user says "domani" → ${this._addDaysIso(todayIso, 1).slice(0, 10)}. ` +
|
|
1443
|
+
`If "lunedì/martedì/..." resolve to the next occurrence. If no time is given default to 09:00. ` +
|
|
1444
|
+
`Output ONLY the JSON, no prose.`;
|
|
1445
|
+
try {
|
|
1446
|
+
const raw = await callLLM(config, sysPrompt, userMessage, { temperature: 0, maxTokens: 200 });
|
|
1447
|
+
const json = this._extractJsonObject(raw);
|
|
1448
|
+
if (!json) return null;
|
|
1449
|
+
if (!json.title || !json.start) return null;
|
|
1450
|
+
// Sanity: start must look like an ISO datetime.
|
|
1451
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(json.start)) return null;
|
|
1452
|
+
return json;
|
|
1453
|
+
} catch { return null; }
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
async _nluExtractCalendarMove(userMessage, config) {
|
|
1457
|
+
const todayIso = new Date().toISOString().slice(0, 10);
|
|
1458
|
+
const sysPrompt =
|
|
1459
|
+
`You extract reschedule-event parameters from natural language.\n` +
|
|
1460
|
+
`Today is ${todayIso}. Output ONLY a JSON object:\n` +
|
|
1461
|
+
` - title (string, the event to find and move)\n` +
|
|
1462
|
+
` - oldDate (string, original date if mentioned, format "YYYY-MM-DD"; else null)\n` +
|
|
1463
|
+
` - newStart (string, NEW datetime "YYYY-MM-DDTHH:MM:00")\n` +
|
|
1464
|
+
` - newEnd (string, optional, same format)\n` +
|
|
1465
|
+
`If "domani" → ${this._addDaysIso(todayIso, 1).slice(0, 10)}. Output JSON only.`;
|
|
1466
|
+
try {
|
|
1467
|
+
const raw = await callLLM(config, sysPrompt, userMessage, { temperature: 0, maxTokens: 200 });
|
|
1468
|
+
const json = this._extractJsonObject(raw);
|
|
1469
|
+
if (!json) return null;
|
|
1470
|
+
if (!json.newStart) return null;
|
|
1471
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(json.newStart)) return null;
|
|
1472
|
+
return json;
|
|
1473
|
+
} catch { return null; }
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/** Robust JSON object extractor — strips fences, finds the first balanced {...}. */
|
|
1477
|
+
_extractJsonObject(text) {
|
|
1478
|
+
if (!text) return null;
|
|
1479
|
+
const cleaned = String(text).replace(/```(?:json)?\s*/gi, '').replace(/```/g, '').trim();
|
|
1480
|
+
const start = cleaned.indexOf('{');
|
|
1481
|
+
if (start < 0) return null;
|
|
1482
|
+
let depth = 0, end = -1, inStr = false, esc = false;
|
|
1483
|
+
for (let i = start; i < cleaned.length; i++) {
|
|
1484
|
+
const c = cleaned[i];
|
|
1485
|
+
if (esc) { esc = false; continue; }
|
|
1486
|
+
if (c === '\\' && inStr) { esc = true; continue; }
|
|
1487
|
+
if (c === '"' && !esc) inStr = !inStr;
|
|
1488
|
+
if (inStr) continue;
|
|
1489
|
+
if (c === '{') depth++;
|
|
1490
|
+
else if (c === '}') { depth--; if (depth === 0) { end = i; break; } }
|
|
1491
|
+
}
|
|
1492
|
+
if (end < 0) return null;
|
|
1493
|
+
try { return JSON.parse(cleaned.slice(start, end + 1)); } catch { return null; }
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
_addDaysIso(isoDate, days) {
|
|
1497
|
+
const d = new Date(isoDate + 'T00:00:00');
|
|
1498
|
+
d.setDate(d.getDate() + days);
|
|
1499
|
+
return d.toISOString();
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// ── Direct fresh EMAIL action (LLM only for NLU, executor server-side) ──
|
|
1503
|
+
// Detects "manda email a X riguardo Y" style requests, extracts recipient
|
|
1504
|
+
// + subject + body via a tiny LLM call, then sends via gmail_send tool.
|
|
1505
|
+
async _tryDirectFreshEmailAction(userMessage, config) {
|
|
1506
|
+
if (!userMessage || typeof userMessage !== 'string') return null;
|
|
1507
|
+
const lower = userMessage.toLowerCase();
|
|
1508
|
+
const isEmailSend = /\b(manda|mand[oa]|invi[oa]|spedis(ci|co)|scriv[io]|send|email\s+to|mail\s+to)\s+(?:un'?\s+)?(email|mail|messaggio|messag|e-?mail)\s+/i.test(lower)
|
|
1509
|
+
|| /\b(invi[oa]r[ea]|mand[ao]|spedire)\s+un'?\s*e[- ]?mail\b/i.test(lower);
|
|
1510
|
+
if (!isEmailSend) return null;
|
|
1511
|
+
|
|
1512
|
+
const parsed = await this._nluExtractEmailSend(userMessage, config);
|
|
1513
|
+
if (!parsed || !parsed.to || !parsed.body) return null;
|
|
1514
|
+
|
|
1515
|
+
const { executeTool } = await import('./tool-executor.mjs');
|
|
1516
|
+
try {
|
|
1517
|
+
const result = await executeTool('gmail_send', {
|
|
1518
|
+
to: parsed.to,
|
|
1519
|
+
subject: parsed.subject || '(no subject)',
|
|
1520
|
+
body: parsed.body,
|
|
1521
|
+
}, config);
|
|
1522
|
+
const ok = typeof result === 'string' && /sent|inviat|✅|message-?id/i.test(result);
|
|
1523
|
+
const message = ok
|
|
1524
|
+
? `Fatto. Email inviata a ${parsed.to}${parsed.subject ? ` con oggetto "${parsed.subject}"` : ''}.`
|
|
1525
|
+
: `Non sono riuscito a inviare l'email: ${result}`;
|
|
1526
|
+
if (this._lastDirectAuditChatId) {
|
|
1527
|
+
this._recordAudit(this._lastDirectAuditChatId, {
|
|
1528
|
+
tool: 'gmail_send',
|
|
1529
|
+
success: ok,
|
|
1530
|
+
summary: `→ ${parsed.to}${parsed.subject ? ` · "${parsed.subject}"` : ''}`,
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
return { action: 'gmail_send', success: ok, message };
|
|
1534
|
+
} catch (e) {
|
|
1535
|
+
return { action: 'gmail_send', success: false, message: `Errore nell'invio: ${e.message}` };
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
async _nluExtractEmailSend(userMessage, config) {
|
|
1540
|
+
const sysPrompt =
|
|
1541
|
+
`You extract email-send parameters from natural language. Output ONLY a JSON object:\n` +
|
|
1542
|
+
` - to (string, recipient email or name if email unknown)\n` +
|
|
1543
|
+
` - subject (string, the email subject; if missing, infer a 2-6 word summary)\n` +
|
|
1544
|
+
` - body (string, the email body in the same language as the user)\n` +
|
|
1545
|
+
`Rules: if any field is genuinely missing AND impossible to infer, use null. ` +
|
|
1546
|
+
`Keep body short and natural. Output JSON only, no markdown.`;
|
|
1547
|
+
try {
|
|
1548
|
+
const raw = await callLLM(config, sysPrompt, userMessage, { temperature: 0, maxTokens: 400 });
|
|
1549
|
+
const json = this._extractJsonObject(raw);
|
|
1550
|
+
if (!json || !json.to || !json.body) return null;
|
|
1551
|
+
return json;
|
|
1552
|
+
} catch { return null; }
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// ── Direct fresh TASK action (add / complete) ──────────────────────────
|
|
1556
|
+
async _tryDirectFreshTaskAction(userMessage, config) {
|
|
1557
|
+
if (!userMessage || typeof userMessage !== 'string') return null;
|
|
1558
|
+
const lower = userMessage.toLowerCase();
|
|
1559
|
+
const isTaskAdd = /\b(aggiung|cre[oai]|fiss|segn|metti|add|create)\w*\s+.*\b(task|attivit[àa]|cosa\s+da\s+fare|todo|to[- ]?do|promemoria\s+da\s+fare)/i.test(lower);
|
|
1560
|
+
const isTaskDone = /\b(complet|fatt[oa]|done|fini[stct]|chiud|spunt|tick|mark\s+as\s+done)\w*\s+.*\b(task|attivit[àa]|todo|to[- ]?do)/i.test(lower);
|
|
1561
|
+
const { executeTool } = await import('./tool-executor.mjs');
|
|
1562
|
+
if (isTaskAdd) {
|
|
1563
|
+
const parsed = await this._nluExtractTaskAdd(userMessage, config);
|
|
1564
|
+
if (!parsed?.title) return null;
|
|
1565
|
+
try {
|
|
1566
|
+
const r = await executeTool('task_add', { title: parsed.title, due: parsed.due || null, priority: parsed.priority || 'medium' }, config);
|
|
1567
|
+
const ok = typeof r === 'string' && !/error/i.test(r);
|
|
1568
|
+
if (this._lastDirectAuditChatId) this._recordAudit(this._lastDirectAuditChatId, { tool: 'task_add', success: ok, summary: `"${parsed.title}"${parsed.due ? ` (entro ${parsed.due})` : ''}` });
|
|
1569
|
+
return { action: 'task_add', success: ok, message: ok ? `Fatto. Ho aggiunto il task "${parsed.title}"${parsed.due ? ` con scadenza ${parsed.due}` : ''}.` : `Errore: ${r}` };
|
|
1570
|
+
} catch (e) { return { action: 'task_add', success: false, message: `Errore: ${e.message}` }; }
|
|
1571
|
+
}
|
|
1572
|
+
if (isTaskDone) {
|
|
1573
|
+
const parsed = await this._nluExtractTaskRef(userMessage, config);
|
|
1574
|
+
if (!parsed?.title) return null;
|
|
1575
|
+
try {
|
|
1576
|
+
const r = await executeTool('task_complete', { title: parsed.title }, config);
|
|
1577
|
+
const ok = typeof r === 'string' && !/error|not found/i.test(r);
|
|
1578
|
+
if (this._lastDirectAuditChatId) this._recordAudit(this._lastDirectAuditChatId, { tool: 'task_complete', success: ok, summary: `"${parsed.title}"` });
|
|
1579
|
+
return { action: 'task_complete', success: ok, message: ok ? `Fatto. Task "${parsed.title}" segnato come completato.` : `Errore: ${r}` };
|
|
1580
|
+
} catch (e) { return { action: 'task_complete', success: false, message: `Errore: ${e.message}` }; }
|
|
1581
|
+
}
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// ── Direct fresh NOTE add ───────────────────────────────────────────────
|
|
1586
|
+
async _tryDirectFreshNoteAction(userMessage, config) {
|
|
1587
|
+
if (!userMessage || typeof userMessage !== 'string') return null;
|
|
1588
|
+
const lower = userMessage.toLowerCase();
|
|
1589
|
+
const isNoteAdd = /\b(crea|nuov[oa]|aggiung|salv|prend|scriv|appunt|new|add|save)\w*\s+(?:una\s+)?\b(nota|note|appunt)/i.test(lower);
|
|
1590
|
+
if (!isNoteAdd) return null;
|
|
1591
|
+
const parsed = await this._nluExtractNoteAdd(userMessage, config);
|
|
1592
|
+
if (!parsed?.title) return null;
|
|
1593
|
+
const { executeTool } = await import('./tool-executor.mjs');
|
|
1594
|
+
try {
|
|
1595
|
+
const r = await executeTool('note_add', { title: parsed.title, content: parsed.content || '' }, config);
|
|
1596
|
+
const ok = typeof r === 'string' && !/error/i.test(r);
|
|
1597
|
+
if (this._lastDirectAuditChatId) this._recordAudit(this._lastDirectAuditChatId, { tool: 'note_add', success: ok, summary: `"${parsed.title}"` });
|
|
1598
|
+
return { action: 'note_add', success: ok, message: ok ? `Fatto. Nota "${parsed.title}" salvata.` : `Errore: ${r}` };
|
|
1599
|
+
} catch (e) { return { action: 'note_add', success: false, message: `Errore: ${e.message}` }; }
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// ── Direct fresh REMINDER create ────────────────────────────────────────
|
|
1603
|
+
async _tryDirectFreshReminderAction(userMessage, config) {
|
|
1604
|
+
if (!userMessage || typeof userMessage !== 'string') return null;
|
|
1605
|
+
const lower = userMessage.toLowerCase();
|
|
1606
|
+
const isReminder = /\b(ricordami|reminder|promemoria|ricord[aoo]|notificami|avvisami|remind)\b/i.test(lower);
|
|
1607
|
+
if (!isReminder) return null;
|
|
1608
|
+
const parsed = await this._nluExtractReminder(userMessage, config);
|
|
1609
|
+
if (!parsed?.message || !parsed?.when) return null;
|
|
1610
|
+
const { executeTool } = await import('./tool-executor.mjs');
|
|
1611
|
+
try {
|
|
1612
|
+
const r = await executeTool('reminder_create', { message: parsed.message, when: parsed.when }, config);
|
|
1613
|
+
const ok = typeof r === 'string' && !/error/i.test(r);
|
|
1614
|
+
if (this._lastDirectAuditChatId) this._recordAudit(this._lastDirectAuditChatId, { tool: 'reminder_create', success: ok, summary: `"${parsed.message}" @ ${parsed.when}` });
|
|
1615
|
+
return { action: 'reminder_create', success: ok, message: ok ? `Fatto. Promemoria impostato: "${parsed.message}" per ${parsed.when}.` : `Errore: ${r}` };
|
|
1616
|
+
} catch (e) { return { action: 'reminder_create', success: false, message: `Errore: ${e.message}` }; }
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// ── Direct fresh SLACK send ─────────────────────────────────────────────
|
|
1620
|
+
async _tryDirectFreshSlackAction(userMessage, config) {
|
|
1621
|
+
if (!userMessage || typeof userMessage !== 'string') return null;
|
|
1622
|
+
const lower = userMessage.toLowerCase();
|
|
1623
|
+
const isSlackSend = /\b(manda|invi[ao]|posta|scriv[io]|send|post)\s+.*\b(slack|canale|channel|#)/i.test(lower)
|
|
1624
|
+
|| /\bsu\s+slack\b/i.test(lower);
|
|
1625
|
+
if (!isSlackSend) return null;
|
|
1626
|
+
const parsed = await this._nluExtractSlackSend(userMessage, config);
|
|
1627
|
+
if (!parsed?.channel || !parsed?.text) return null;
|
|
1628
|
+
const { executeTool } = await import('./tool-executor.mjs');
|
|
1629
|
+
try {
|
|
1630
|
+
const r = await executeTool('slack_send', { channel: parsed.channel, text: parsed.text }, config);
|
|
1631
|
+
const ok = typeof r === 'string' && !/error|not found/i.test(r);
|
|
1632
|
+
if (this._lastDirectAuditChatId) this._recordAudit(this._lastDirectAuditChatId, { tool: 'slack_send', success: ok, summary: `→ ${parsed.channel}: "${parsed.text.slice(0, 60)}"` });
|
|
1633
|
+
return { action: 'slack_send', success: ok, message: ok ? `Fatto. Messaggio inviato a ${parsed.channel}.` : `Errore: ${r}` };
|
|
1634
|
+
} catch (e) { return { action: 'slack_send', success: false, message: `Errore: ${e.message}` }; }
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// ── NLU extractors (LLM-driven JSON parsing only, never tool execution) ──
|
|
1638
|
+
async _nluExtractTaskAdd(userMessage, config) {
|
|
1639
|
+
const todayIso = new Date().toISOString().slice(0, 10);
|
|
1640
|
+
const sys = `Today is ${todayIso}. Extract task params from the message. Output ONLY JSON:\n` +
|
|
1641
|
+
` - title (string, the task description)\n` +
|
|
1642
|
+
` - due (string, "YYYY-MM-DD" if mentioned, else null)\n` +
|
|
1643
|
+
` - priority ("low" | "medium" | "high" | null)`;
|
|
1644
|
+
try { return this._extractJsonObject(await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 200 })); } catch { return null; }
|
|
1645
|
+
}
|
|
1646
|
+
async _nluExtractTaskRef(userMessage, config) {
|
|
1647
|
+
const sys = `Extract which task to mark completed. Output ONLY JSON:\n - title (string)`;
|
|
1648
|
+
try { return this._extractJsonObject(await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 150 })); } catch { return null; }
|
|
1649
|
+
}
|
|
1650
|
+
async _nluExtractNoteAdd(userMessage, config) {
|
|
1651
|
+
const sys = `Extract a new-note request. Output ONLY JSON:\n - title (string)\n - content (string, can be empty)`;
|
|
1652
|
+
try { return this._extractJsonObject(await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 400 })); } catch { return null; }
|
|
1653
|
+
}
|
|
1654
|
+
async _nluExtractReminder(userMessage, config) {
|
|
1655
|
+
const todayIso = new Date().toISOString().slice(0, 10);
|
|
1656
|
+
const sys = `Today is ${todayIso}. Extract reminder params. Output ONLY JSON:\n` +
|
|
1657
|
+
` - message (string, what to remind)\n - when (string, ISO datetime "YYYY-MM-DDTHH:MM:00")`;
|
|
1658
|
+
try { return this._extractJsonObject(await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 200 })); } catch { return null; }
|
|
1659
|
+
}
|
|
1660
|
+
async _nluExtractSlackSend(userMessage, config) {
|
|
1661
|
+
const sys = `Extract Slack-send params. Output ONLY JSON:\n` +
|
|
1662
|
+
` - channel (string, with "#" prefix for channels or "@user" for DMs)\n - text (string, message body)`;
|
|
1663
|
+
try { return this._extractJsonObject(await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 300 })); } catch { return null; }
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1410
1666
|
// ── Direct fresh calendar action (no LLM) ─────────────────────────────────
|
|
1411
1667
|
// Detects DELETE / LIST_MONTH / LIST_WEEK / LIST_DAY / LIST_TODAY /
|
|
1412
1668
|
// LIST_TOMORROW intents from a fresh user message and runs the proper tool
|
|
@@ -1432,6 +1688,14 @@ class TelegramResponder {
|
|
|
1432
1688
|
// a date or title reference. We answer FACTUALLY by re-querying the
|
|
1433
1689
|
// calendar — never by apologizing in the abstract.
|
|
1434
1690
|
const isVerify = /\b(non\s+(vedo|c'è|esiste)|c'è\s+(ancora|sempre)|è\s+ancora|stai\s+sbagliand|hai\s+sbagliat|in\s+realt[àa]|sei\s+sicur|davvero|inaffidabil|controlla|verifica|conferma\b|guarda)\b/.test(lower);
|
|
1691
|
+
// CREATE event intent — action verb + event noun. Robust enough to catch
|
|
1692
|
+
// "fissami un appuntamento dal dentista venerdì alle 10", "crea evento
|
|
1693
|
+
// ortocheratologia 18 maggio 10-11", "segna riunione con Marco lunedì 14:30".
|
|
1694
|
+
const isCreate = /\b(fiss|cre[oai]|aggiung|inserisc|programm|segn|prenoti?|impost|metti|registr|set\s+up|create|add|schedule|book)\w*\s+/.test(lower)
|
|
1695
|
+
&& /\b(appuntament|evento|event\b|meeting|riunion|incontro|chiamat|call\b|webinar|memo|reminder|promemoria|task|impegn)/i.test(lower);
|
|
1696
|
+
// MOVE intent — reschedule/postpone an existing event.
|
|
1697
|
+
const isMove = /\b(spost[ao]|rimand|rinvi|riprogramm|move|reschedule|postpone|cambia\s+(?:data|ora|orari))\w*/.test(lower)
|
|
1698
|
+
&& /\b(appuntament|evento|event\b|meeting|riunion|incontro|chiamat|call\b)/i.test(lower);
|
|
1435
1699
|
|
|
1436
1700
|
const { executeTool } = await import('./tool-executor.mjs');
|
|
1437
1701
|
|
|
@@ -1491,8 +1755,108 @@ class TelegramResponder {
|
|
|
1491
1755
|
// No date/title to verify — fall through to LIST/DELETE detection.
|
|
1492
1756
|
}
|
|
1493
1757
|
|
|
1758
|
+
// ─── CREATE intent ─────────────────────────────────────────────────────
|
|
1759
|
+
// We use a tiny LLM call ONLY to extract structured params from natural
|
|
1760
|
+
// language (NLU). Tool execution stays deterministic server-side. This
|
|
1761
|
+
// is the enterprise pattern: LLM for understanding + rendering, NEVER
|
|
1762
|
+
// for state-changing actions.
|
|
1763
|
+
if (isCreate && !isDelete && !isVerify) {
|
|
1764
|
+
const parsed = await this._nluExtractCalendarCreate(userMessage, config);
|
|
1765
|
+
if (parsed && parsed.title && parsed.start) {
|
|
1766
|
+
try {
|
|
1767
|
+
const result = await executeTool('calendar_create', {
|
|
1768
|
+
summary: parsed.title,
|
|
1769
|
+
start: parsed.start,
|
|
1770
|
+
end: parsed.end || this._addMinutesIso(parsed.start, 60),
|
|
1771
|
+
description: parsed.description || '',
|
|
1772
|
+
}, config);
|
|
1773
|
+
const ok = typeof result === 'string' && /created|event\s+"|eventId/i.test(result);
|
|
1774
|
+
const startLabel = `${this._formatDateIT(parsed.start.slice(0, 10))} alle ${parsed.start.slice(11, 16)}`;
|
|
1775
|
+
const message = ok
|
|
1776
|
+
? `Fatto. Ho creato l'appuntamento "${parsed.title}" il ${startLabel}.`
|
|
1777
|
+
: `Non sono riuscito a creare l'appuntamento: ${result}`;
|
|
1778
|
+
if (this._lastDirectAuditChatId) {
|
|
1779
|
+
this._recordAudit(this._lastDirectAuditChatId, {
|
|
1780
|
+
tool: 'calendar_create',
|
|
1781
|
+
success: ok,
|
|
1782
|
+
summary: `"${parsed.title}" il ${startLabel}`,
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
return { action: 'calendar_create', success: ok, message };
|
|
1786
|
+
} catch (e) {
|
|
1787
|
+
return { action: 'calendar_create', success: false, message: `Errore durante la creazione: ${e.message}` };
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
// Couldn't extract title/date with confidence → let the LLM ask the user.
|
|
1791
|
+
// Fall through.
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// ─── MOVE intent ───────────────────────────────────────────────────────
|
|
1795
|
+
if (isMove && !isDelete && !isVerify && !isCreate) {
|
|
1796
|
+
const parsed = await this._nluExtractCalendarMove(userMessage, config);
|
|
1797
|
+
if (parsed && (parsed.title || parsed.oldDate) && parsed.newStart) {
|
|
1798
|
+
// Find the source event.
|
|
1799
|
+
let candidates = [];
|
|
1800
|
+
try {
|
|
1801
|
+
if (parsed.oldDate) {
|
|
1802
|
+
const r = await executeTool('calendar_date', { date: parsed.oldDate }, config);
|
|
1803
|
+
candidates = this._parseEventsFromToolOutput(r);
|
|
1804
|
+
}
|
|
1805
|
+
if (candidates.length === 0 && parsed.title) {
|
|
1806
|
+
const r = await executeTool('calendar_find', { query: parsed.title, daysAhead: 60 }, config);
|
|
1807
|
+
candidates = this._parseEventsFromToolOutput(r);
|
|
1808
|
+
}
|
|
1809
|
+
} catch (e) {
|
|
1810
|
+
return { action: 'calendar_move', success: false, message: `Errore nella ricerca dell'evento: ${e.message}` };
|
|
1811
|
+
}
|
|
1812
|
+
// Match by title token overlap, same heuristic as DELETE.
|
|
1813
|
+
const norm = (s) => String(s || '').toLowerCase()
|
|
1814
|
+
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
1815
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
1816
|
+
.split(/\s+/).filter(t => t.length > 2);
|
|
1817
|
+
let match = null;
|
|
1818
|
+
if (parsed.title) {
|
|
1819
|
+
const titleTokens = norm(parsed.title);
|
|
1820
|
+
const scored = candidates.map(c => {
|
|
1821
|
+
const summaryTokens = new Set(norm(c.summary));
|
|
1822
|
+
const score = titleTokens.filter(t => summaryTokens.has(t)).length;
|
|
1823
|
+
return { c, score };
|
|
1824
|
+
}).sort((a, b) => b.score - a.score);
|
|
1825
|
+
const top = scored[0];
|
|
1826
|
+
if (top && top.score >= Math.max(1, Math.ceil(titleTokens.length * 0.5))) match = top.c;
|
|
1827
|
+
}
|
|
1828
|
+
if (!match && candidates.length === 1) match = candidates[0];
|
|
1829
|
+
if (!match || !match.eventId) {
|
|
1830
|
+
return { action: 'calendar_move', success: false,
|
|
1831
|
+
message: `Non ho trovato l'appuntamento "${parsed.title || ''}" da spostare. Verifica titolo o data originale.` };
|
|
1832
|
+
}
|
|
1833
|
+
try {
|
|
1834
|
+
const result = await executeTool('calendar_move', {
|
|
1835
|
+
eventId: match.eventId,
|
|
1836
|
+
newStart: parsed.newStart,
|
|
1837
|
+
newEnd: parsed.newEnd || this._addMinutesIso(parsed.newStart, 60),
|
|
1838
|
+
}, config);
|
|
1839
|
+
const ok = typeof result === 'string' && /rescheduled|moved|spostat/i.test(result);
|
|
1840
|
+
const newLabel = `${this._formatDateIT(parsed.newStart.slice(0, 10))} alle ${parsed.newStart.slice(11, 16)}`;
|
|
1841
|
+
const message = ok
|
|
1842
|
+
? `Fatto. Ho spostato "${match.summary}" al ${newLabel}.`
|
|
1843
|
+
: `Non sono riuscito a spostare l'appuntamento: ${result}`;
|
|
1844
|
+
if (this._lastDirectAuditChatId) {
|
|
1845
|
+
this._recordAudit(this._lastDirectAuditChatId, {
|
|
1846
|
+
tool: 'calendar_move',
|
|
1847
|
+
success: ok,
|
|
1848
|
+
summary: `"${match.summary}" → ${newLabel} (eventId ${match.eventId.slice(0, 16)}…)`,
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
return { action: 'calendar_move', success: ok, message };
|
|
1852
|
+
} catch (e) {
|
|
1853
|
+
return { action: 'calendar_move', success: false, message: `Errore nello spostamento: ${e.message}` };
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1494
1858
|
// ─── LIST intents ──────────────────────────────────────────────────────
|
|
1495
|
-
if (isList && !isDelete && !isVerify) {
|
|
1859
|
+
if (isList && !isDelete && !isVerify && !isCreate && !isMove) {
|
|
1496
1860
|
// "appuntamenti di oggi"
|
|
1497
1861
|
if (/\b(oggi|today)\b/.test(lower)) {
|
|
1498
1862
|
try {
|