@fallom/trace 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -1
- package/dist/chunk-IGJD7GBO.mjs +248 -0
- package/dist/chunk-VNUUS74T.mjs +242 -0
- package/dist/index.d.mts +141 -16
- package/dist/index.d.ts +141 -16
- package/dist/index.js +472 -120
- package/dist/index.mjs +141 -37
- package/dist/prompts-67DJ33I4.mjs +14 -0
- package/dist/prompts-ODF4KO2E.mjs +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @fallom/trace
|
|
2
2
|
|
|
3
|
-
Model A/B testing and tracing for LLM applications. Zero latency, production-ready.
|
|
3
|
+
Model A/B testing, prompt management, and tracing for LLM applications. Zero latency, production-ready.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -52,6 +52,82 @@ const model = await models.get("my-config", sessionId, {
|
|
|
52
52
|
});
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
## Prompt Management
|
|
56
|
+
|
|
57
|
+
Manage prompts centrally and A/B test them with zero latency.
|
|
58
|
+
|
|
59
|
+
### Basic Prompt Retrieval
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { prompts } from "@fallom/trace";
|
|
63
|
+
|
|
64
|
+
// Get a managed prompt (with template variables)
|
|
65
|
+
const prompt = await prompts.get("onboarding", {
|
|
66
|
+
variables: { userName: "John", company: "Acme" },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Use the prompt with any LLM
|
|
70
|
+
const response = await openai.chat.completions.create({
|
|
71
|
+
model: "gpt-4o",
|
|
72
|
+
messages: [
|
|
73
|
+
{ role: "system", content: prompt.system },
|
|
74
|
+
{ role: "user", content: prompt.user },
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The `prompt` object contains:
|
|
80
|
+
- `key`: The prompt key
|
|
81
|
+
- `version`: The prompt version
|
|
82
|
+
- `system`: The system prompt (with variables replaced)
|
|
83
|
+
- `user`: The user template (with variables replaced)
|
|
84
|
+
|
|
85
|
+
### Prompt A/B Testing
|
|
86
|
+
|
|
87
|
+
Run experiments on different prompt versions:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { prompts } from "@fallom/trace";
|
|
91
|
+
|
|
92
|
+
// Get prompt from A/B test (sticky assignment based on sessionId)
|
|
93
|
+
const prompt = await prompts.getAB("onboarding-test", sessionId, {
|
|
94
|
+
variables: { userName: "John" },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// prompt.abTestKey and prompt.variantIndex are set
|
|
98
|
+
// for analytics in your dashboard
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Version Pinning
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// Use latest version (default)
|
|
105
|
+
const prompt = await prompts.get("my-prompt");
|
|
106
|
+
|
|
107
|
+
// Pin to specific version
|
|
108
|
+
const prompt = await prompts.get("my-prompt", { version: 2 });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Automatic Trace Tagging
|
|
112
|
+
|
|
113
|
+
When you call `prompts.get()` or `prompts.getAB()`, the next LLM call is automatically tagged with the prompt information. This allows you to see which prompts are used in your traces without any extra code.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// Get prompt - sets up auto-tagging for next LLM call
|
|
117
|
+
const prompt = await prompts.get("onboarding", {
|
|
118
|
+
variables: { userName: "John" },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// This call is automatically tagged with promptKey, promptVersion, etc.
|
|
122
|
+
const response = await openai.chat.completions.create({
|
|
123
|
+
model: "gpt-4o",
|
|
124
|
+
messages: [
|
|
125
|
+
{ role: "system", content: prompt.system },
|
|
126
|
+
{ role: "user", content: prompt.user },
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
55
131
|
## Tracing
|
|
56
132
|
|
|
57
133
|
Wrap your LLM client once, all calls are automatically traced.
|
|
@@ -128,6 +204,7 @@ For each LLM call, Fallom automatically captures:
|
|
|
128
204
|
- ✅ Input/output content (can be disabled)
|
|
129
205
|
- ✅ Errors
|
|
130
206
|
- ✅ Config key + session ID (for A/B analysis)
|
|
207
|
+
- ✅ Prompt key + version (when using prompt management)
|
|
131
208
|
|
|
132
209
|
## Custom Metrics
|
|
133
210
|
|
|
@@ -187,10 +264,52 @@ Set session context for tracing.
|
|
|
187
264
|
|
|
188
265
|
Get model assignment for A/B testing. Returns `Promise<string>`.
|
|
189
266
|
|
|
267
|
+
### `fallom.prompts.get(promptKey, options?)`
|
|
268
|
+
|
|
269
|
+
Get a managed prompt. Returns `Promise<PromptResult>`.
|
|
270
|
+
- `promptKey`: Your prompt key from the dashboard
|
|
271
|
+
- `options.variables`: Template variables (e.g., `{ userName: "John" }`)
|
|
272
|
+
- `options.version`: Pin to specific version (default: latest)
|
|
273
|
+
|
|
274
|
+
### `fallom.prompts.getAB(abTestKey, sessionId, options?)`
|
|
275
|
+
|
|
276
|
+
Get a prompt from an A/B test. Returns `Promise<PromptResult>`.
|
|
277
|
+
- `abTestKey`: Your A/B test key from the dashboard
|
|
278
|
+
- `sessionId`: Session ID for sticky assignment
|
|
279
|
+
- `options.variables`: Template variables
|
|
280
|
+
|
|
190
281
|
### `fallom.trace.span(data)`
|
|
191
282
|
|
|
192
283
|
Record custom business metrics.
|
|
193
284
|
|
|
285
|
+
## Testing
|
|
286
|
+
|
|
287
|
+
Run the test suite:
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
cd sdk/typescript-sdk
|
|
291
|
+
npm install
|
|
292
|
+
npm test
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Deploying
|
|
296
|
+
|
|
297
|
+
To publish a new version to npm:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
cd sdk/typescript-sdk
|
|
301
|
+
|
|
302
|
+
# Update version in package.json
|
|
303
|
+
# Then:
|
|
304
|
+
npm run build
|
|
305
|
+
npm publish --access public
|
|
306
|
+
|
|
307
|
+
# Or use convenience scripts:
|
|
308
|
+
npm run publish:patch # 0.1.0 -> 0.1.1
|
|
309
|
+
npm run publish:minor # 0.1.0 -> 0.2.0
|
|
310
|
+
npm run publish:major # 0.1.0 -> 1.0.0
|
|
311
|
+
```
|
|
312
|
+
|
|
194
313
|
## Requirements
|
|
195
314
|
|
|
196
315
|
- Node.js >= 18.0.0
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/prompts.ts
|
|
8
|
+
var prompts_exports = {};
|
|
9
|
+
__export(prompts_exports, {
|
|
10
|
+
clearPromptContext: () => clearPromptContext,
|
|
11
|
+
get: () => get,
|
|
12
|
+
getAB: () => getAB,
|
|
13
|
+
getPromptContext: () => getPromptContext,
|
|
14
|
+
init: () => init
|
|
15
|
+
});
|
|
16
|
+
import { createHash } from "crypto";
|
|
17
|
+
var apiKey = null;
|
|
18
|
+
var baseUrl = "https://spans.fallom.com";
|
|
19
|
+
var initialized = false;
|
|
20
|
+
var syncInterval = null;
|
|
21
|
+
var debugMode = false;
|
|
22
|
+
var promptCache = /* @__PURE__ */ new Map();
|
|
23
|
+
var promptABCache = /* @__PURE__ */ new Map();
|
|
24
|
+
var promptContext = null;
|
|
25
|
+
var SYNC_TIMEOUT = 2e3;
|
|
26
|
+
function log(msg) {
|
|
27
|
+
if (debugMode) {
|
|
28
|
+
console.log(`[Fallom Prompts] ${msg}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function init(options = {}) {
|
|
32
|
+
apiKey = options.apiKey || process.env.FALLOM_API_KEY || null;
|
|
33
|
+
baseUrl = options.baseUrl || process.env.FALLOM_BASE_URL || "https://spans.fallom.com";
|
|
34
|
+
initialized = true;
|
|
35
|
+
if (!apiKey) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
fetchAll().catch(() => {
|
|
39
|
+
});
|
|
40
|
+
if (!syncInterval) {
|
|
41
|
+
syncInterval = setInterval(() => {
|
|
42
|
+
fetchAll().catch(() => {
|
|
43
|
+
});
|
|
44
|
+
}, 3e4);
|
|
45
|
+
syncInterval.unref();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function ensureInit() {
|
|
49
|
+
if (!initialized) {
|
|
50
|
+
try {
|
|
51
|
+
init();
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function fetchAll() {
|
|
57
|
+
await Promise.all([fetchPrompts(), fetchPromptABTests()]);
|
|
58
|
+
}
|
|
59
|
+
async function fetchPrompts(timeout = SYNC_TIMEOUT) {
|
|
60
|
+
if (!apiKey) return;
|
|
61
|
+
try {
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
64
|
+
const resp = await fetch(`${baseUrl}/prompts`, {
|
|
65
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
66
|
+
signal: controller.signal
|
|
67
|
+
});
|
|
68
|
+
clearTimeout(timeoutId);
|
|
69
|
+
if (resp.ok) {
|
|
70
|
+
const data = await resp.json();
|
|
71
|
+
for (const p of data.prompts || []) {
|
|
72
|
+
if (!promptCache.has(p.key)) {
|
|
73
|
+
promptCache.set(p.key, { versions: /* @__PURE__ */ new Map(), current: null });
|
|
74
|
+
}
|
|
75
|
+
const cached = promptCache.get(p.key);
|
|
76
|
+
cached.versions.set(p.version, {
|
|
77
|
+
systemPrompt: p.system_prompt,
|
|
78
|
+
userTemplate: p.user_template
|
|
79
|
+
});
|
|
80
|
+
cached.current = p.version;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function fetchPromptABTests(timeout = SYNC_TIMEOUT) {
|
|
87
|
+
if (!apiKey) return;
|
|
88
|
+
try {
|
|
89
|
+
const controller = new AbortController();
|
|
90
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
91
|
+
const resp = await fetch(`${baseUrl}/prompt-ab-tests`, {
|
|
92
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
93
|
+
signal: controller.signal
|
|
94
|
+
});
|
|
95
|
+
clearTimeout(timeoutId);
|
|
96
|
+
if (resp.ok) {
|
|
97
|
+
const data = await resp.json();
|
|
98
|
+
for (const t of data.prompt_ab_tests || []) {
|
|
99
|
+
if (!promptABCache.has(t.key)) {
|
|
100
|
+
promptABCache.set(t.key, { versions: /* @__PURE__ */ new Map(), current: null });
|
|
101
|
+
}
|
|
102
|
+
const cached = promptABCache.get(t.key);
|
|
103
|
+
cached.versions.set(t.version, { variants: t.variants });
|
|
104
|
+
cached.current = t.version;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function replaceVariables(template, variables) {
|
|
111
|
+
if (!variables) return template;
|
|
112
|
+
return template.replace(/\{\{(\s*\w+\s*)\}\}/g, (match, varName) => {
|
|
113
|
+
const key = varName.trim();
|
|
114
|
+
return key in variables ? String(variables[key]) : match;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function setPromptContext(ctx) {
|
|
118
|
+
promptContext = ctx;
|
|
119
|
+
}
|
|
120
|
+
function getPromptContext() {
|
|
121
|
+
const ctx = promptContext;
|
|
122
|
+
promptContext = null;
|
|
123
|
+
return ctx;
|
|
124
|
+
}
|
|
125
|
+
async function get(promptKey, options = {}) {
|
|
126
|
+
const { variables, version, debug = false } = options;
|
|
127
|
+
debugMode = debug;
|
|
128
|
+
ensureInit();
|
|
129
|
+
log(`get() called: promptKey=${promptKey}`);
|
|
130
|
+
let promptData = promptCache.get(promptKey);
|
|
131
|
+
if (!promptData) {
|
|
132
|
+
log("Not in cache, fetching...");
|
|
133
|
+
await fetchPrompts(SYNC_TIMEOUT);
|
|
134
|
+
promptData = promptCache.get(promptKey);
|
|
135
|
+
}
|
|
136
|
+
if (!promptData) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Prompt '${promptKey}' not found. Check that it exists in your Fallom dashboard.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const targetVersion = version ?? promptData.current;
|
|
142
|
+
const content = promptData.versions.get(targetVersion);
|
|
143
|
+
if (!content) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Prompt '${promptKey}' version ${targetVersion} not found.`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const system = replaceVariables(content.systemPrompt, variables);
|
|
149
|
+
const user = replaceVariables(content.userTemplate, variables);
|
|
150
|
+
setPromptContext({
|
|
151
|
+
promptKey,
|
|
152
|
+
promptVersion: targetVersion
|
|
153
|
+
});
|
|
154
|
+
log(`\u2705 Got prompt: ${promptKey} v${targetVersion}`);
|
|
155
|
+
return {
|
|
156
|
+
key: promptKey,
|
|
157
|
+
version: targetVersion,
|
|
158
|
+
system,
|
|
159
|
+
user
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async function getAB(abTestKey, sessionId, options = {}) {
|
|
163
|
+
const { variables, debug = false } = options;
|
|
164
|
+
debugMode = debug;
|
|
165
|
+
ensureInit();
|
|
166
|
+
log(`getAB() called: abTestKey=${abTestKey}, sessionId=${sessionId}`);
|
|
167
|
+
let abData = promptABCache.get(abTestKey);
|
|
168
|
+
if (!abData) {
|
|
169
|
+
log("Not in cache, fetching...");
|
|
170
|
+
await fetchPromptABTests(SYNC_TIMEOUT);
|
|
171
|
+
abData = promptABCache.get(abTestKey);
|
|
172
|
+
}
|
|
173
|
+
if (!abData) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Prompt A/B test '${abTestKey}' not found. Check that it exists in your Fallom dashboard.`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const currentVersion = abData.current;
|
|
179
|
+
const versionData = abData.versions.get(currentVersion);
|
|
180
|
+
if (!versionData) {
|
|
181
|
+
throw new Error(`Prompt A/B test '${abTestKey}' has no current version.`);
|
|
182
|
+
}
|
|
183
|
+
const { variants } = versionData;
|
|
184
|
+
const hashBytes = createHash("md5").update(sessionId).digest();
|
|
185
|
+
const hashVal = hashBytes.readUInt32BE(0) % 1e6;
|
|
186
|
+
let cumulative = 0;
|
|
187
|
+
let selectedVariant = variants[variants.length - 1];
|
|
188
|
+
let selectedIndex = variants.length - 1;
|
|
189
|
+
for (let i = 0; i < variants.length; i++) {
|
|
190
|
+
cumulative += variants[i].weight * 1e4;
|
|
191
|
+
if (hashVal < cumulative) {
|
|
192
|
+
selectedVariant = variants[i];
|
|
193
|
+
selectedIndex = i;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const promptKey = selectedVariant.prompt_key;
|
|
198
|
+
const promptVersion = selectedVariant.prompt_version;
|
|
199
|
+
let promptData = promptCache.get(promptKey);
|
|
200
|
+
if (!promptData) {
|
|
201
|
+
await fetchPrompts(SYNC_TIMEOUT);
|
|
202
|
+
promptData = promptCache.get(promptKey);
|
|
203
|
+
}
|
|
204
|
+
if (!promptData) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Prompt '${promptKey}' (from A/B test '${abTestKey}') not found.`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const targetVersion = promptVersion ?? promptData.current;
|
|
210
|
+
const content = promptData.versions.get(targetVersion);
|
|
211
|
+
if (!content) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Prompt '${promptKey}' version ${targetVersion} not found.`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const system = replaceVariables(content.systemPrompt, variables);
|
|
217
|
+
const user = replaceVariables(content.userTemplate, variables);
|
|
218
|
+
setPromptContext({
|
|
219
|
+
promptKey,
|
|
220
|
+
promptVersion: targetVersion,
|
|
221
|
+
abTestKey,
|
|
222
|
+
variantIndex: selectedIndex
|
|
223
|
+
});
|
|
224
|
+
log(
|
|
225
|
+
`\u2705 Got prompt from A/B: ${promptKey} v${targetVersion} (variant ${selectedIndex})`
|
|
226
|
+
);
|
|
227
|
+
return {
|
|
228
|
+
key: promptKey,
|
|
229
|
+
version: targetVersion,
|
|
230
|
+
system,
|
|
231
|
+
user,
|
|
232
|
+
abTestKey,
|
|
233
|
+
variantIndex: selectedIndex
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function clearPromptContext() {
|
|
237
|
+
promptContext = null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export {
|
|
241
|
+
__export,
|
|
242
|
+
init,
|
|
243
|
+
getPromptContext,
|
|
244
|
+
get,
|
|
245
|
+
getAB,
|
|
246
|
+
clearPromptContext,
|
|
247
|
+
prompts_exports
|
|
248
|
+
};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/prompts.ts
|
|
8
|
+
var prompts_exports = {};
|
|
9
|
+
__export(prompts_exports, {
|
|
10
|
+
clearPromptContext: () => clearPromptContext,
|
|
11
|
+
get: () => get,
|
|
12
|
+
getAB: () => getAB,
|
|
13
|
+
getPromptContext: () => getPromptContext,
|
|
14
|
+
init: () => init
|
|
15
|
+
});
|
|
16
|
+
import { createHash } from "crypto";
|
|
17
|
+
var apiKey = null;
|
|
18
|
+
var baseUrl = "https://spans.fallom.com";
|
|
19
|
+
var initialized = false;
|
|
20
|
+
var syncInterval = null;
|
|
21
|
+
var debugMode = false;
|
|
22
|
+
var promptCache = /* @__PURE__ */ new Map();
|
|
23
|
+
var promptABCache = /* @__PURE__ */ new Map();
|
|
24
|
+
var promptContext = null;
|
|
25
|
+
var SYNC_TIMEOUT = 2e3;
|
|
26
|
+
function log(msg) {
|
|
27
|
+
if (debugMode) {
|
|
28
|
+
console.log(`[Fallom Prompts] ${msg}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function init(options = {}) {
|
|
32
|
+
apiKey = options.apiKey || process.env.FALLOM_API_KEY || null;
|
|
33
|
+
baseUrl = options.baseUrl || process.env.FALLOM_BASE_URL || "https://spans.fallom.com";
|
|
34
|
+
initialized = true;
|
|
35
|
+
if (!apiKey) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
fetchAll().catch(() => {
|
|
39
|
+
});
|
|
40
|
+
if (!syncInterval) {
|
|
41
|
+
syncInterval = setInterval(() => {
|
|
42
|
+
fetchAll().catch(() => {
|
|
43
|
+
});
|
|
44
|
+
}, 3e4);
|
|
45
|
+
syncInterval.unref();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function ensureInit() {
|
|
49
|
+
if (!initialized) {
|
|
50
|
+
try {
|
|
51
|
+
init();
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function fetchAll() {
|
|
57
|
+
await Promise.all([fetchPrompts(), fetchPromptABTests()]);
|
|
58
|
+
}
|
|
59
|
+
async function fetchPrompts(timeout = SYNC_TIMEOUT) {
|
|
60
|
+
if (!apiKey) return;
|
|
61
|
+
try {
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
64
|
+
const resp = await fetch(`${baseUrl}/prompts`, {
|
|
65
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
66
|
+
signal: controller.signal
|
|
67
|
+
});
|
|
68
|
+
clearTimeout(timeoutId);
|
|
69
|
+
if (resp.ok) {
|
|
70
|
+
const data = await resp.json();
|
|
71
|
+
for (const p of data.prompts || []) {
|
|
72
|
+
if (!promptCache.has(p.key)) {
|
|
73
|
+
promptCache.set(p.key, { versions: /* @__PURE__ */ new Map(), current: null });
|
|
74
|
+
}
|
|
75
|
+
const cached = promptCache.get(p.key);
|
|
76
|
+
cached.versions.set(p.version, {
|
|
77
|
+
systemPrompt: p.system_prompt,
|
|
78
|
+
userTemplate: p.user_template
|
|
79
|
+
});
|
|
80
|
+
cached.current = p.version;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function fetchPromptABTests(timeout = SYNC_TIMEOUT) {
|
|
87
|
+
if (!apiKey) return;
|
|
88
|
+
try {
|
|
89
|
+
const controller = new AbortController();
|
|
90
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
91
|
+
const resp = await fetch(`${baseUrl}/prompt-ab-tests`, {
|
|
92
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
93
|
+
signal: controller.signal
|
|
94
|
+
});
|
|
95
|
+
clearTimeout(timeoutId);
|
|
96
|
+
if (resp.ok) {
|
|
97
|
+
const data = await resp.json();
|
|
98
|
+
for (const t of data.prompt_ab_tests || []) {
|
|
99
|
+
if (!promptABCache.has(t.key)) {
|
|
100
|
+
promptABCache.set(t.key, { versions: /* @__PURE__ */ new Map(), current: null });
|
|
101
|
+
}
|
|
102
|
+
const cached = promptABCache.get(t.key);
|
|
103
|
+
cached.versions.set(t.version, { variants: t.variants });
|
|
104
|
+
cached.current = t.version;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function replaceVariables(template, variables) {
|
|
111
|
+
if (!variables) return template;
|
|
112
|
+
return template.replace(/\{\{(\s*\w+\s*)\}\}/g, (match, varName) => {
|
|
113
|
+
const key = varName.trim();
|
|
114
|
+
return key in variables ? String(variables[key]) : match;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function setPromptContext(ctx) {
|
|
118
|
+
promptContext = ctx;
|
|
119
|
+
}
|
|
120
|
+
function getPromptContext() {
|
|
121
|
+
const ctx = promptContext;
|
|
122
|
+
promptContext = null;
|
|
123
|
+
return ctx;
|
|
124
|
+
}
|
|
125
|
+
async function get(promptKey, options = {}) {
|
|
126
|
+
const { variables, version, debug = false } = options;
|
|
127
|
+
debugMode = debug;
|
|
128
|
+
ensureInit();
|
|
129
|
+
log(`get() called: promptKey=${promptKey}`);
|
|
130
|
+
let promptData = promptCache.get(promptKey);
|
|
131
|
+
if (!promptData) {
|
|
132
|
+
log("Not in cache, fetching...");
|
|
133
|
+
await fetchPrompts(SYNC_TIMEOUT);
|
|
134
|
+
promptData = promptCache.get(promptKey);
|
|
135
|
+
}
|
|
136
|
+
if (!promptData) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Prompt '${promptKey}' not found. Check that it exists in your Fallom dashboard.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const targetVersion = version ?? promptData.current;
|
|
142
|
+
const content = promptData.versions.get(targetVersion);
|
|
143
|
+
if (!content) {
|
|
144
|
+
throw new Error(`Prompt '${promptKey}' version ${targetVersion} not found.`);
|
|
145
|
+
}
|
|
146
|
+
const system = replaceVariables(content.systemPrompt, variables);
|
|
147
|
+
const user = replaceVariables(content.userTemplate, variables);
|
|
148
|
+
setPromptContext({
|
|
149
|
+
promptKey,
|
|
150
|
+
promptVersion: targetVersion
|
|
151
|
+
});
|
|
152
|
+
log(`\u2705 Got prompt: ${promptKey} v${targetVersion}`);
|
|
153
|
+
return {
|
|
154
|
+
key: promptKey,
|
|
155
|
+
version: targetVersion,
|
|
156
|
+
system,
|
|
157
|
+
user
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function getAB(abTestKey, sessionId, options = {}) {
|
|
161
|
+
const { variables, debug = false } = options;
|
|
162
|
+
debugMode = debug;
|
|
163
|
+
ensureInit();
|
|
164
|
+
log(`getAB() called: abTestKey=${abTestKey}, sessionId=${sessionId}`);
|
|
165
|
+
let abData = promptABCache.get(abTestKey);
|
|
166
|
+
if (!abData) {
|
|
167
|
+
log("Not in cache, fetching...");
|
|
168
|
+
await fetchPromptABTests(SYNC_TIMEOUT);
|
|
169
|
+
abData = promptABCache.get(abTestKey);
|
|
170
|
+
}
|
|
171
|
+
if (!abData) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Prompt A/B test '${abTestKey}' not found. Check that it exists in your Fallom dashboard.`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const currentVersion = abData.current;
|
|
177
|
+
const versionData = abData.versions.get(currentVersion);
|
|
178
|
+
if (!versionData) {
|
|
179
|
+
throw new Error(`Prompt A/B test '${abTestKey}' has no current version.`);
|
|
180
|
+
}
|
|
181
|
+
const { variants } = versionData;
|
|
182
|
+
const hashBytes = createHash("md5").update(sessionId).digest();
|
|
183
|
+
const hashVal = hashBytes.readUInt32BE(0) % 1e6;
|
|
184
|
+
let cumulative = 0;
|
|
185
|
+
let selectedVariant = variants[variants.length - 1];
|
|
186
|
+
let selectedIndex = variants.length - 1;
|
|
187
|
+
for (let i = 0; i < variants.length; i++) {
|
|
188
|
+
cumulative += variants[i].weight * 1e4;
|
|
189
|
+
if (hashVal < cumulative) {
|
|
190
|
+
selectedVariant = variants[i];
|
|
191
|
+
selectedIndex = i;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const promptKey = selectedVariant.prompt_key;
|
|
196
|
+
const promptVersion = selectedVariant.prompt_version;
|
|
197
|
+
let promptData = promptCache.get(promptKey);
|
|
198
|
+
if (!promptData) {
|
|
199
|
+
await fetchPrompts(SYNC_TIMEOUT);
|
|
200
|
+
promptData = promptCache.get(promptKey);
|
|
201
|
+
}
|
|
202
|
+
if (!promptData) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Prompt '${promptKey}' (from A/B test '${abTestKey}') not found.`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const targetVersion = promptVersion ?? promptData.current;
|
|
208
|
+
const content = promptData.versions.get(targetVersion);
|
|
209
|
+
if (!content) {
|
|
210
|
+
throw new Error(`Prompt '${promptKey}' version ${targetVersion} not found.`);
|
|
211
|
+
}
|
|
212
|
+
const system = replaceVariables(content.systemPrompt, variables);
|
|
213
|
+
const user = replaceVariables(content.userTemplate, variables);
|
|
214
|
+
setPromptContext({
|
|
215
|
+
promptKey,
|
|
216
|
+
promptVersion: targetVersion,
|
|
217
|
+
abTestKey,
|
|
218
|
+
variantIndex: selectedIndex
|
|
219
|
+
});
|
|
220
|
+
log(`\u2705 Got prompt from A/B: ${promptKey} v${targetVersion} (variant ${selectedIndex})`);
|
|
221
|
+
return {
|
|
222
|
+
key: promptKey,
|
|
223
|
+
version: targetVersion,
|
|
224
|
+
system,
|
|
225
|
+
user,
|
|
226
|
+
abTestKey,
|
|
227
|
+
variantIndex: selectedIndex
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function clearPromptContext() {
|
|
231
|
+
promptContext = null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export {
|
|
235
|
+
__export,
|
|
236
|
+
init,
|
|
237
|
+
getPromptContext,
|
|
238
|
+
get,
|
|
239
|
+
getAB,
|
|
240
|
+
clearPromptContext,
|
|
241
|
+
prompts_exports
|
|
242
|
+
};
|