@diegoaltoworks/chatter 0.30.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/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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// src/personas/greeter.ts
|
|
2
|
+
function capitalize(name) {
|
|
3
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
4
|
+
}
|
|
5
|
+
function createGreeter({
|
|
6
|
+
resolver,
|
|
7
|
+
templates,
|
|
8
|
+
random = Math.random
|
|
9
|
+
}) {
|
|
10
|
+
return {
|
|
11
|
+
greet(contactId) {
|
|
12
|
+
try {
|
|
13
|
+
const contact = resolver.contactFor(contactId);
|
|
14
|
+
if (!contact?.name) return null;
|
|
15
|
+
const personaId = resolver.rollPersonaId(contactId) ?? resolver.defaultPersonaId();
|
|
16
|
+
const pool = personaId ? templates[personaId] : void 0;
|
|
17
|
+
if (!pool || pool.length === 0) return null;
|
|
18
|
+
const template = pool[Math.floor(random() * pool.length)];
|
|
19
|
+
if (!template) return null;
|
|
20
|
+
return template.replaceAll("{name}", capitalize(contact.name));
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/personas/resolver.ts
|
|
29
|
+
import { readFileSync } from "node:fs";
|
|
30
|
+
import { dirname, resolve } from "node:path";
|
|
31
|
+
var DEFAULT_WINDOW_MINUTES = 10;
|
|
32
|
+
var DEFAULT_PROBABILITY = 0.5;
|
|
33
|
+
function interpolate(template, vars) {
|
|
34
|
+
return Object.entries(vars).reduce(
|
|
35
|
+
(acc, [key, value]) => acc.replaceAll(`{{${key}}}`, value),
|
|
36
|
+
template
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
function createPersonaResolver(options = {}) {
|
|
40
|
+
if (!options.registry && !options.registryPath) {
|
|
41
|
+
throw new Error("createPersonaResolver requires a registry or registryPath");
|
|
42
|
+
}
|
|
43
|
+
const random = options.random ?? Math.random;
|
|
44
|
+
const now = options.now ?? Date.now;
|
|
45
|
+
const vars = options.vars ?? {};
|
|
46
|
+
const promptsDir = options.promptsDir ?? (options.registryPath ? dirname(options.registryPath) : process.cwd());
|
|
47
|
+
let registry = options.registry;
|
|
48
|
+
let registryLoadError;
|
|
49
|
+
const promptCache = /* @__PURE__ */ new Map();
|
|
50
|
+
const window = /* @__PURE__ */ new Map();
|
|
51
|
+
const warned = /* @__PURE__ */ new Set();
|
|
52
|
+
function warnOnce(kind, message, error) {
|
|
53
|
+
if (warned.has(kind)) return;
|
|
54
|
+
warned.add(kind);
|
|
55
|
+
console.warn(message, error);
|
|
56
|
+
}
|
|
57
|
+
function loadRegistry() {
|
|
58
|
+
if (registry) return registry;
|
|
59
|
+
if (registryLoadError !== void 0) throw registryLoadError;
|
|
60
|
+
try {
|
|
61
|
+
registry = JSON.parse(readFileSync(options.registryPath, "utf-8"));
|
|
62
|
+
return registry;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
registryLoadError = error;
|
|
65
|
+
warnOnce(
|
|
66
|
+
"registry",
|
|
67
|
+
`Persona registry load failed (${options.registryPath}); falling back to the default persona for every contact.`,
|
|
68
|
+
error
|
|
69
|
+
);
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function contactFor(contactId) {
|
|
74
|
+
try {
|
|
75
|
+
return loadRegistry().contacts?.[contactId];
|
|
76
|
+
} catch {
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function rollPersonaId(contactId) {
|
|
81
|
+
try {
|
|
82
|
+
const reg = loadRegistry();
|
|
83
|
+
const nowMs = now();
|
|
84
|
+
const existing = window.get(contactId);
|
|
85
|
+
if (existing && existing.expiresAt > nowMs) {
|
|
86
|
+
return existing.personaId;
|
|
87
|
+
}
|
|
88
|
+
for (const [key, value] of window) {
|
|
89
|
+
if (value.expiresAt <= nowMs) window.delete(key);
|
|
90
|
+
}
|
|
91
|
+
const contact = reg.contacts?.[contactId];
|
|
92
|
+
let personaId = null;
|
|
93
|
+
const probability = contact?.probability ?? reg.personaProbability ?? DEFAULT_PROBABILITY;
|
|
94
|
+
if (contact && random() < probability) {
|
|
95
|
+
personaId = contact.persona === reg.defaultPersona ? null : contact.persona;
|
|
96
|
+
}
|
|
97
|
+
const windowMs = (reg.personaWindowMinutes ?? DEFAULT_WINDOW_MINUTES) * 60 * 1e3;
|
|
98
|
+
window.set(contactId, { personaId, expiresAt: nowMs + windowMs });
|
|
99
|
+
return personaId;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function personaLayerFor(personaId) {
|
|
105
|
+
if (!personaId) return null;
|
|
106
|
+
try {
|
|
107
|
+
const definition = loadRegistry().personas[personaId];
|
|
108
|
+
if (!definition) return null;
|
|
109
|
+
if (promptCache.has(personaId)) return promptCache.get(personaId) ?? null;
|
|
110
|
+
try {
|
|
111
|
+
const prompt = interpolate(
|
|
112
|
+
readFileSync(resolve(promptsDir, definition.prompt), "utf-8"),
|
|
113
|
+
vars
|
|
114
|
+
);
|
|
115
|
+
promptCache.set(personaId, prompt);
|
|
116
|
+
return prompt;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
promptCache.set(personaId, null);
|
|
119
|
+
warnOnce(
|
|
120
|
+
`prompt:${personaId}`,
|
|
121
|
+
`Persona prompt load failed for "${personaId}" (${definition.prompt}); falling back to the default persona.`,
|
|
122
|
+
error
|
|
123
|
+
);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function defaultPersonaId() {
|
|
131
|
+
try {
|
|
132
|
+
return loadRegistry().defaultPersona ?? null;
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function resolvePersonaLayer(contactId) {
|
|
138
|
+
return personaLayerFor(rollPersonaId(contactId)) ?? personaLayerFor(defaultPersonaId());
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
rollPersonaId,
|
|
142
|
+
resolvePersonaLayer,
|
|
143
|
+
personaLayerFor,
|
|
144
|
+
contactFor,
|
|
145
|
+
defaultPersonaId
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/personas/timeContext.ts
|
|
150
|
+
function timeContext(zones, now = Date.now()) {
|
|
151
|
+
if (zones.length === 0) return "";
|
|
152
|
+
const at = new Date(now);
|
|
153
|
+
const parts = zones.flatMap((zone) => {
|
|
154
|
+
try {
|
|
155
|
+
const formatted = new Intl.DateTimeFormat("en-GB", {
|
|
156
|
+
timeZone: zone,
|
|
157
|
+
weekday: "long",
|
|
158
|
+
day: "numeric",
|
|
159
|
+
month: "long",
|
|
160
|
+
year: "numeric",
|
|
161
|
+
hour: "2-digit",
|
|
162
|
+
minute: "2-digit",
|
|
163
|
+
hour12: false
|
|
164
|
+
}).format(at);
|
|
165
|
+
return [`${formatted} (${zone})`];
|
|
166
|
+
} catch {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
if (parts.length === 0) return "";
|
|
171
|
+
return `Current date and time: ${parts.join(" / ")}.`;
|
|
172
|
+
}
|
|
173
|
+
export {
|
|
174
|
+
createGreeter,
|
|
175
|
+
createPersonaResolver,
|
|
176
|
+
timeContext
|
|
177
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persona registry resolver — windowed persona selection driven by a JSON
|
|
3
|
+
* registry. Framework-free and content-free: it knows how to roll and load a
|
|
4
|
+
* persona layer, never what a persona says. A registry/prompt failure
|
|
5
|
+
* degrades to `null` (the caller's default layer) rather than throwing into
|
|
6
|
+
* a chat request.
|
|
7
|
+
*/
|
|
8
|
+
export interface PersonaDefinition {
|
|
9
|
+
name: string;
|
|
10
|
+
/** Path to the persona's prompt file, resolved against `promptsDir`. */
|
|
11
|
+
prompt: string;
|
|
12
|
+
language?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface PersonaContact {
|
|
15
|
+
name?: string;
|
|
16
|
+
role?: string;
|
|
17
|
+
/** Persona id rolled for this contact. */
|
|
18
|
+
persona: string;
|
|
19
|
+
/** Overrides `personaProbability` for this contact when set. */
|
|
20
|
+
probability?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface PersonaRegistry {
|
|
23
|
+
/** Persona id used when a contact is unmapped or its roll doesn't land. */
|
|
24
|
+
defaultPersona: string;
|
|
25
|
+
/** 0–1 chance a mapped contact gets their persona on a fresh roll. Default 0.5. */
|
|
26
|
+
personaProbability?: number;
|
|
27
|
+
/** Minutes a roll is held before the next message re-rolls it. Default 10. */
|
|
28
|
+
personaWindowMinutes?: number;
|
|
29
|
+
contacts?: Record<string, PersonaContact>;
|
|
30
|
+
personas: Record<string, PersonaDefinition>;
|
|
31
|
+
}
|
|
32
|
+
export interface PersonaResolverOptions {
|
|
33
|
+
/** Inline registry — takes priority over `registryPath` when both are set. */
|
|
34
|
+
registry?: PersonaRegistry;
|
|
35
|
+
/** Path to a JSON registry file, read once and memoized. */
|
|
36
|
+
registryPath?: string;
|
|
37
|
+
/** Base directory prompt paths resolve against. Defaults to `registryPath`'s directory, or cwd for an inline registry. */
|
|
38
|
+
promptsDir?: string;
|
|
39
|
+
/** Injectable randomness for tests. Default `Math.random`. */
|
|
40
|
+
random?: () => number;
|
|
41
|
+
/** Injectable clock for tests. Default `Date.now`. */
|
|
42
|
+
now?: () => number;
|
|
43
|
+
/** Template vars (`{{name}}`) interpolated into loaded prompt files. */
|
|
44
|
+
vars?: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
export interface PersonaResolver {
|
|
47
|
+
/**
|
|
48
|
+
* The persona id in effect for a contact right now: rolled at
|
|
49
|
+
* `personaProbability` once per window, held for `personaWindowMinutes`.
|
|
50
|
+
* `null` means "use the default persona" (unmapped contact or a lost roll).
|
|
51
|
+
*/
|
|
52
|
+
rollPersonaId(contactId: string): string | null;
|
|
53
|
+
/**
|
|
54
|
+
* The interpolated prompt layer for the contact's currently-held persona,
|
|
55
|
+
* falling back to the default persona's layer. Feeds `prepareChat`'s
|
|
56
|
+
* `personaLayer` directly.
|
|
57
|
+
*/
|
|
58
|
+
resolvePersonaLayer(contactId: string): string | null;
|
|
59
|
+
/** Deterministic layer for an explicit persona id (no dice roll), or null. */
|
|
60
|
+
personaLayerFor(personaId: string | null): string | null;
|
|
61
|
+
/** Registry contact record, or undefined. */
|
|
62
|
+
contactFor(contactId: string): PersonaContact | undefined;
|
|
63
|
+
/** The registry's configured default persona id, or null on failure. */
|
|
64
|
+
defaultPersonaId(): string | null;
|
|
65
|
+
}
|
|
66
|
+
export declare function createPersonaResolver(options?: PersonaResolverOptions): PersonaResolver;
|
|
67
|
+
//# sourceMappingURL=resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/personas/resolver.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAC;IACvB,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8EAA8E;IAC9E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC1C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,sBAAsB;IACrC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0HAA0H;IAC1H,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,sDAAsD;IACtD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAChD;;;;OAIG;IACH,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACtD,8EAA8E;IAC9E,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC;IACzD,6CAA6C;IAC7C,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAAC;IAC1D,wEAAwE;IACxE,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAAC;CACnC;AAYD,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,eAAe,CA+H3F"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.test.d.ts","sourceRoot":"","sources":["../../src/personas/resolver.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live clock line for prompts that answer time-relative questions ("what's
|
|
3
|
+
* next", "today", "tomorrow") — a chat surface has no other way to know the
|
|
4
|
+
* current date/time for the zones it cares about.
|
|
5
|
+
*/
|
|
6
|
+
export declare function timeContext(zones: string[], now?: number): string;
|
|
7
|
+
//# sourceMappingURL=timeContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeContext.d.ts","sourceRoot":"","sources":["../../src/personas/timeContext.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,CAuB7E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeContext.test.d.ts","sourceRoot":"","sources":["../../src/personas/timeContext.test.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegoaltoworks/chatter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Embeddable AI chatbot framework with RAG, authentication, and customizable widgets",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
"import": "./dist/usage/index.mjs",
|
|
36
36
|
"require": "./dist/usage/index.js"
|
|
37
37
|
},
|
|
38
|
+
"./personas": {
|
|
39
|
+
"types": "./dist/personas/index.d.ts",
|
|
40
|
+
"import": "./dist/personas/index.mjs",
|
|
41
|
+
"require": "./dist/personas/index.js"
|
|
42
|
+
},
|
|
38
43
|
"./channels": {
|
|
39
44
|
"types": "./dist/channels/index.d.ts",
|
|
40
45
|
"import": "./dist/channels/index.mjs",
|
|
@@ -53,13 +58,14 @@
|
|
|
53
58
|
],
|
|
54
59
|
"scripts": {
|
|
55
60
|
"dev": "bun --watch src/index.ts",
|
|
56
|
-
"build": "bun run build:clean && bun run build:types && bun run build:esm && bun run build:cjs && bun run build:mcp && bun run build:usage && bun run build:channels && bun run build:whatsapp && bun run build:bin && bun run build:client && bun run build:copy-client",
|
|
61
|
+
"build": "bun run build:clean && bun run build:types && bun run build:esm && bun run build:cjs && bun run build:mcp && bun run build:usage && bun run build:personas && bun run build:channels && bun run build:whatsapp && bun run build:bin && bun run build:client && bun run build:copy-client",
|
|
57
62
|
"build:clean": "rm -rf dist",
|
|
58
63
|
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
|
59
64
|
"build:esm": "esbuild src/index.ts --bundle --format=esm --outfile=dist/index.mjs --external:hono --external:openai --external:@libsql/client --external:jose --platform=node --target=node24",
|
|
60
65
|
"build:cjs": "esbuild src/index.ts --bundle --format=cjs --outfile=dist/index.js --external:hono --external:openai --external:@libsql/client --external:jose --platform=node --target=node24",
|
|
61
66
|
"build:mcp": "esbuild src/mcp-server.ts --bundle --format=esm --outfile=dist/mcp-server.mjs --external:@modelcontextprotocol/sdk --external:openai --external:@libsql/client --external:zod --platform=node --target=node24 && esbuild src/mcp-server.ts --bundle --format=cjs --outfile=dist/mcp-server.js --external:@modelcontextprotocol/sdk --external:openai --external:@libsql/client --external:zod --platform=node --target=node24",
|
|
62
67
|
"build:usage": "esbuild src/usage/index.ts --bundle --format=esm --outfile=dist/usage/index.mjs --external:@libsql/client --platform=node --target=node24 && esbuild src/usage/index.ts --bundle --format=cjs --outfile=dist/usage/index.js --external:@libsql/client --platform=node --target=node24",
|
|
68
|
+
"build:personas": "esbuild src/personas/index.ts --bundle --format=esm --outfile=dist/personas/index.mjs --platform=node --target=node24 && esbuild src/personas/index.ts --bundle --format=cjs --outfile=dist/personas/index.js --platform=node --target=node24",
|
|
63
69
|
"build:channels": "esbuild src/channels/index.ts --bundle --format=esm --outfile=dist/channels/index.mjs --external:hono --external:openai --external:@libsql/client --platform=node --target=node24 && esbuild src/channels/index.ts --bundle --format=cjs --outfile=dist/channels/index.js --external:hono --external:openai --external:@libsql/client --platform=node --target=node24",
|
|
64
70
|
"build:whatsapp": "esbuild src/channels/whatsapp/index.ts --bundle --format=esm --outfile=dist/channels/whatsapp/index.mjs --external:@libsql/client --external:@whiskeysockets/baileys --platform=node --target=node24 && esbuild src/channels/whatsapp/index.ts --bundle --format=cjs --outfile=dist/channels/whatsapp/index.js --external:@libsql/client --external:@whiskeysockets/baileys --platform=node --target=node24",
|
|
65
71
|
"build:bin": "esbuild bin/create-apikey.ts --bundle --format=cjs --outfile=dist/bin/create-apikey.js --external:jose --platform=node --target=node18 --banner:js='#!/usr/bin/env node' && esbuild bin/wa-pair.ts --bundle --format=cjs --outfile=dist/bin/wa-pair.js --external:@libsql/client --external:@whiskeysockets/baileys --external:qrcode-terminal --platform=node --target=node18 --banner:js='#!/usr/bin/env node'",
|