@fallom/trace 0.1.3 → 0.1.4

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 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
+ };