@diegoaltoworks/chatter 0.29.0 → 0.31.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/channels/whatsapp/inbound.d.ts +131 -0
- package/dist/channels/whatsapp/inbound.d.ts.map +1 -0
- package/dist/channels/whatsapp/inbound.test.d.ts +2 -0
- package/dist/channels/whatsapp/inbound.test.d.ts.map +1 -0
- package/dist/channels/whatsapp/index.d.ts +1 -0
- package/dist/channels/whatsapp/index.d.ts.map +1 -1
- package/dist/channels/whatsapp/index.js +332 -0
- package/dist/channels/whatsapp/index.mjs +325 -0
- package/dist/personas/greeter.d.ts +19 -0
- package/dist/personas/greeter.d.ts.map +1 -0
- package/dist/personas/greeter.test.d.ts +2 -0
- package/dist/personas/greeter.test.d.ts.map +1 -0
- package/dist/personas/index.d.ts +14 -0
- package/dist/personas/index.d.ts.map +1 -0
- package/dist/personas/index.js +206 -0
- package/dist/personas/index.mjs +177 -0
- package/dist/personas/resolver.d.ts +67 -0
- package/dist/personas/resolver.d.ts.map +1 -0
- package/dist/personas/resolver.test.d.ts +2 -0
- package/dist/personas/resolver.test.d.ts.map +1 -0
- package/dist/personas/timeContext.d.ts +7 -0
- package/dist/personas/timeContext.d.ts.map +1 -0
- package/dist/personas/timeContext.test.d.ts +2 -0
- package/dist/personas/timeContext.test.d.ts.map +1 -0
- package/package.json +8 -2
|
@@ -410,6 +410,324 @@ function createWhatsAppChannel(config) {
|
|
|
410
410
|
}
|
|
411
411
|
};
|
|
412
412
|
}
|
|
413
|
+
|
|
414
|
+
// src/core/guardrails.ts
|
|
415
|
+
function detectLeakage(text) {
|
|
416
|
+
const flags = [
|
|
417
|
+
/system prompt/i,
|
|
418
|
+
/hidden instruction/i,
|
|
419
|
+
/here (are|is) (my|the) rules/i,
|
|
420
|
+
/BEGIN SYSTEM PROMPT/i,
|
|
421
|
+
/tool instructions/i
|
|
422
|
+
];
|
|
423
|
+
return flags.some((rx) => rx.test(text));
|
|
424
|
+
}
|
|
425
|
+
function scrubOutput(text) {
|
|
426
|
+
return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
|
|
427
|
+
}
|
|
428
|
+
function guardOutput(text) {
|
|
429
|
+
if (detectLeakage(text)) {
|
|
430
|
+
return "Sorry, I can't share internal instructions. How else can I help?";
|
|
431
|
+
}
|
|
432
|
+
return scrubOutput(text);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/core/llm.ts
|
|
436
|
+
var DEFAULT_MODEL = "gpt-4o";
|
|
437
|
+
async function completeOnce({
|
|
438
|
+
client,
|
|
439
|
+
system,
|
|
440
|
+
messages,
|
|
441
|
+
temperature = 0.2,
|
|
442
|
+
model = DEFAULT_MODEL
|
|
443
|
+
}) {
|
|
444
|
+
const res = await client.chat.completions.create({
|
|
445
|
+
model,
|
|
446
|
+
temperature,
|
|
447
|
+
messages: [{ role: "system", content: system }, ...messages]
|
|
448
|
+
});
|
|
449
|
+
const text = res.choices[0]?.message?.content ?? "";
|
|
450
|
+
return {
|
|
451
|
+
content: guardOutput(text),
|
|
452
|
+
usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/core/answer.ts
|
|
457
|
+
var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
|
|
458
|
+
async function runAnswerFn(answerFn, { system, messages, mode, sender }) {
|
|
459
|
+
const result = await answerFn({ system, messages, mode, sender });
|
|
460
|
+
const raw = typeof result === "string" ? result : result?.content;
|
|
461
|
+
const usage = typeof result === "string" ? void 0 : result?.usage;
|
|
462
|
+
return {
|
|
463
|
+
content: guardOutput(typeof raw === "string" ? raw : ""),
|
|
464
|
+
usage: usage ?? noUsage()
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
async function answerOnce({
|
|
468
|
+
answerFn,
|
|
469
|
+
client,
|
|
470
|
+
system,
|
|
471
|
+
messages,
|
|
472
|
+
mode,
|
|
473
|
+
sender,
|
|
474
|
+
temperature,
|
|
475
|
+
model
|
|
476
|
+
}) {
|
|
477
|
+
if (answerFn) {
|
|
478
|
+
return runAnswerFn(answerFn, { system, messages, mode, sender });
|
|
479
|
+
}
|
|
480
|
+
return completeOnce({ client, system, messages, temperature, model });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/core/buckets.ts
|
|
484
|
+
function defaultBuckets(mode) {
|
|
485
|
+
return ["base", mode];
|
|
486
|
+
}
|
|
487
|
+
function isIdentified(sender) {
|
|
488
|
+
return typeof sender === "string" && sender.trim().length > 0;
|
|
489
|
+
}
|
|
490
|
+
function normalize(buckets) {
|
|
491
|
+
const out = [];
|
|
492
|
+
for (const raw of buckets) {
|
|
493
|
+
if (typeof raw !== "string") continue;
|
|
494
|
+
const bucket = raw.trim();
|
|
495
|
+
if (bucket && !out.includes(bucket)) out.push(bucket);
|
|
496
|
+
}
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
async function resolveBuckets({
|
|
500
|
+
mode,
|
|
501
|
+
sender,
|
|
502
|
+
bucketsFor
|
|
503
|
+
}) {
|
|
504
|
+
if (!bucketsFor) return void 0;
|
|
505
|
+
const identified = isIdentified(sender);
|
|
506
|
+
const requested = await bucketsFor(identified ? { mode, sender } : { mode });
|
|
507
|
+
if (!Array.isArray(requested)) return void 0;
|
|
508
|
+
const cleaned = normalize(requested);
|
|
509
|
+
if (identified) return cleaned;
|
|
510
|
+
const ceiling = new Set(defaultBuckets(mode));
|
|
511
|
+
return cleaned.filter((bucket) => ceiling.has(bucket));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/core/pipeline.ts
|
|
515
|
+
var MODE_SETTINGS = {
|
|
516
|
+
public: { topK: 6, label: "Context" },
|
|
517
|
+
private: { topK: 8, label: "Internal Context" }
|
|
518
|
+
};
|
|
519
|
+
async function prepareChat({
|
|
520
|
+
store,
|
|
521
|
+
prompts,
|
|
522
|
+
mode,
|
|
523
|
+
messages,
|
|
524
|
+
personaLayer,
|
|
525
|
+
channelHint,
|
|
526
|
+
buckets
|
|
527
|
+
}) {
|
|
528
|
+
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
529
|
+
if (!lastUserMsg) {
|
|
530
|
+
throw new Error("no user message found in conversation");
|
|
531
|
+
}
|
|
532
|
+
const { topK, label } = MODE_SETTINGS[mode];
|
|
533
|
+
const ctx = await store.query(lastUserMsg.content, topK, buckets ?? defaultBuckets(mode));
|
|
534
|
+
const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
|
|
535
|
+
const hint = channelHint?.trim();
|
|
536
|
+
const system = [
|
|
537
|
+
prompts.baseSystemRules,
|
|
538
|
+
persona,
|
|
539
|
+
...hint ? [hint] : [],
|
|
540
|
+
`${label}:
|
|
541
|
+
${ctx.join("\n\n")}`
|
|
542
|
+
].join("\n\n");
|
|
543
|
+
return { system, messages };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/channels/gates.ts
|
|
547
|
+
function matches(re, text) {
|
|
548
|
+
return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
|
|
549
|
+
}
|
|
550
|
+
function decideChannelAction(msg, config) {
|
|
551
|
+
if (msg.fromBot || !msg.text.trim()) return "ignore";
|
|
552
|
+
if (!msg.isDirectMessage) {
|
|
553
|
+
if (config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId)) {
|
|
554
|
+
return "ignore";
|
|
555
|
+
}
|
|
556
|
+
if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
|
|
557
|
+
if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
|
|
558
|
+
if (config.mutedChats.has(msg.chatId)) return "ignore";
|
|
559
|
+
return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
|
|
560
|
+
}
|
|
561
|
+
return "reply";
|
|
562
|
+
}
|
|
563
|
+
function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
|
|
564
|
+
const log = /* @__PURE__ */ new Map();
|
|
565
|
+
return function allow(key) {
|
|
566
|
+
const cutoff = now() - windowMs;
|
|
567
|
+
const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
|
|
568
|
+
if (entries.length >= maxPerWindow) {
|
|
569
|
+
if (entries.length > 0) log.set(key, entries);
|
|
570
|
+
else log.delete(key);
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
entries.push(now());
|
|
574
|
+
log.set(key, entries);
|
|
575
|
+
return true;
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function underReplyRateLimit(msg, limiters) {
|
|
579
|
+
return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
|
|
580
|
+
}
|
|
581
|
+
function isFromAnySession(id, registry) {
|
|
582
|
+
if (!id) return false;
|
|
583
|
+
for (const ids of registry.values()) {
|
|
584
|
+
if (ids.includes(id)) return true;
|
|
585
|
+
}
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
function isEffectivelyFromSelf(msg, registry) {
|
|
589
|
+
return msg.fromBot || isFromAnySession(msg.senderId, registry);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/channels/whatsapp/inbound.ts
|
|
593
|
+
function isGroupJid(jid) {
|
|
594
|
+
return jid.endsWith("@g.us");
|
|
595
|
+
}
|
|
596
|
+
function jidsMatch(a, b) {
|
|
597
|
+
if (!a || !b) return false;
|
|
598
|
+
const bare = (jid) => jid.split("@")[0]?.split(":")[0] ?? jid;
|
|
599
|
+
return bare(a) === bare(b);
|
|
600
|
+
}
|
|
601
|
+
function jidToPhoneNumber(jid) {
|
|
602
|
+
const bare = jid.split("@")[0]?.split(":")[0] ?? "";
|
|
603
|
+
return bare.startsWith("+") ? bare : `+${bare}`;
|
|
604
|
+
}
|
|
605
|
+
function normalizeJid(jid) {
|
|
606
|
+
const [user = "", domain] = jid.split("@");
|
|
607
|
+
const bareUser = user.split(":")[0] ?? user;
|
|
608
|
+
return domain ? `${bareUser}@${domain}` : bareUser;
|
|
609
|
+
}
|
|
610
|
+
async function senderPhoneFor(sock, message, chatJid) {
|
|
611
|
+
const key = message.key;
|
|
612
|
+
let jid = [key.participantPn, key.senderPn, key.participant, key.remoteJidAlt, chatJid].find(
|
|
613
|
+
(candidate) => typeof candidate === "string" && candidate.length > 0
|
|
614
|
+
) ?? chatJid;
|
|
615
|
+
if (jid.endsWith("@lid")) {
|
|
616
|
+
try {
|
|
617
|
+
const mapped = await sock.signalRepository?.lidMapping?.getPNForLID?.(jid);
|
|
618
|
+
if (mapped) jid = mapped;
|
|
619
|
+
} catch {
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return jid.endsWith("@lid") ? `lid:${jid.split("@")[0]?.split(":")[0] ?? jid}` : jidToPhoneNumber(jid);
|
|
623
|
+
}
|
|
624
|
+
function messageContext(message) {
|
|
625
|
+
const content = message.message;
|
|
626
|
+
const contextInfo = content?.extendedTextMessage?.contextInfo ?? content?.imageMessage?.contextInfo ?? content?.videoMessage?.contextInfo ?? content?.documentMessage?.contextInfo;
|
|
627
|
+
return {
|
|
628
|
+
mentionedJids: contextInfo?.mentionedJid ?? [],
|
|
629
|
+
quotedParticipantJid: contextInfo?.participant ?? void 0
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function extractText(message) {
|
|
633
|
+
const content = message.message;
|
|
634
|
+
if (!content) return "";
|
|
635
|
+
return content.conversation ?? content.extendedTextMessage?.text ?? content.imageMessage?.caption ?? content.videoMessage?.caption ?? content.documentMessage?.caption ?? "";
|
|
636
|
+
}
|
|
637
|
+
var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
|
|
638
|
+
var DEFAULT_DM_RATE_LIMIT = { max: 20, windowMs: 60 * 60 * 1e3 };
|
|
639
|
+
async function resolvePersonaLayer(config, ctx) {
|
|
640
|
+
if (!config.personaResolver) return void 0;
|
|
641
|
+
try {
|
|
642
|
+
return await config.personaResolver(ctx);
|
|
643
|
+
} catch {
|
|
644
|
+
return void 0;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function createWhatsAppInboundHandler(config) {
|
|
648
|
+
const mutedChats = /* @__PURE__ */ new Set();
|
|
649
|
+
const now = config.now ?? Date.now;
|
|
650
|
+
const group = config.groupRateLimit ?? DEFAULT_GROUP_RATE_LIMIT;
|
|
651
|
+
const dm = config.dmRateLimit ?? DEFAULT_DM_RATE_LIMIT;
|
|
652
|
+
const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
|
|
653
|
+
const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
|
|
654
|
+
return async function handleWhatsAppMessage(event) {
|
|
655
|
+
const { sessionId, sock, message } = event;
|
|
656
|
+
try {
|
|
657
|
+
const chatId = message.key.remoteJid ?? "";
|
|
658
|
+
if (!chatId || chatId === "status@broadcast") return;
|
|
659
|
+
const text = extractText(message);
|
|
660
|
+
const context = messageContext(message);
|
|
661
|
+
const senderId = message.key.participant ?? chatId;
|
|
662
|
+
const ownIds = [sock.user?.id, sock.user?.lid].filter((id) => Boolean(id)).map(normalizeJid);
|
|
663
|
+
config.registry.set(sessionId, ownIds);
|
|
664
|
+
const fromBot = isEffectivelyFromSelf(
|
|
665
|
+
{ fromBot: message.key.fromMe ?? false, senderId: normalizeJid(senderId) },
|
|
666
|
+
config.registry
|
|
667
|
+
);
|
|
668
|
+
const msg = {
|
|
669
|
+
chatId,
|
|
670
|
+
senderId,
|
|
671
|
+
text,
|
|
672
|
+
isDirectMessage: !isGroupJid(chatId),
|
|
673
|
+
mentionsBot: context.mentionedJids.some((jid) => ownIds.some((own) => jidsMatch(jid, own))),
|
|
674
|
+
isReplyToBot: ownIds.some((own) => jidsMatch(context.quotedParticipantJid, own)),
|
|
675
|
+
fromBot
|
|
676
|
+
};
|
|
677
|
+
const action = decideChannelAction(msg, {
|
|
678
|
+
allowedChats: config.allowedChats ?? [],
|
|
679
|
+
mutedChats,
|
|
680
|
+
muteRegex: config.muteRegex,
|
|
681
|
+
unmuteRegex: config.unmuteRegex
|
|
682
|
+
});
|
|
683
|
+
if (action === "mute") {
|
|
684
|
+
mutedChats.add(chatId);
|
|
685
|
+
if (config.muteReply) await sock.sendMessage(chatId, { text: config.muteReply });
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (action === "unmute") {
|
|
689
|
+
mutedChats.delete(chatId);
|
|
690
|
+
if (config.unmuteReply) await sock.sendMessage(chatId, { text: config.unmuteReply });
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (action !== "reply") return;
|
|
694
|
+
if (!underReplyRateLimit(msg, { dm: underDmRateLimit, group: underGroupRateLimit })) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const senderPhone = await senderPhoneFor(sock, message, chatId);
|
|
698
|
+
const personaLayer = await resolvePersonaLayer(config, { senderPhone, text });
|
|
699
|
+
const buckets = await resolveBuckets({
|
|
700
|
+
mode: "public",
|
|
701
|
+
sender: senderPhone,
|
|
702
|
+
bucketsFor: config.bucketsFor
|
|
703
|
+
});
|
|
704
|
+
const messages = [{ role: "user", content: text }];
|
|
705
|
+
const { system } = await prepareChat({
|
|
706
|
+
store: config.store,
|
|
707
|
+
prompts: config.prompts,
|
|
708
|
+
mode: "public",
|
|
709
|
+
messages,
|
|
710
|
+
personaLayer,
|
|
711
|
+
channelHint: config.channelHint,
|
|
712
|
+
buckets
|
|
713
|
+
});
|
|
714
|
+
const { content } = await answerOnce({
|
|
715
|
+
answerFn: config.answerFn,
|
|
716
|
+
client: config.client,
|
|
717
|
+
system,
|
|
718
|
+
messages,
|
|
719
|
+
mode: "public",
|
|
720
|
+
sender: senderPhone,
|
|
721
|
+
model: config.model
|
|
722
|
+
});
|
|
723
|
+
if (content) {
|
|
724
|
+
await sock.sendMessage(chatId, { text: content }, { quoted: message });
|
|
725
|
+
}
|
|
726
|
+
} catch (error) {
|
|
727
|
+
console.warn(`WhatsApp[${sessionId}]: inbound message handling failed:`, error);
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
}
|
|
413
731
|
export {
|
|
414
732
|
LEASE_HEARTBEAT_MS,
|
|
415
733
|
LEASE_STALE_MS,
|
|
@@ -418,11 +736,18 @@ export {
|
|
|
418
736
|
canAcquireLease,
|
|
419
737
|
createTursoWaLeaseStore,
|
|
420
738
|
createWhatsAppChannel,
|
|
739
|
+
createWhatsAppInboundHandler,
|
|
421
740
|
decrypt,
|
|
422
741
|
encrypt,
|
|
742
|
+
extractText,
|
|
743
|
+
isGroupJid,
|
|
744
|
+
jidToPhoneNumber,
|
|
745
|
+
jidsMatch,
|
|
423
746
|
loadBaileys,
|
|
747
|
+
messageContext,
|
|
424
748
|
reconnectDelayMs,
|
|
425
749
|
senderNameFor,
|
|
750
|
+
senderPhoneFor,
|
|
426
751
|
shutdownWaSessions,
|
|
427
752
|
useTursoAuthState
|
|
428
753
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named greetings for known contacts — rides the same persona window as the
|
|
3
|
+
* chat that follows, so a call greeted in one persona/language continues in
|
|
4
|
+
* it. Ships no content: templates are entirely caller-supplied.
|
|
5
|
+
*/
|
|
6
|
+
import type { PersonaResolver } from "./resolver";
|
|
7
|
+
export interface GreeterOptions {
|
|
8
|
+
resolver: PersonaResolver;
|
|
9
|
+
/** Greeting templates keyed by persona id, each containing a `{name}` placeholder. */
|
|
10
|
+
templates: Record<string, readonly string[]>;
|
|
11
|
+
/** Injectable randomness for tests. Default `Math.random`. */
|
|
12
|
+
random?: () => number;
|
|
13
|
+
}
|
|
14
|
+
export interface Greeter {
|
|
15
|
+
/** A named greeting for a known contact in their current persona, or null (unknown contact, or no template pool for that persona). */
|
|
16
|
+
greet(contactId: string): string | null;
|
|
17
|
+
}
|
|
18
|
+
export declare function createGreeter({ resolver, templates, random, }: GreeterOptions): Greeter;
|
|
19
|
+
//# sourceMappingURL=greeter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"greeter.d.ts","sourceRoot":"","sources":["../../src/personas/greeter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,eAAe,CAAC;IAC1B,sFAAsF;IACtF,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IAC7C,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,OAAO;IACtB,sIAAsI;IACtI,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CACzC;AAMD,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,SAAS,EACT,MAAoB,GACrB,EAAE,cAAc,GAAG,OAAO,CAmB1B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"greeter.test.d.ts","sourceRoot":"","sources":["../../src/personas/greeter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persona machinery — dynamic prompt layers and named greetings driven by a
|
|
3
|
+
* JSON registry. Published as the `@diegoaltoworks/chatter/personas` subpath
|
|
4
|
+
* so the core install stays untouched by it; this module adds no required
|
|
5
|
+
* dependencies.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
export type { Greeter, GreeterOptions } from "./greeter";
|
|
10
|
+
export { createGreeter } from "./greeter";
|
|
11
|
+
export type { PersonaContact, PersonaDefinition, PersonaRegistry, PersonaResolver, PersonaResolverOptions, } from "./resolver";
|
|
12
|
+
export { createPersonaResolver } from "./resolver";
|
|
13
|
+
export { timeContext } from "./timeContext";
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/personas/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,sBAAsB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/personas/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
createGreeter: () => createGreeter,
|
|
24
|
+
createPersonaResolver: () => createPersonaResolver,
|
|
25
|
+
timeContext: () => timeContext
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/personas/greeter.ts
|
|
30
|
+
function capitalize(name) {
|
|
31
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
32
|
+
}
|
|
33
|
+
function createGreeter({
|
|
34
|
+
resolver,
|
|
35
|
+
templates,
|
|
36
|
+
random = Math.random
|
|
37
|
+
}) {
|
|
38
|
+
return {
|
|
39
|
+
greet(contactId) {
|
|
40
|
+
try {
|
|
41
|
+
const contact = resolver.contactFor(contactId);
|
|
42
|
+
if (!contact?.name) return null;
|
|
43
|
+
const personaId = resolver.rollPersonaId(contactId) ?? resolver.defaultPersonaId();
|
|
44
|
+
const pool = personaId ? templates[personaId] : void 0;
|
|
45
|
+
if (!pool || pool.length === 0) return null;
|
|
46
|
+
const template = pool[Math.floor(random() * pool.length)];
|
|
47
|
+
if (!template) return null;
|
|
48
|
+
return template.replaceAll("{name}", capitalize(contact.name));
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/personas/resolver.ts
|
|
57
|
+
var import_node_fs = require("node:fs");
|
|
58
|
+
var import_node_path = require("node:path");
|
|
59
|
+
var DEFAULT_WINDOW_MINUTES = 10;
|
|
60
|
+
var DEFAULT_PROBABILITY = 0.5;
|
|
61
|
+
function interpolate(template, vars) {
|
|
62
|
+
return Object.entries(vars).reduce(
|
|
63
|
+
(acc, [key, value]) => acc.replaceAll(`{{${key}}}`, value),
|
|
64
|
+
template
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
function createPersonaResolver(options = {}) {
|
|
68
|
+
if (!options.registry && !options.registryPath) {
|
|
69
|
+
throw new Error("createPersonaResolver requires a registry or registryPath");
|
|
70
|
+
}
|
|
71
|
+
const random = options.random ?? Math.random;
|
|
72
|
+
const now = options.now ?? Date.now;
|
|
73
|
+
const vars = options.vars ?? {};
|
|
74
|
+
const promptsDir = options.promptsDir ?? (options.registryPath ? (0, import_node_path.dirname)(options.registryPath) : process.cwd());
|
|
75
|
+
let registry = options.registry;
|
|
76
|
+
let registryLoadError;
|
|
77
|
+
const promptCache = /* @__PURE__ */ new Map();
|
|
78
|
+
const window = /* @__PURE__ */ new Map();
|
|
79
|
+
const warned = /* @__PURE__ */ new Set();
|
|
80
|
+
function warnOnce(kind, message, error) {
|
|
81
|
+
if (warned.has(kind)) return;
|
|
82
|
+
warned.add(kind);
|
|
83
|
+
console.warn(message, error);
|
|
84
|
+
}
|
|
85
|
+
function loadRegistry() {
|
|
86
|
+
if (registry) return registry;
|
|
87
|
+
if (registryLoadError !== void 0) throw registryLoadError;
|
|
88
|
+
try {
|
|
89
|
+
registry = JSON.parse((0, import_node_fs.readFileSync)(options.registryPath, "utf-8"));
|
|
90
|
+
return registry;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
registryLoadError = error;
|
|
93
|
+
warnOnce(
|
|
94
|
+
"registry",
|
|
95
|
+
`Persona registry load failed (${options.registryPath}); falling back to the default persona for every contact.`,
|
|
96
|
+
error
|
|
97
|
+
);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function contactFor(contactId) {
|
|
102
|
+
try {
|
|
103
|
+
return loadRegistry().contacts?.[contactId];
|
|
104
|
+
} catch {
|
|
105
|
+
return void 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function rollPersonaId(contactId) {
|
|
109
|
+
try {
|
|
110
|
+
const reg = loadRegistry();
|
|
111
|
+
const nowMs = now();
|
|
112
|
+
const existing = window.get(contactId);
|
|
113
|
+
if (existing && existing.expiresAt > nowMs) {
|
|
114
|
+
return existing.personaId;
|
|
115
|
+
}
|
|
116
|
+
for (const [key, value] of window) {
|
|
117
|
+
if (value.expiresAt <= nowMs) window.delete(key);
|
|
118
|
+
}
|
|
119
|
+
const contact = reg.contacts?.[contactId];
|
|
120
|
+
let personaId = null;
|
|
121
|
+
const probability = contact?.probability ?? reg.personaProbability ?? DEFAULT_PROBABILITY;
|
|
122
|
+
if (contact && random() < probability) {
|
|
123
|
+
personaId = contact.persona === reg.defaultPersona ? null : contact.persona;
|
|
124
|
+
}
|
|
125
|
+
const windowMs = (reg.personaWindowMinutes ?? DEFAULT_WINDOW_MINUTES) * 60 * 1e3;
|
|
126
|
+
window.set(contactId, { personaId, expiresAt: nowMs + windowMs });
|
|
127
|
+
return personaId;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function personaLayerFor(personaId) {
|
|
133
|
+
if (!personaId) return null;
|
|
134
|
+
try {
|
|
135
|
+
const definition = loadRegistry().personas[personaId];
|
|
136
|
+
if (!definition) return null;
|
|
137
|
+
if (promptCache.has(personaId)) return promptCache.get(personaId) ?? null;
|
|
138
|
+
try {
|
|
139
|
+
const prompt = interpolate(
|
|
140
|
+
(0, import_node_fs.readFileSync)((0, import_node_path.resolve)(promptsDir, definition.prompt), "utf-8"),
|
|
141
|
+
vars
|
|
142
|
+
);
|
|
143
|
+
promptCache.set(personaId, prompt);
|
|
144
|
+
return prompt;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
promptCache.set(personaId, null);
|
|
147
|
+
warnOnce(
|
|
148
|
+
`prompt:${personaId}`,
|
|
149
|
+
`Persona prompt load failed for "${personaId}" (${definition.prompt}); falling back to the default persona.`,
|
|
150
|
+
error
|
|
151
|
+
);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function defaultPersonaId() {
|
|
159
|
+
try {
|
|
160
|
+
return loadRegistry().defaultPersona ?? null;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function resolvePersonaLayer(contactId) {
|
|
166
|
+
return personaLayerFor(rollPersonaId(contactId)) ?? personaLayerFor(defaultPersonaId());
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
rollPersonaId,
|
|
170
|
+
resolvePersonaLayer,
|
|
171
|
+
personaLayerFor,
|
|
172
|
+
contactFor,
|
|
173
|
+
defaultPersonaId
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/personas/timeContext.ts
|
|
178
|
+
function timeContext(zones, now = Date.now()) {
|
|
179
|
+
if (zones.length === 0) return "";
|
|
180
|
+
const at = new Date(now);
|
|
181
|
+
const parts = zones.flatMap((zone) => {
|
|
182
|
+
try {
|
|
183
|
+
const formatted = new Intl.DateTimeFormat("en-GB", {
|
|
184
|
+
timeZone: zone,
|
|
185
|
+
weekday: "long",
|
|
186
|
+
day: "numeric",
|
|
187
|
+
month: "long",
|
|
188
|
+
year: "numeric",
|
|
189
|
+
hour: "2-digit",
|
|
190
|
+
minute: "2-digit",
|
|
191
|
+
hour12: false
|
|
192
|
+
}).format(at);
|
|
193
|
+
return [`${formatted} (${zone})`];
|
|
194
|
+
} catch {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
if (parts.length === 0) return "";
|
|
199
|
+
return `Current date and time: ${parts.join(" / ")}.`;
|
|
200
|
+
}
|
|
201
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
202
|
+
0 && (module.exports = {
|
|
203
|
+
createGreeter,
|
|
204
|
+
createPersonaResolver,
|
|
205
|
+
timeContext
|
|
206
|
+
});
|