n8n-nodes-tembory 1.1.3 → 1.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.
|
@@ -399,6 +399,275 @@ const safeParseToolPayload = (value) => {
|
|
|
399
399
|
return text;
|
|
400
400
|
}
|
|
401
401
|
};
|
|
402
|
+
const parseNestedToolPayload = (value, depth = 0) => {
|
|
403
|
+
if (depth > 8)
|
|
404
|
+
return value;
|
|
405
|
+
if (typeof value === 'string') {
|
|
406
|
+
const text = value.trim();
|
|
407
|
+
if (!text)
|
|
408
|
+
return '';
|
|
409
|
+
if (!/^[\[{]/.test(text))
|
|
410
|
+
return value;
|
|
411
|
+
try {
|
|
412
|
+
return parseNestedToolPayload(JSON.parse(text), depth + 1);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
return value;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (Array.isArray(value))
|
|
419
|
+
return value.map((item) => parseNestedToolPayload(item, depth + 1));
|
|
420
|
+
if (!value || typeof value !== 'object')
|
|
421
|
+
return value;
|
|
422
|
+
const out = {};
|
|
423
|
+
for (const [key, item] of Object.entries(value))
|
|
424
|
+
out[key] = parseNestedToolPayload(item, depth + 1);
|
|
425
|
+
return out;
|
|
426
|
+
};
|
|
427
|
+
const collectObjectsDeep = (value, out = [], seen = new Set()) => {
|
|
428
|
+
if (!value || typeof value !== 'object' || seen.has(value))
|
|
429
|
+
return out;
|
|
430
|
+
seen.add(value);
|
|
431
|
+
if (!Array.isArray(value))
|
|
432
|
+
out.push(value);
|
|
433
|
+
for (const item of Array.isArray(value) ? value : Object.values(value))
|
|
434
|
+
collectObjectsDeep(item, out, seen);
|
|
435
|
+
return out;
|
|
436
|
+
};
|
|
437
|
+
const compactObject = (value) => {
|
|
438
|
+
if (Array.isArray(value)) {
|
|
439
|
+
const arr = value.map(compactObject).filter((item) => item !== undefined);
|
|
440
|
+
return arr.length ? arr : undefined;
|
|
441
|
+
}
|
|
442
|
+
if (!value || typeof value !== 'object')
|
|
443
|
+
return value === '' || value === null || value === undefined ? undefined : value;
|
|
444
|
+
const out = {};
|
|
445
|
+
for (const [key, item] of Object.entries(value)) {
|
|
446
|
+
const cleaned = compactObject(item);
|
|
447
|
+
if (cleaned !== undefined)
|
|
448
|
+
out[key] = cleaned;
|
|
449
|
+
}
|
|
450
|
+
return Object.keys(out).length ? out : undefined;
|
|
451
|
+
};
|
|
452
|
+
const normalizeFieldKey = (value = '') => String(value || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
453
|
+
const hasFieldKey = (key, candidates) => candidates.map(normalizeFieldKey).includes(normalizeFieldKey(key));
|
|
454
|
+
const findFieldDeep = (value, keys) => {
|
|
455
|
+
const objects = collectObjectsDeep(value);
|
|
456
|
+
for (const object of objects) {
|
|
457
|
+
for (const [key, item] of Object.entries(object)) {
|
|
458
|
+
if (hasFieldKey(key, keys) && item !== undefined && item !== null && item !== '')
|
|
459
|
+
return item;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return undefined;
|
|
463
|
+
};
|
|
464
|
+
const readPath = (value, path) => {
|
|
465
|
+
let current = value;
|
|
466
|
+
for (const segment of path) {
|
|
467
|
+
if (!current || typeof current !== 'object')
|
|
468
|
+
return undefined;
|
|
469
|
+
current = current[segment];
|
|
470
|
+
}
|
|
471
|
+
return current;
|
|
472
|
+
};
|
|
473
|
+
const firstPath = (roots, paths) => {
|
|
474
|
+
for (const root of roots) {
|
|
475
|
+
for (const path of paths) {
|
|
476
|
+
const value = readPath(root, path);
|
|
477
|
+
if (value !== undefined && value !== null && value !== '')
|
|
478
|
+
return value;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return undefined;
|
|
482
|
+
};
|
|
483
|
+
const numberFrom = (value) => {
|
|
484
|
+
if (value === undefined || value === null || value === '')
|
|
485
|
+
return undefined;
|
|
486
|
+
const match = String(value).match(/-?\d+(?:[.,]\d+)?/);
|
|
487
|
+
if (!match)
|
|
488
|
+
return undefined;
|
|
489
|
+
const parsed = Number(match[0].replace(',', '.'));
|
|
490
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
491
|
+
};
|
|
492
|
+
const integerFrom = (value) => {
|
|
493
|
+
const parsed = numberFrom(value);
|
|
494
|
+
return parsed === undefined ? undefined : Math.trunc(parsed);
|
|
495
|
+
};
|
|
496
|
+
const normalizeTimeValue = (value) => {
|
|
497
|
+
const text = String(value || '').trim().toLowerCase();
|
|
498
|
+
const match = /^(\d{1,2})(?:(?::|h)(\d{0,2}))?$/.exec(text);
|
|
499
|
+
if (!match)
|
|
500
|
+
return '';
|
|
501
|
+
const hour = Number(match[1]);
|
|
502
|
+
const minute = match[2] === '' || match[2] === undefined ? 0 : Number(match[2]);
|
|
503
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
|
|
504
|
+
return '';
|
|
505
|
+
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
|
506
|
+
};
|
|
507
|
+
const isoDateFromParts = (day, month, year, referenceDates = []) => {
|
|
508
|
+
const dd = String(day || '').padStart(2, '0');
|
|
509
|
+
const mm = String(month || '').padStart(2, '0');
|
|
510
|
+
let yyyy = String(year || '');
|
|
511
|
+
if (!yyyy) {
|
|
512
|
+
const matched = (referenceDates || []).find((date) => String(date || '').slice(5, 10) === `${mm}-${dd}`);
|
|
513
|
+
yyyy = matched ? String(matched).slice(0, 4) : String(new Date().getFullYear());
|
|
514
|
+
}
|
|
515
|
+
if (yyyy.length === 2)
|
|
516
|
+
yyyy = `20${yyyy}`;
|
|
517
|
+
return /^\d{4}$/.test(yyyy) && /^\d{2}$/.test(mm) && /^\d{2}$/.test(dd) ? `${yyyy}-${mm}-${dd}` : '';
|
|
518
|
+
};
|
|
519
|
+
const bookingToolName = (name = '') => String(name || '').toLowerCase();
|
|
520
|
+
const serviceNameFromPageContent = (value) => {
|
|
521
|
+
const content = String(value || '').trim();
|
|
522
|
+
if (!content)
|
|
523
|
+
return '';
|
|
524
|
+
return content.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || '';
|
|
525
|
+
};
|
|
526
|
+
const extractAvailabilitySlots = (parsedResult) => {
|
|
527
|
+
const slots = [];
|
|
528
|
+
const roots = Array.isArray(parsedResult) ? parsedResult : [parsedResult];
|
|
529
|
+
for (const root of roots) {
|
|
530
|
+
if (!root || typeof root !== 'object')
|
|
531
|
+
continue;
|
|
532
|
+
const availability = Array.isArray(root.disponibilidade) ? root.disponibilidade : [];
|
|
533
|
+
for (const item of availability) {
|
|
534
|
+
const date = item.dataDisponivel || item.date || item.data || '';
|
|
535
|
+
const providers = Array.isArray(item.providers) ? item.providers : [];
|
|
536
|
+
for (const provider of providers) {
|
|
537
|
+
const providerId = integerFrom(provider.provider_id ?? provider.providerId ?? provider.id);
|
|
538
|
+
const providerName = provider.provider_name || provider.providerName || provider.name || '';
|
|
539
|
+
const horarios = Array.isArray(provider.horarios) ? provider.horarios : [];
|
|
540
|
+
for (const horario of horarios) {
|
|
541
|
+
const time = normalizeTimeValue(horario);
|
|
542
|
+
if (date && time)
|
|
543
|
+
slots.push(compactObject({ date, time, providerId, providerName }));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return slots.filter(Boolean);
|
|
549
|
+
};
|
|
550
|
+
const extractToolBookingFacts = (toolName = '', input = '', rawResult = '') => {
|
|
551
|
+
const name = bookingToolName(toolName);
|
|
552
|
+
const inputObj = parseNestedToolPayload(safeParseToolPayload(input));
|
|
553
|
+
const parsedResult = parseNestedToolPayload(safeParseToolPayload(rawResult));
|
|
554
|
+
const roots = Array.isArray(parsedResult) ? parsedResult : [parsedResult];
|
|
555
|
+
const objects = collectObjectsDeep(parsedResult);
|
|
556
|
+
const facts = { source_tool: toolName };
|
|
557
|
+
if (/check[_-]?availabilities|disponibilidade/i.test(name)) {
|
|
558
|
+
facts.kind = 'availability';
|
|
559
|
+
facts.locationId = integerFrom(firstPath(roots, [['locationId'], ['location_id']]) ?? findFieldDeep(parsedResult, ['locationId', 'location_id']));
|
|
560
|
+
facts.locationName = inputObj && typeof inputObj === 'object' ? inputObj.location_name || inputObj.locationName : undefined;
|
|
561
|
+
facts.serviceName = firstPath(roots, [['especialidade'], ['serviceName'], ['service_name']]) || (inputObj && typeof inputObj === 'object' ? inputObj.service_name || inputObj.serviceName : undefined);
|
|
562
|
+
facts.serviceId = integerFrom(firstPath(roots, [['services', 'id'], ['service', 'id'], ['serviceId'], ['service_id']]) ?? findFieldDeep(parsedResult, ['serviceId', 'service_id']));
|
|
563
|
+
facts.durationMinutes = integerFrom(firstPath(roots, [['services', 'duration'], ['duration'], ['durationMinutes']]) ?? findFieldDeep(parsedResult, ['duration', 'durationMinutes']));
|
|
564
|
+
facts.price = firstPath(roots, [['services', 'price'], ['price']]) ?? findFieldDeep(parsedResult, ['price', 'price_num']);
|
|
565
|
+
facts.providerId = integerFrom(firstPath(roots, [['provider', 'id'], ['providerId'], ['provider_id']]) ?? findFieldDeep(parsedResult, ['providerId', 'provider_id']));
|
|
566
|
+
facts.availability_slots = extractAvailabilitySlots(parsedResult).slice(0, 80);
|
|
567
|
+
}
|
|
568
|
+
else if (/services?|veterinarios?/i.test(name)) {
|
|
569
|
+
facts.kind = /veterinarios?/i.test(name) ? 'provider_service' : 'service';
|
|
570
|
+
facts.serviceId = integerFrom(firstPath(roots, [['metadata', 'metadata', 'service_id'], ['metadata', 'service_id'], ['service_id'], ['serviceId']]) ?? findFieldDeep(parsedResult, ['service_id', 'serviceId']));
|
|
571
|
+
facts.serviceName = firstPath(roots, [['pageContent']]) || (objects.map((object) => serviceNameFromPageContent(object.pageContent)).find(Boolean));
|
|
572
|
+
facts.serviceName = serviceNameFromPageContent(facts.serviceName || '');
|
|
573
|
+
facts.locationId = integerFrom(firstPath(roots, [['metadata', 'metadata', 'location_id'], ['metadata', 'location_id'], ['locationId'], ['local']]) ?? findFieldDeep(parsedResult, ['location_id', 'locationId', 'local']));
|
|
574
|
+
facts.providerId = integerFrom(firstPath(roots, [['metadata', 'metadata', 'provider_id'], ['metadata', 'provider_id'], ['provider_id'], ['providerId']]) ?? findFieldDeep(parsedResult, ['provider_id', 'providerId']));
|
|
575
|
+
facts.price = firstPath(roots, [['metadata', 'metadata', 'price_num'], ['metadata', 'price_num'], ['price']]) ?? findFieldDeep(parsedResult, ['price_num', 'price']);
|
|
576
|
+
facts.durationMinutes = integerFrom(firstPath(roots, [['metadata', 'metadata', 'duration'], ['metadata', 'duration'], ['duration']]) ?? findFieldDeep(parsedResult, ['duration']));
|
|
577
|
+
}
|
|
578
|
+
else if (/anclivepa.*customer.*upsert|customer.*upsert/i.test(name)) {
|
|
579
|
+
facts.kind = 'booking_customer';
|
|
580
|
+
facts.customerId = integerFrom(firstPath(roots, [['customerId'], ['customer_id']]) ?? findFieldDeep(parsedResult, ['customerId', 'customer_id']));
|
|
581
|
+
facts.petName = firstPath(roots, [['petChosen'], ['petName'], ['pet_name']]) || (inputObj && typeof inputObj === 'object' ? inputObj.petName || inputObj.pet_name : undefined);
|
|
582
|
+
facts.readyForBooking = Boolean(firstPath(roots, [['readyForBooking']]) ?? findFieldDeep(parsedResult, ['readyForBooking']));
|
|
583
|
+
}
|
|
584
|
+
const compacted = compactObject(facts);
|
|
585
|
+
return compacted && Object.keys(compacted).length > 2 ? compacted : undefined;
|
|
586
|
+
};
|
|
587
|
+
const extractBookingSelectionFromMessages = (recentMessages = [], referenceDates = []) => {
|
|
588
|
+
const ordered = [...(recentMessages || [])].reverse();
|
|
589
|
+
for (const msg of ordered) {
|
|
590
|
+
const text = String(msg.content || '');
|
|
591
|
+
const dateTime = /(\d{1,2})[/-](\d{1,2})(?:[/-](\d{2,4}))?[\s\S]{0,160}?(?:às|as|hor[aá]rio)\s*(\d{1,2})(?:(?::|h)(\d{2}))?/i.exec(text);
|
|
592
|
+
if (dateTime) {
|
|
593
|
+
return compactObject({
|
|
594
|
+
date: isoDateFromParts(dateTime[1], dateTime[2], dateTime[3], referenceDates),
|
|
595
|
+
time: normalizeTimeValue(`${dateTime[4]}:${dateTime[5] || '00'}`),
|
|
596
|
+
source: 'recent_message',
|
|
597
|
+
}) || {};
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const dateMsg = ordered.find((msg) => /(\d{1,2})[/-](\d{1,2})(?:[/-](\d{2,4}))?/.test(String(msg.content || '')));
|
|
601
|
+
const timeMsg = ordered.find((msg) => /^(?:\s*(?:às|as)\s*)?\d{1,2}(?:(?::|h)\d{0,2})?\s*$/i.test(String(msg.content || '')));
|
|
602
|
+
const dateMatch = dateMsg ? /(\d{1,2})[/-](\d{1,2})(?:[/-](\d{2,4}))?/.exec(String(dateMsg.content || '')) : null;
|
|
603
|
+
return compactObject({
|
|
604
|
+
date: dateMatch ? isoDateFromParts(dateMatch[1], dateMatch[2], dateMatch[3], referenceDates) : undefined,
|
|
605
|
+
time: timeMsg ? normalizeTimeValue(timeMsg.content) : undefined,
|
|
606
|
+
source: dateMatch || timeMsg ? 'recent_message_partial' : undefined,
|
|
607
|
+
}) || {};
|
|
608
|
+
};
|
|
609
|
+
const addMinutesToSlot = (date, time, minutes = 60) => {
|
|
610
|
+
if (!date || !time)
|
|
611
|
+
return '';
|
|
612
|
+
const start = new Date(`${date}T${time}:00Z`);
|
|
613
|
+
if (!Number.isFinite(start.getTime()))
|
|
614
|
+
return '';
|
|
615
|
+
const end = new Date(start.getTime() + (Number(minutes) || 60) * 60000);
|
|
616
|
+
return `${end.toISOString().slice(0, 10)} ${end.toISOString().slice(11, 19)}`;
|
|
617
|
+
};
|
|
618
|
+
const deriveBookingState = (toolHistory = [], recentMessages = []) => {
|
|
619
|
+
const facts = (toolHistory || []).map((tool) => tool.booking_facts || extractToolBookingFacts(tool.name, tool.input, tool.result)).filter(Boolean);
|
|
620
|
+
if (!facts.length)
|
|
621
|
+
return undefined;
|
|
622
|
+
const latest = (predicate) => [...facts].reverse().find(predicate) || {};
|
|
623
|
+
const availability = latest((fact) => Array.isArray(fact.availability_slots) && fact.availability_slots.length);
|
|
624
|
+
const service = latest((fact) => fact.serviceId);
|
|
625
|
+
const customer = latest((fact) => fact.kind === 'booking_customer' && fact.customerId);
|
|
626
|
+
const slots = availability.availability_slots || [];
|
|
627
|
+
const referenceDates = Array.from(new Set(slots.map((slot) => slot.date).filter(Boolean)));
|
|
628
|
+
const selection = extractBookingSelectionFromMessages(recentMessages, referenceDates);
|
|
629
|
+
const selectedSlot = slots.find((slot) => slot.date === selection.date && slot.time === selection.time) || {};
|
|
630
|
+
const uniqueProviderIds = Array.from(new Set(slots.map((slot) => slot.providerId).filter(Boolean)));
|
|
631
|
+
const durationMinutes = availability.durationMinutes || service.durationMinutes || 60;
|
|
632
|
+
const providerId = selectedSlot.providerId || (uniqueProviderIds.length === 1 ? uniqueProviderIds[0] : availability.providerId || service.providerId);
|
|
633
|
+
const serviceId = availability.serviceId || service.serviceId;
|
|
634
|
+
const locationId = availability.locationId || service.locationId;
|
|
635
|
+
const customerId = customer.customerId;
|
|
636
|
+
const start = selection.date && selection.time ? `${selection.date} ${selection.time}:00` : undefined;
|
|
637
|
+
const end = start ? addMinutesToSlot(selection.date, selection.time, durationMinutes) : undefined;
|
|
638
|
+
const bookingCandidate = compactObject({
|
|
639
|
+
start,
|
|
640
|
+
end,
|
|
641
|
+
date: selection.date,
|
|
642
|
+
time: selection.time,
|
|
643
|
+
providerId,
|
|
644
|
+
providerName: selectedSlot.providerName,
|
|
645
|
+
serviceId,
|
|
646
|
+
serviceName: availability.serviceName || service.serviceName,
|
|
647
|
+
locationId,
|
|
648
|
+
locationName: availability.locationName,
|
|
649
|
+
customerId,
|
|
650
|
+
petName: customer.petName,
|
|
651
|
+
durationMinutes,
|
|
652
|
+
price: availability.price || service.price,
|
|
653
|
+
source: 'derived_from_tool_history',
|
|
654
|
+
}) || {};
|
|
655
|
+
const required = ['providerId', 'serviceId', 'locationId', 'customerId', 'date', 'time'];
|
|
656
|
+
const missing = required.filter((key) => bookingCandidate[key] === undefined || bookingCandidate[key] === null || bookingCandidate[key] === '');
|
|
657
|
+
const warnings = [];
|
|
658
|
+
if (bookingCandidate.providerId && bookingCandidate.locationId && String(bookingCandidate.providerId) === String(bookingCandidate.locationId))
|
|
659
|
+
warnings.push('providerId_equals_locationId');
|
|
660
|
+
if (bookingCandidate.serviceId && bookingCandidate.locationId && String(bookingCandidate.serviceId) === String(bookingCandidate.locationId))
|
|
661
|
+
warnings.push('serviceId_equals_locationId');
|
|
662
|
+
return compactObject({
|
|
663
|
+
booking_candidate: bookingCandidate,
|
|
664
|
+
selected_slot: selectedSlot,
|
|
665
|
+
missing,
|
|
666
|
+
warnings,
|
|
667
|
+
available_slot_count: slots.length || undefined,
|
|
668
|
+
source_tools: Array.from(new Set(facts.map((fact) => fact.source_tool).filter(Boolean))),
|
|
669
|
+
});
|
|
670
|
+
};
|
|
402
671
|
const compactToolPayload = (value) => truncate(typeof value === 'string' ? value : safeStringify(value), 900);
|
|
403
672
|
const maybeToolResult = (tool, includeResults = true) => includeResults === false ? undefined : compactToolPayload(safeParseToolPayload(tool === null || tool === void 0 ? void 0 : tool.result));
|
|
404
673
|
const isToolName = (value = '') => /^[A-Za-z_][A-Za-z0-9_.:-]{1,127}$/.test(String(value || ''));
|
|
@@ -438,6 +707,7 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
|
|
|
438
707
|
at: tool.at || null,
|
|
439
708
|
input: compactInput,
|
|
440
709
|
result: compactResult,
|
|
710
|
+
booking_facts: tool.booking_facts,
|
|
441
711
|
reason,
|
|
442
712
|
source: tool.source || 'unknown',
|
|
443
713
|
};
|
|
@@ -449,6 +719,7 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
|
|
|
449
719
|
reason,
|
|
450
720
|
input: compactInput,
|
|
451
721
|
output: compactResult,
|
|
722
|
+
booking_facts: tool.booking_facts,
|
|
452
723
|
}));
|
|
453
724
|
if (tool.ok === false)
|
|
454
725
|
failedByName[name] = (failedByName[name] || 0) + 1;
|
|
@@ -458,9 +729,11 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
|
|
|
458
729
|
const guidance = [
|
|
459
730
|
'Use tool_state and action_ledger as the source of truth for prior tool calls, inputs, outputs, failures and decisions. Domain-specific tool policy must come from the agent prompt, not from memory.',
|
|
460
731
|
];
|
|
732
|
+
const bookingState = deriveBookingState(tools, recentMessages);
|
|
461
733
|
return {
|
|
462
734
|
profile_complete: Boolean(profileFacts && profileFacts.name && profileFacts.company && profileFacts.email && profileFacts.phone),
|
|
463
735
|
last_tool: lastTool ? { name: lastTool.name, ok: lastTool.ok, at: lastTool.at } : null,
|
|
736
|
+
booking_state: bookingState,
|
|
464
737
|
tool_counts: {
|
|
465
738
|
total: tools.length,
|
|
466
739
|
ok: successfulTools.length,
|
|
@@ -480,6 +753,7 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
|
|
|
480
753
|
name: successfulTools[successfulTools.length - 1].name,
|
|
481
754
|
at: successfulTools[successfulTools.length - 1].at || null,
|
|
482
755
|
result: maybeToolResult(successfulTools[successfulTools.length - 1], includeResults),
|
|
756
|
+
booking_facts: successfulTools[successfulTools.length - 1].booking_facts,
|
|
483
757
|
} : null,
|
|
484
758
|
},
|
|
485
759
|
blocked_without_context: Array.from(new Set(blockedWithoutContext)),
|
|
@@ -498,6 +772,7 @@ const deriveActionLedger = (toolHistory = [], maxItems = 20, includeResults = tr
|
|
|
498
772
|
reason: truncate(String(tool.reason || tool.decision || tool.why || tool.source || 'recorded tool call'), 260),
|
|
499
773
|
input: compactToolPayload(safeParseToolPayload(tool.input)),
|
|
500
774
|
result: maybeToolResult(tool, includeResults),
|
|
775
|
+
booking_facts: tool.booking_facts,
|
|
501
776
|
at: tool.at || null,
|
|
502
777
|
source: tool.source || 'unknown',
|
|
503
778
|
})), maxItems || 20);
|
|
@@ -602,6 +877,7 @@ const encodeToolCall = (tool, threadId) => `${TOOL_HISTORY_MARKER}${safeStringif
|
|
|
602
877
|
input: tool.input || '',
|
|
603
878
|
ok: tool.ok !== false,
|
|
604
879
|
result: tool.result || '',
|
|
880
|
+
booking_facts: tool.booking_facts,
|
|
605
881
|
at: tool.at || nowIso(),
|
|
606
882
|
source: tool.source || 'n8n',
|
|
607
883
|
thread_id: threadId,
|
|
@@ -621,6 +897,7 @@ const encodeToolLedger = (tools = [], threadId) => {
|
|
|
621
897
|
input: tool.input || '',
|
|
622
898
|
ok: tool.ok !== false,
|
|
623
899
|
result: tool.result || '',
|
|
900
|
+
booking_facts: tool.booking_facts,
|
|
624
901
|
at: tool.at || nowIso(),
|
|
625
902
|
source: tool.source || 'n8n',
|
|
626
903
|
})),
|
|
@@ -647,6 +924,7 @@ const encodeTurnArchive = ({ threadId, messages = [], tools = [], workingMemory
|
|
|
647
924
|
input: tool.input || '',
|
|
648
925
|
ok: tool.ok !== false,
|
|
649
926
|
output: tool.result || '',
|
|
927
|
+
booking_facts: tool.booking_facts,
|
|
650
928
|
at: tool.at || nowIso(),
|
|
651
929
|
source: tool.source || 'n8n',
|
|
652
930
|
})),
|
|
@@ -688,6 +966,7 @@ const parseToolHistoryMarker = (text) => {
|
|
|
688
966
|
input: parsed.input === undefined ? '' : String(parsed.input),
|
|
689
967
|
ok: parsed.ok !== false,
|
|
690
968
|
result: truncate(parsed.result || '', 1000),
|
|
969
|
+
booking_facts: parsed.booking_facts || parsed.bookingFacts,
|
|
691
970
|
at: parsed.at || nowIso(),
|
|
692
971
|
source: parsed.source || 'tembory_marker',
|
|
693
972
|
};
|
|
@@ -715,6 +994,7 @@ const parseToolLedgerMarker = (text) => {
|
|
|
715
994
|
input: tool.input === undefined ? '' : String(tool.input),
|
|
716
995
|
ok: tool.ok !== false,
|
|
717
996
|
result: truncate(tool.result || '', 1000),
|
|
997
|
+
booking_facts: tool.booking_facts || tool.bookingFacts,
|
|
718
998
|
at: tool.at || parsed.generated_at || nowIso(),
|
|
719
999
|
source: tool.source || 'tool_ledger_marker',
|
|
720
1000
|
}));
|
|
@@ -818,6 +1098,7 @@ const toolHistoryFromMemory = (item) => {
|
|
|
818
1098
|
input: truncate((factMatch[2] || '').trim(), 1000),
|
|
819
1099
|
ok: true,
|
|
820
1100
|
result: truncate((factMatch[3] || '').trim(), 1000),
|
|
1101
|
+
booking_facts: meta.booking_facts || meta.bookingFacts,
|
|
821
1102
|
at: meta.at || item.created_at || item.createdAt || nowIso(),
|
|
822
1103
|
source: 'semantic_fact',
|
|
823
1104
|
};
|
|
@@ -847,6 +1128,7 @@ const toolHistoryFromMemory = (item) => {
|
|
|
847
1128
|
input: meta.input === undefined ? '' : String(meta.input),
|
|
848
1129
|
ok: meta.ok !== false,
|
|
849
1130
|
result: truncate(meta.result || content || '', 1000),
|
|
1131
|
+
booking_facts: meta.booking_facts || meta.bookingFacts,
|
|
850
1132
|
at: meta.at || item.created_at || item.createdAt || nowIso(),
|
|
851
1133
|
source: meta.source || 'metadata',
|
|
852
1134
|
};
|
|
@@ -866,6 +1148,7 @@ const explicitToolHistoryItemsFromMemory = (item) => {
|
|
|
866
1148
|
input: tool.input === undefined ? '' : String(tool.input),
|
|
867
1149
|
ok: tool.ok !== false,
|
|
868
1150
|
result: truncate(tool.output || tool.result || '', 1000),
|
|
1151
|
+
booking_facts: tool.booking_facts || tool.bookingFacts,
|
|
869
1152
|
at: tool.at || archive.generated_at || nowIso(),
|
|
870
1153
|
source: tool.source || 'turn_archive',
|
|
871
1154
|
}));
|
|
@@ -930,7 +1213,9 @@ const canonicalToolInput = (value) => {
|
|
|
930
1213
|
const normalizeToolCall = (tool, sequence = 0, defaults = {}) => {
|
|
931
1214
|
const at = tool.at || defaults.at || nowIso();
|
|
932
1215
|
const input = canonicalToolInput(tool.input);
|
|
933
|
-
const
|
|
1216
|
+
const rawResult = tool.result || tool.output || tool.observation || '';
|
|
1217
|
+
const result = summarizeToolResult(rawResult);
|
|
1218
|
+
const bookingFacts = tool.booking_facts || tool.bookingFacts || extractToolBookingFacts(tool.name || tool.tool || tool.toolName || '', input, rawResult);
|
|
934
1219
|
const normalized = {
|
|
935
1220
|
id: tool.id || tool.callId || tool.call_id || '',
|
|
936
1221
|
call_id: tool.id || tool.callId || tool.call_id || '',
|
|
@@ -945,11 +1230,14 @@ const normalizeToolCall = (tool, sequence = 0, defaults = {}) => {
|
|
|
945
1230
|
ok: tool.ok !== false,
|
|
946
1231
|
result,
|
|
947
1232
|
result_summary: result,
|
|
1233
|
+
booking_facts: bookingFacts,
|
|
948
1234
|
at,
|
|
949
1235
|
timestamp: at,
|
|
950
1236
|
source: tool.source || defaults.source || 'n8n',
|
|
951
1237
|
status: tool.ok === false ? 'failed' : 'ok',
|
|
952
1238
|
};
|
|
1239
|
+
if (!normalized.booking_facts)
|
|
1240
|
+
delete normalized.booking_facts;
|
|
953
1241
|
if (!normalized.id)
|
|
954
1242
|
normalized.id = makeToolEventId(normalized, normalized.sequence);
|
|
955
1243
|
if (!normalized.call_id)
|
|
@@ -1026,8 +1314,12 @@ const extractToolCallsFromMessages = (messages = []) => {
|
|
|
1026
1314
|
const name = message.name || message.toolName || message.tool || additional.name || additional.tool_name || additional.toolName || kwargs.name || 'tool';
|
|
1027
1315
|
const existing = id ? toolCallById.get(String(id)) : null;
|
|
1028
1316
|
if (existing) {
|
|
1029
|
-
|
|
1317
|
+
const rawContent = messageContentOf(message);
|
|
1318
|
+
existing.result = summarizeToolResult(rawContent);
|
|
1030
1319
|
existing.result_summary = existing.result;
|
|
1320
|
+
existing.booking_facts = extractToolBookingFacts(existing.name, existing.input, rawContent) || existing.booking_facts;
|
|
1321
|
+
if (!existing.booking_facts)
|
|
1322
|
+
delete existing.booking_facts;
|
|
1031
1323
|
existing.ok = true;
|
|
1032
1324
|
existing.status = 'ok';
|
|
1033
1325
|
existing.result_hash = stableHash(existing.result || '');
|
|
@@ -1737,6 +2029,8 @@ const inferUserIntent = (query = '', recentMessages = []) => {
|
|
|
1737
2029
|
return 'operational_status_question';
|
|
1738
2030
|
if (/^(ok|sim|pode|pode sim|isso|isso mesmo|confirmo|confirmar)$/.test(normalizedQuery))
|
|
1739
2031
|
return 'affirm';
|
|
2032
|
+
if (/^(\d{1,2}|\d{1,2}[:h]\d{0,2}|\d{1,2}[/-]\d{1,2}(?:[/-]\d{2,4})?)$/.test(normalizedQuery))
|
|
2033
|
+
return 'selection_or_slot';
|
|
1740
2034
|
if (hasCommitIntent(text))
|
|
1741
2035
|
return 'commit_or_continue';
|
|
1742
2036
|
if (/\b(buscar|busca|criar|cria|atualizar|atualiza|consultar|consulta|reservar|reserva|agendar|agenda|abrir|abre|cancelar|cancela|enviar|envia|gerar|gera|validar|valida|processar|processa|executar|executa)\b/.test(text))
|
|
@@ -1748,7 +2042,7 @@ const inferUserIntent = (query = '', recentMessages = []) => {
|
|
|
1748
2042
|
return 'unknown';
|
|
1749
2043
|
};
|
|
1750
2044
|
const deriveNextExpectedAction = (intent, operationalState = {}) => {
|
|
1751
|
-
if (['affirm', 'commit_or_continue', 'tool_action_candidate'].includes(intent))
|
|
2045
|
+
if (['affirm', 'commit_or_continue', 'tool_action_candidate', 'selection_or_slot'].includes(intent))
|
|
1752
2046
|
return 'continue according to the agent prompt using conversation_frame, tool_state, tool_history and action_ledger; do not apply domain-specific memory rules';
|
|
1753
2047
|
if (intent === 'profile_update')
|
|
1754
2048
|
return 'save stable profile facts and continue the conversation';
|
|
@@ -1756,8 +2050,9 @@ const deriveNextExpectedAction = (intent, operationalState = {}) => {
|
|
|
1756
2050
|
return 'answer directly using conversation_history_chronological/all_user_messages_chronological; do not call tools for recall-only questions';
|
|
1757
2051
|
if (intent === 'operational_status_question')
|
|
1758
2052
|
return 'answer status questions from tool_state, tool_history and action_ledger; do not call tools unless the agent prompt requires it';
|
|
1759
|
-
return '
|
|
2053
|
+
return 'continue according to the agent prompt using retrieved context; call tools when the agent prompt or tool policy requires them';
|
|
1760
2054
|
};
|
|
2055
|
+
const isGenericMemoryNextAction = (value = '') => /answer using retrieved context and avoid unnecessary tool calls|continue according to the agent prompt using retrieved context/i.test(String(value || ''));
|
|
1761
2056
|
const deriveWorkingMemory = ({ query = '', profileFacts = {}, recentMessages = [], toolHistory = [], operationalState = {}, previous = {} }) => {
|
|
1762
2057
|
const intent = inferUserIntent(query, recentMessages);
|
|
1763
2058
|
const lastUser = [...(recentMessages || [])].reverse().find((msg) => /^(user|human)$/i.test(String(msg.role || '')));
|
|
@@ -1767,15 +2062,16 @@ const deriveWorkingMemory = ({ query = '', profileFacts = {}, recentMessages = [
|
|
|
1767
2062
|
activeEntities.push({ type: key, value: profileFacts[key] });
|
|
1768
2063
|
}
|
|
1769
2064
|
const lastTool = toolHistory && toolHistory.length ? toolHistory[toolHistory.length - 1] : null;
|
|
2065
|
+
const nextExpectedAction = deriveNextExpectedAction(intent, operationalState);
|
|
1770
2066
|
return {
|
|
1771
|
-
current_goal: previous.current_goal
|
|
1772
|
-
current_task:
|
|
2067
|
+
current_goal: previous.current_goal && !isGenericMemoryNextAction(previous.current_goal) ? previous.current_goal : nextExpectedAction,
|
|
2068
|
+
current_task: nextExpectedAction,
|
|
1773
2069
|
last_user_intent: intent,
|
|
1774
2070
|
last_user_message: lastUser ? truncate(lastUser.content, 500) : truncate(query, 500),
|
|
1775
2071
|
active_entities: activeEntities,
|
|
1776
2072
|
open_decisions: previous.open_decisions || [],
|
|
1777
2073
|
last_error: lastTool && lastTool.ok === false ? { tool: lastTool.name, at: lastTool.at, result: lastTool.result } : null,
|
|
1778
|
-
next_expected_action:
|
|
2074
|
+
next_expected_action: nextExpectedAction,
|
|
1779
2075
|
updated_at: nowIso(),
|
|
1780
2076
|
};
|
|
1781
2077
|
};
|
|
@@ -1952,7 +2248,7 @@ const contextMemoryText = (memory, max = 700) => {
|
|
|
1952
2248
|
};
|
|
1953
2249
|
const approxTokenCount = (text) => Math.ceil(String(text || '').length / 4);
|
|
1954
2250
|
const noisyToolFields = new Set(['requested_from', 'thread', 'thread_id', 'run_id', 'dedupe_key', 'args_hash', 'result_hash', 'source']);
|
|
1955
|
-
const importantJsonFields = ['id', 'status', 'state', 'next_step', 'action', 'intent', 'tool', 'input', 'output', 'result', 'error', 'message', 'reason', 'customer_id', 'user_id', 'lead_id', 'ticket_id', 'charge_id', 'reservation_id', 'confirmation_id', 'selected_from_message', 'available_slots', 'timezone', 'name', 'phone', 'segmento', 'platform', 'plataforma', 'lifecycle_stage', 'provider', 'sku', 'product', 'stock', 'queue', 'priority'];
|
|
2251
|
+
const importantJsonFields = ['id', 'status', 'state', 'next_step', 'action', 'intent', 'tool', 'input', 'output', 'result', 'error', 'message', 'reason', 'customer_id', 'customerId', 'user_id', 'lead_id', 'ticket_id', 'charge_id', 'reservation_id', 'confirmation_id', 'selected_from_message', 'available_slots', 'availability_slots', 'disponibilidade', 'dataDisponivel', 'horarios', 'timezone', 'name', 'phone', 'segmento', 'platform', 'plataforma', 'lifecycle_stage', 'provider', 'provider_id', 'providerId', 'provider_name', 'services', 'service_id', 'serviceId', 'locationId', 'location_id', 'local', 'especialidade', 'duration', 'price', 'price_num', 'sku', 'product', 'stock', 'queue', 'priority'];
|
|
1956
2252
|
const stripNoisyToolFields = (value, depth = 0) => {
|
|
1957
2253
|
if (depth > 8)
|
|
1958
2254
|
return undefined;
|
|
@@ -2024,6 +2320,7 @@ const compactToolHistoryForAgent = (toolHistory = [], maxItems = 6, includeResul
|
|
|
2024
2320
|
reason: truncate(String(tool.reason || tool.decision || tool.why || tool.source || 'recorded tool call'), 140),
|
|
2025
2321
|
input: truncate(String(tool.input || ''), 140) || undefined,
|
|
2026
2322
|
result: includeResults ? compactToolResult(tool.result, 240) : undefined,
|
|
2323
|
+
booking_facts: tool.booking_facts,
|
|
2027
2324
|
}));
|
|
2028
2325
|
const cleanContextValue = (value) => {
|
|
2029
2326
|
if (Array.isArray(value)) {
|
|
@@ -2080,6 +2377,7 @@ const compactOperationalStateForAgent = (state = {}) => {
|
|
|
2080
2377
|
const toolState = state.tool_state || {};
|
|
2081
2378
|
return {
|
|
2082
2379
|
last_tool: state.last_tool || null,
|
|
2380
|
+
booking_state: state.booking_state,
|
|
2083
2381
|
tool_counts: {
|
|
2084
2382
|
total: counts.total || 0,
|
|
2085
2383
|
ok: counts.ok || 0,
|
|
@@ -2108,6 +2406,7 @@ const compactActionLedgerForAgent = (ledger = [], maxItems = 6, includeResults =
|
|
|
2108
2406
|
reason: item.reason,
|
|
2109
2407
|
input: item.input,
|
|
2110
2408
|
result: includeResults ? compactToolResult(item.result, 180) : undefined,
|
|
2409
|
+
booking_facts: item.booking_facts,
|
|
2111
2410
|
}));
|
|
2112
2411
|
const compactEntityTimelineForAgent = (timeline = [], maxItems = 8) => pruneByLimit(timeline || [], maxItems).map((item) => cleanContextValue({
|
|
2113
2412
|
entity: item.entity || item.source || item.name,
|
|
@@ -2416,6 +2715,7 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
|
|
|
2416
2715
|
const vectorFacts = sectionValue('summary');
|
|
2417
2716
|
const slmSummary = sectionValue('connected_model_summary') || sectionValue('active_summary');
|
|
2418
2717
|
const directive = sectionValue('action_directive');
|
|
2718
|
+
const bookingState = (operationalState || {}).booking_state;
|
|
2419
2719
|
const inferredIntent = deriveUserIntentObservation({ query, workingMemory, decisionState, recentMessages });
|
|
2420
2720
|
const minimalState = cleanContextValue({
|
|
2421
2721
|
next_expected_action: directive ? undefined : workingMemory.next_expected_action,
|
|
@@ -2441,6 +2741,7 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
|
|
|
2441
2741
|
slm: hasToolLedger ? undefined : slmSummary,
|
|
2442
2742
|
},
|
|
2443
2743
|
state: minimalState,
|
|
2744
|
+
booking: bookingState,
|
|
2444
2745
|
profile: sectionValue('profile_facts'),
|
|
2445
2746
|
tools: compactToolLedger,
|
|
2446
2747
|
});
|
|
@@ -3166,6 +3467,7 @@ class TemboryMemory {
|
|
|
3166
3467
|
input: tool.input,
|
|
3167
3468
|
ok: tool.ok,
|
|
3168
3469
|
result: tool.result,
|
|
3470
|
+
booking_facts: tool.booking_facts,
|
|
3169
3471
|
at: tool.at,
|
|
3170
3472
|
source: tool.source || 'tembory_transcript',
|
|
3171
3473
|
thread_id: threadId,
|
|
@@ -3893,6 +4195,7 @@ class TemboryMemory {
|
|
|
3893
4195
|
dedupe_key: tool.dedupe_key || toolEventKey(tool),
|
|
3894
4196
|
result: adv.includeToolResults === false ? undefined : tool.result,
|
|
3895
4197
|
result_summary: adv.includeToolResults === false ? undefined : (tool.result_summary || tool.result),
|
|
4198
|
+
booking_facts: tool.booking_facts,
|
|
3896
4199
|
})),
|
|
3897
4200
|
},
|
|
3898
4201
|
diagnostics,
|
|
@@ -4056,6 +4359,8 @@ exports.__private = {
|
|
|
4056
4359
|
embedQueryCached,
|
|
4057
4360
|
compactToolResult,
|
|
4058
4361
|
compactToolHistoryForAgent,
|
|
4362
|
+
extractToolBookingFacts,
|
|
4363
|
+
deriveBookingState,
|
|
4059
4364
|
compactVectorMemoriesForAgent,
|
|
4060
4365
|
isConversationEchoMemory,
|
|
4061
4366
|
compactOperationalStateForAgent,
|
package/package.json
CHANGED