@getfoil/foil-js 0.4.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.
Files changed (4) hide show
  1. package/README.md +1029 -0
  2. package/package.json +54 -0
  3. package/src/foil.js +2214 -0
  4. package/src/otel.js +610 -0
package/README.md ADDED
@@ -0,0 +1,1029 @@
1
+ # Foil JavaScript SDK
2
+
3
+ JavaScript/Node.js SDK for monitoring and logging AI agent invocations with Foil. Features distributed tracing, semantic search, custom evaluations, multimodal content support (images, documents), signals/feedback, and framework integrations.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @foil/foil-js
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```javascript
14
+ const { createFoilTracer, SpanKind, content, ContentBlock, MediaCategory } = require('@foil/foil-js');
15
+
16
+ // Create a tracer for your agent
17
+ const tracer = createFoilTracer({
18
+ apiKey: 'your-api-key',
19
+ agentName: 'my-agent',
20
+ baseUrl: 'https://api.foil.dev/api', // or your self-hosted URL
21
+ });
22
+
23
+ // Trace an agent execution
24
+ const result = await tracer.trace(async (ctx) => {
25
+ // Create an LLM span
26
+ const llmSpan = await ctx.startSpan(SpanKind.LLM, 'gpt-4', {
27
+ input: 'What is the weather like?',
28
+ });
29
+
30
+ // ... call your LLM ...
31
+
32
+ await llmSpan.end({
33
+ output: 'The weather is sunny today!',
34
+ tokens: { prompt: 10, completion: 8, total: 18 },
35
+ });
36
+
37
+ return 'Agent completed';
38
+ }, {
39
+ name: 'weather-query',
40
+ input: 'User asked about weather',
41
+ });
42
+ ```
43
+
44
+ ## Table of Contents
45
+
46
+ - [Distributed Tracing](#distributed-tracing)
47
+ - [Semantic Search](#semantic-search)
48
+ - [Custom Evaluations](#custom-evaluations)
49
+ - [Multimodal Content](#multimodal-content)
50
+ - [Signals & Feedback](#signals--feedback)
51
+ - [Framework Integrations](#framework-integrations)
52
+ - [OpenTelemetry Integration](#opentelemetry-integration)
53
+ - [Experiments (A/B Testing)](#experiments-ab-testing)
54
+ - [Low-Level API](#low-level-api)
55
+ - [Debug Mode](#debug-mode)
56
+ - [API Reference](#api-reference)
57
+ - [Examples](#examples)
58
+
59
+ ## Distributed Tracing
60
+
61
+ ### Basic Tracing
62
+
63
+ The tracer captures the full lifecycle of your AI agent: prompts → LLM calls → tool executions → responses.
64
+
65
+ ```javascript
66
+ const tracer = createFoilTracer({
67
+ apiKey: 'your-api-key',
68
+ agentName: 'customer-support-agent',
69
+ });
70
+
71
+ await tracer.trace(async (ctx) => {
72
+ // LLM span for the main model call
73
+ const llm = await ctx.startSpan(SpanKind.LLM, 'gpt-4-turbo', {
74
+ input: messages,
75
+ });
76
+
77
+ const response = await openai.chat.completions.create({
78
+ model: 'gpt-4-turbo',
79
+ messages,
80
+ });
81
+
82
+ await llm.end({
83
+ output: response.choices[0].message.content,
84
+ tokens: {
85
+ prompt: response.usage.prompt_tokens,
86
+ completion: response.usage.completion_tokens,
87
+ total: response.usage.total_tokens,
88
+ },
89
+ });
90
+
91
+ return response.choices[0].message.content;
92
+ }, {
93
+ name: 'support-query',
94
+ input: userMessage,
95
+ sessionId: conversationId, // Group traces by conversation
96
+ });
97
+ ```
98
+
99
+ ### Span Kinds
100
+
101
+ ```javascript
102
+ const { SpanKind } = require('@foil/foil-js');
103
+
104
+ SpanKind.AGENT // Root agent span (automatic for trace())
105
+ SpanKind.LLM // Language model calls
106
+ SpanKind.TOOL // Tool/function executions
107
+ SpanKind.CHAIN // Chain of operations
108
+ SpanKind.RETRIEVER // RAG retrieval operations
109
+ SpanKind.EMBEDDING // Embedding model calls
110
+ SpanKind.CUSTOM // Custom operation types
111
+ ```
112
+
113
+ ### Nested Spans
114
+
115
+ Spans automatically nest based on the call stack:
116
+
117
+ ```javascript
118
+ await tracer.trace(async (ctx) => {
119
+ // LLM decides to use a tool
120
+ const llmSpan = await ctx.startSpan(SpanKind.LLM, 'gpt-4', {
121
+ input: 'Find customer order #12345',
122
+ });
123
+
124
+ // Tool execution (automatically becomes child of LLM span)
125
+ const result = await ctx.tool('lookup_order', async () => {
126
+ return await database.findOrder('12345');
127
+ }, {
128
+ input: { orderId: '12345' },
129
+ });
130
+
131
+ await llmSpan.end({
132
+ output: `Order found: ${JSON.stringify(result)}`,
133
+ });
134
+ }, { name: 'order-lookup' });
135
+ ```
136
+
137
+ ### Convenience Methods
138
+
139
+ ```javascript
140
+ // Wrap tool execution
141
+ await ctx.tool('search_database', async () => {
142
+ return await db.search(query);
143
+ }, { input: { query } });
144
+
145
+ // Wrap retriever operation
146
+ await ctx.retriever('vector_store', async () => {
147
+ return await vectorStore.similaritySearch(query, 5);
148
+ }, { input: query });
149
+
150
+ // Wrap embedding operation
151
+ await ctx.embedding('text-embedding-3-small', async () => {
152
+ return await openai.embeddings.create({ input: text, model: 'text-embedding-3-small' });
153
+ }, { input: text });
154
+ ```
155
+
156
+ ## Semantic Search
157
+
158
+ Search through your traces using natural language queries. Foil automatically embeds your trace content and enables AI-powered semantic search.
159
+
160
+ ### Basic Search
161
+
162
+ ```javascript
163
+ const Foil = require('@foil/foil-js');
164
+
165
+ const foil = new Foil({
166
+ apiKey: 'your-api-key',
167
+ baseUrl: 'https://api.foil.dev/api',
168
+ });
169
+
170
+ // Search for conversations about a topic
171
+ const results = await foil.semanticSearch('conversations about refund requests');
172
+
173
+ console.log(`Found ${results.results.length} matching traces`);
174
+ results.results.forEach((result) => {
175
+ console.log(`Trace: ${result.traceId}, Similarity: ${(result.similarity * 100).toFixed(1)}%`);
176
+ });
177
+ ```
178
+
179
+ ### Search with Filters
180
+
181
+ ```javascript
182
+ const results = await foil.semanticSearch('user asking about pricing', {
183
+ agentId: 'customer-support-agent', // Filter by specific agent
184
+ agentName: 'support-bot', // Or filter by agent name
185
+ from: '2024-01-01', // Start date (ISO format)
186
+ to: '2024-12-31', // End date (ISO format)
187
+ limit: 10, // Max results (default: 20)
188
+ offset: 0, // Pagination offset
189
+ threshold: 0.4, // Min similarity score 0-1 (default: 0.3)
190
+ });
191
+ ```
192
+
193
+ ### Find Similar Traces
194
+
195
+ Find traces that are semantically similar to a specific trace:
196
+
197
+ ```javascript
198
+ // Find traces similar to a known trace
199
+ const similar = await foil.findSimilarTraces('trace-abc-123', {
200
+ limit: 5,
201
+ threshold: 0.4,
202
+ });
203
+
204
+ console.log(`Found ${similar.results.length} similar traces`);
205
+ similar.results.forEach((result) => {
206
+ console.log(`${result.traceId}: ${(result.similarity * 100).toFixed(1)}% similar`);
207
+ });
208
+ ```
209
+
210
+ ### Check Semantic Search Status
211
+
212
+ ```javascript
213
+ // Get overall embedding status
214
+ const status = await foil.getSemanticSearchStatus();
215
+ console.log(`Embedded: ${status.embeddedSpans} / ${status.totalSpans} spans`);
216
+ console.log(`Coverage: ${status.coveragePercent}%`);
217
+ console.log(`Ready: ${status.ready}`);
218
+
219
+ // Get status for specific agent
220
+ const agentStatus = await foil.getSemanticSearchStatus('my-agent-id');
221
+ ```
222
+
223
+ ## Custom Evaluations
224
+
225
+ Define custom evaluation criteria to analyze your agent's responses. Evaluations run automatically on new traces and can be boolean, score-based, or categorical.
226
+
227
+ ### Get Evaluation Templates
228
+
229
+ ```javascript
230
+ // List available pre-built evaluation templates
231
+ const templates = await foil.getEvaluationTemplates();
232
+
233
+ templates.templates.forEach((t) => {
234
+ console.log(`${t.name}: ${t.description}`);
235
+ });
236
+ ```
237
+
238
+ ### Create a Boolean Evaluation
239
+
240
+ ```javascript
241
+ const evaluation = await foil.createEvaluation('agent-123', {
242
+ name: 'professional_tone',
243
+ description: 'Checks if the response maintains a professional tone',
244
+ prompt: `Evaluate the assistant's response for professional tone.
245
+
246
+ Consider:
247
+ - Is the language respectful and courteous?
248
+ - Does it avoid slang or casual expressions?
249
+ - Is it helpful without being condescending?
250
+
251
+ Return true if professional, false otherwise.`,
252
+ evaluationType: 'boolean',
253
+ enabled: true,
254
+ });
255
+
256
+ console.log('Created evaluation:', evaluation.evaluation.name);
257
+ ```
258
+
259
+ ### Create a Score Evaluation (1-10)
260
+
261
+ ```javascript
262
+ const evaluation = await foil.createEvaluation('agent-123', {
263
+ name: 'helpfulness_score',
264
+ description: 'Rates response helpfulness on a scale of 1-10',
265
+ prompt: `Rate the helpfulness of the response on a scale of 1-10.
266
+
267
+ Scoring:
268
+ - 1-3: Unhelpful
269
+ - 4-5: Somewhat helpful
270
+ - 6-7: Helpful
271
+ - 8-9: Very helpful
272
+ - 10: Exceptional
273
+
274
+ Return a single number from 1 to 10.`,
275
+ evaluationType: 'score',
276
+ scoreMin: 1,
277
+ scoreMax: 10,
278
+ enabled: true,
279
+ });
280
+ ```
281
+
282
+ ### Create a Category Evaluation
283
+
284
+ ```javascript
285
+ const evaluation = await foil.createEvaluation('agent-123', {
286
+ name: 'response_intent',
287
+ description: 'Categorizes the type of response',
288
+ prompt: `Classify the response into one category:
289
+
290
+ - informational: Provides facts or explanations
291
+ - action: Guides through a process
292
+ - clarification: Asks for more information
293
+ - escalation: Suggests human assistance
294
+ - closure: Wraps up the conversation
295
+
296
+ Return only the category name.`,
297
+ evaluationType: 'category',
298
+ categories: ['informational', 'action', 'clarification', 'escalation', 'closure'],
299
+ enabled: true,
300
+ });
301
+ ```
302
+
303
+ ### Test an Evaluation
304
+
305
+ Validate your evaluation prompt before enabling:
306
+
307
+ ```javascript
308
+ const result = await foil.testEvaluation('agent-123', evaluationId, {
309
+ input: 'How do I reset my password?',
310
+ output: 'I\'d be happy to help! Go to Settings > Security > Reset Password.',
311
+ });
312
+
313
+ console.log('Result:', result.result); // true/false, score, or category
314
+ console.log('Reasoning:', result.reasoning); // Explanation
315
+ ```
316
+
317
+ ### Add Few-Shot Examples
318
+
319
+ Improve evaluation accuracy with examples:
320
+
321
+ ```javascript
322
+ // Add a positive example
323
+ await foil.addEvaluationExample('agent-123', evaluationId, {
324
+ input: 'Why is my order late?',
325
+ output: 'I apologize for the delay. Let me check the status for you.',
326
+ expectedResult: true,
327
+ reasoning: 'Professional and helpful response',
328
+ });
329
+
330
+ // Add a negative example
331
+ await foil.addEvaluationExample('agent-123', evaluationId, {
332
+ input: 'Why is my order late?',
333
+ output: 'idk check tracking yourself',
334
+ expectedResult: false,
335
+ reasoning: 'Casual and unhelpful',
336
+ });
337
+ ```
338
+
339
+ ### Manage Evaluations
340
+
341
+ ```javascript
342
+ // List all evaluations for an agent
343
+ const evaluations = await foil.getAgentEvaluations('agent-123');
344
+
345
+ // Get specific evaluation details
346
+ const evaluation = await foil.getEvaluation('agent-123', evaluationId);
347
+
348
+ // Update an evaluation
349
+ await foil.updateEvaluation('agent-123', evaluationId, {
350
+ description: 'Updated description',
351
+ enabled: false, // Disable temporarily
352
+ });
353
+
354
+ // Get evaluation analytics
355
+ const analytics = await foil.getEvaluationAnalytics('agent-123', evaluationId);
356
+ console.log('Total evaluations:', analytics.totalEvaluations);
357
+ console.log('Distribution:', analytics.distribution);
358
+
359
+ // Clone a template
360
+ const cloned = await foil.cloneEvaluationTemplate('agent-123', templateId);
361
+
362
+ // Delete an evaluation
363
+ await foil.deleteEvaluation('agent-123', evaluationId);
364
+ ```
365
+
366
+ ## Multimodal Content
367
+
368
+ Foil supports multimodal input/output including images, documents, and other media types.
369
+
370
+ ### Media Categories
371
+
372
+ ```javascript
373
+ const { MediaCategory } = require('@foil/foil-js');
374
+
375
+ MediaCategory.IMAGE // Images (png, jpg, gif, webp, etc.)
376
+ MediaCategory.DOCUMENT // Documents (pdf, doc, docx, etc.)
377
+ MediaCategory.SPREADSHEET // Spreadsheets (xlsx, csv, etc.)
378
+ MediaCategory.CODE // Code files
379
+ MediaCategory.AUDIO // Audio files
380
+ MediaCategory.VIDEO // Video files
381
+ MediaCategory.ARCHIVE // Archives (zip, tar, etc.)
382
+ MediaCategory.NOTEBOOK // Jupyter notebooks
383
+ MediaCategory.OTHER // Other file types
384
+ ```
385
+
386
+ ### Uploading Media
387
+
388
+ ```javascript
389
+ const Foil = require('@foil/foil-js');
390
+
391
+ const foil = new Foil({
392
+ apiKey: 'your-api-key',
393
+ baseUrl: 'https://api.foil.dev/api',
394
+ });
395
+
396
+ // Upload from file path
397
+ const result = await foil.uploadMedia('/path/to/image.png');
398
+ console.log(result.mediaId); // 'media_abc123'
399
+ console.log(result.category); // 'image'
400
+ console.log(result.filename); // 'image.png'
401
+ console.log(result.mimeType); // 'image/png'
402
+
403
+ // Upload from Buffer
404
+ const buffer = await fs.promises.readFile('/path/to/document.pdf');
405
+ const result = await foil.uploadMedia(buffer, {
406
+ filename: 'document.pdf',
407
+ mimeType: 'application/pdf',
408
+ });
409
+
410
+ // Upload with trace/span association
411
+ const result = await foil.uploadMedia('/path/to/image.png', {
412
+ traceId: 'trace_123',
413
+ spanId: 'span_456',
414
+ direction: 'input', // or 'output'
415
+ });
416
+ ```
417
+
418
+ ### Content Blocks
419
+
420
+ Use content blocks to create multimodal input/output:
421
+
422
+ ```javascript
423
+ const { content, ContentBlock, MediaCategory } = require('@foil/foil-js');
424
+
425
+ // Create multimodal content array
426
+ const multimodalInput = content(
427
+ 'Please analyze this image:',
428
+ ContentBlock.media(uploadResult.mediaId, {
429
+ category: MediaCategory.IMAGE,
430
+ filename: 'photo.jpg',
431
+ mimeType: 'image/jpeg',
432
+ }),
433
+ 'Focus on the composition and colors.'
434
+ );
435
+
436
+ // Use in a span
437
+ const span = await ctx.startSpan(SpanKind.LLM, 'gpt-4-vision-preview', {
438
+ input: multimodalInput,
439
+ });
440
+ ```
441
+
442
+ ### Image Analysis Example
443
+
444
+ ```javascript
445
+ const Foil = require('@foil/foil-js');
446
+ const { createFoilTracer, SpanKind, content, ContentBlock, MediaCategory } = require('@foil/foil-js');
447
+
448
+ const foil = new Foil({ apiKey: API_KEY, baseUrl: BASE_URL });
449
+ const tracer = createFoilTracer({ apiKey: API_KEY, agentName: 'vision-agent', baseUrl: BASE_URL });
450
+
451
+ // Upload the image
452
+ const uploadResult = await foil.uploadMedia('/path/to/image.jpg');
453
+
454
+ // Create a trace with multimodal content
455
+ await tracer.trace(async (ctx) => {
456
+ const span = await ctx.startSpan(SpanKind.LLM, 'gpt-4-vision-preview', {
457
+ input: content(
458
+ 'What objects do you see in this image?',
459
+ ContentBlock.media(uploadResult.mediaId, {
460
+ category: MediaCategory.IMAGE,
461
+ filename: uploadResult.filename,
462
+ mimeType: uploadResult.mimeType,
463
+ })
464
+ ),
465
+ });
466
+
467
+ // Call vision model...
468
+ const response = await callVisionModel(uploadResult.mediaId);
469
+
470
+ await span.end({
471
+ output: response.description,
472
+ tokens: { prompt: 1200, completion: 150, total: 1350 },
473
+ });
474
+
475
+ return response;
476
+ }, {
477
+ name: 'image-analysis',
478
+ input: content(
479
+ 'User uploaded an image for analysis',
480
+ ContentBlock.media(uploadResult.mediaId, {
481
+ category: MediaCategory.IMAGE,
482
+ filename: uploadResult.filename,
483
+ })
484
+ ),
485
+ });
486
+ ```
487
+
488
+ ### Media Retrieval
489
+
490
+ ```javascript
491
+ // Get media information
492
+ const mediaInfo = await foil.getMedia(mediaId);
493
+
494
+ // Get presigned URL for download
495
+ const urlInfo = await foil.getMediaUrl(mediaId, 'original');
496
+ console.log(urlInfo.url); // Presigned S3 URL
497
+
498
+ // Get extracted content URL (for documents)
499
+ const extractedUrl = await foil.getMediaUrl(mediaId, 'extracted');
500
+
501
+ // Batch get multiple media
502
+ const batchInfo = await foil.batchMediaInfo([mediaId1, mediaId2, mediaId3]);
503
+ ```
504
+
505
+ ## Signals & Feedback
506
+
507
+ Record user feedback and custom signals for your traces:
508
+
509
+ ```javascript
510
+ await tracer.trace(async (ctx) => {
511
+ // ... agent logic ...
512
+
513
+ // Record user feedback (thumbs up/down)
514
+ await ctx.recordFeedback(true); // thumbs up
515
+
516
+ // Record a rating
517
+ await ctx.recordRating(5); // 5-star rating
518
+
519
+ // Record custom signal
520
+ await ctx.recordSignal('response_quality', 0.95, {
521
+ signalType: 'quality',
522
+ source: 'llm', // or 'user', 'system'
523
+ metadata: { evaluator: 'gpt-4' },
524
+ });
525
+
526
+ return result;
527
+ }, { name: 'support-query' });
528
+ ```
529
+
530
+ ### Direct Signal Recording
531
+
532
+ ```javascript
533
+ const foil = new Foil({ apiKey: API_KEY });
534
+
535
+ // Record a signal for a specific trace
536
+ await foil.recordSignal({
537
+ traceId: 'trace_123',
538
+ signalName: 'user_satisfaction',
539
+ value: 4,
540
+ signalType: 'feedback',
541
+ source: 'user',
542
+ });
543
+
544
+ // Batch record signals
545
+ await foil.recordSignalBatch([
546
+ { traceId: 'trace_123', signalName: 'thumbs', value: true, source: 'user' },
547
+ { traceId: 'trace_123', signalName: 'rating', value: 5, source: 'user' },
548
+ ]);
549
+
550
+ // Get signals for a trace
551
+ const signals = await foil.getTraceSignals('trace_123');
552
+ ```
553
+
554
+ ## Framework Integrations
555
+
556
+ ### OpenAI Integration
557
+
558
+ Automatic tracing for OpenAI calls:
559
+
560
+ ```javascript
561
+ const OpenAI = require('openai');
562
+ const { createFoilTracer } = require('@foil/foil-js');
563
+
564
+ const tracer = createFoilTracer({ apiKey: API_KEY, agentName: 'my-agent' });
565
+
566
+ await tracer.trace(async (ctx) => {
567
+ const openai = new OpenAI();
568
+
569
+ // Wrap OpenAI client for automatic tracing
570
+ tracer.wrapOpenAI(openai, { context: ctx });
571
+
572
+ // All calls are now automatically traced
573
+ const response = await openai.chat.completions.create({
574
+ model: 'gpt-4',
575
+ messages: [{ role: 'user', content: 'Hello!' }],
576
+ });
577
+
578
+ return response.choices[0].message.content;
579
+ }, { name: 'chat' });
580
+ ```
581
+
582
+ ### LangChain Integration
583
+
584
+ ```javascript
585
+ const { createFoilTracer, createLangChainCallback } = require('@foil/foil-js');
586
+
587
+ const tracer = createFoilTracer({ apiKey: API_KEY, agentName: 'langchain-agent' });
588
+ const callbacks = [createLangChainCallback(tracer)];
589
+
590
+ // Use with LangChain
591
+ const chain = new LLMChain({ llm, prompt });
592
+ const result = await chain.call({ input: 'Hello' }, { callbacks });
593
+ ```
594
+
595
+ ### Vercel AI SDK Integration
596
+
597
+ ```javascript
598
+ const { createFoilTracer, createVercelAICallbacks } = require('@foil/foil-js');
599
+
600
+ const tracer = createFoilTracer({ apiKey: API_KEY, agentName: 'vercel-agent' });
601
+ const callbacks = createVercelAICallbacks(tracer);
602
+
603
+ // Use with Vercel AI SDK
604
+ const result = await streamText({
605
+ model: openai('gpt-4'),
606
+ messages,
607
+ ...callbacks,
608
+ });
609
+ ```
610
+
611
+ ## OpenTelemetry Integration
612
+
613
+ Foil supports OpenTelemetry (OTEL) for automatic instrumentation of LLM calls. This enables zero-code tracing when combined with instrumentations like [OpenLLMetry](https://github.com/traceloop/openllmetry-js).
614
+
615
+ ### Quick Setup with Foil.init()
616
+
617
+ The easiest way to set up OpenTelemetry with Foil:
618
+
619
+ ```javascript
620
+ const { Foil } = require('@foil/foil-js/otel');
621
+
622
+ // Initialize Foil with OpenTelemetry
623
+ Foil.init({
624
+ apiKey: process.env.FOIL_API_KEY,
625
+ agentName: 'my-ai-agent', // Optional, groups traces under this agent in Foil
626
+ });
627
+
628
+ // Now all OpenTelemetry traces will be exported to Foil
629
+ // Add instrumentations like OpenLLMetry for automatic LLM tracing
630
+ ```
631
+
632
+ ### Using with OpenLLMetry
633
+
634
+ [OpenLLMetry](https://github.com/traceloop/openllmetry-js) provides automatic instrumentation for popular LLM libraries:
635
+
636
+ ```bash
637
+ npm install @traceloop/node-server-sdk @foil/foil-js @opentelemetry/sdk-trace-node
638
+ ```
639
+
640
+ ```javascript
641
+ const { Foil } = require('@foil/foil-js/otel');
642
+ const Traceloop = require('@traceloop/node-server-sdk');
643
+
644
+ // Initialize Foil first
645
+ const { provider } = Foil.init({
646
+ apiKey: process.env.FOIL_API_KEY,
647
+ agentName: 'my-openai-agent',
648
+ });
649
+
650
+ // Initialize OpenLLMetry with the Foil provider
651
+ Traceloop.initialize({
652
+ disableBatch: true, // Send traces immediately
653
+ tracerProvider: provider,
654
+ });
655
+
656
+ // Now OpenAI, Anthropic, LangChain, etc. calls are automatically traced
657
+ const OpenAI = require('openai');
658
+ const openai = new OpenAI();
659
+
660
+ const response = await openai.chat.completions.create({
661
+ model: 'gpt-4',
662
+ messages: [{ role: 'user', content: 'Hello!' }],
663
+ });
664
+ // ↑ This call is automatically traced to Foil!
665
+ ```
666
+
667
+ ### Manual Setup with FoilSpanProcessor
668
+
669
+ For more control over the OpenTelemetry configuration:
670
+
671
+ ```javascript
672
+ const { FoilSpanProcessor } = require('@foil/foil-js/otel');
673
+ const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
674
+ const { Resource } = require('@opentelemetry/resources');
675
+
676
+ // Create a custom provider
677
+ const provider = new NodeTracerProvider({
678
+ resource: new Resource({
679
+ 'service.name': 'my-custom-agent',
680
+ 'deployment.environment': 'production',
681
+ }),
682
+ });
683
+
684
+ // Add the Foil processor
685
+ provider.addSpanProcessor(new FoilSpanProcessor({
686
+ apiKey: process.env.FOIL_API_KEY,
687
+ maxBatchSize: 100, // Max spans per batch (default: 100)
688
+ scheduledDelayMs: 5000, // Batch export interval (default: 5000ms)
689
+ exportTimeoutMs: 30000, // Request timeout (default: 30000ms)
690
+ debug: true, // Enable debug logging
691
+ }));
692
+
693
+ // Register globally
694
+ provider.register();
695
+ ```
696
+
697
+ ### Configuration Options
698
+
699
+ #### Foil.init() Options
700
+
701
+ | Option | Type | Required | Description |
702
+ |--------|------|----------|-------------|
703
+ | `apiKey` | string | Yes | Your Foil API key |
704
+ | `agentName` | string | No | Agent name for grouping traces in Foil |
705
+ | `endpoint` | string | No | Custom OTLP endpoint URL |
706
+ | `debug` | boolean | No | Enable debug logging |
707
+
708
+ #### FoilSpanProcessor Options
709
+
710
+ | Option | Type | Required | Description |
711
+ |--------|------|----------|-------------|
712
+ | `apiKey` | string | Yes | Your Foil API key |
713
+ | `endpoint` | string | No | Custom OTLP endpoint (default: `https://api.foil.dev/api/otlp/v1/traces`) |
714
+ | `maxBatchSize` | number | No | Maximum spans per batch (default: 100) |
715
+ | `scheduledDelayMs` | number | No | Batch export interval in ms (default: 5000) |
716
+ | `exportTimeoutMs` | number | No | Export request timeout in ms (default: 30000) |
717
+ | `debug` | boolean | No | Enable debug logging |
718
+
719
+ ### Environment Variables
720
+
721
+ | Variable | Description |
722
+ |----------|-------------|
723
+ | `FOIL_API_KEY` | Foil API key (alternative to passing in options) |
724
+ | `FOIL_OTLP_ENDPOINT` | Custom OTLP endpoint URL |
725
+ | `FOIL_DEBUG` | Set to `'true'` to enable debug logging |
726
+ | `OTEL_SERVICE_NAME` | Default service name for traces |
727
+
728
+ ### Supported Instrumentations
729
+
730
+ When using OpenLLMetry or similar, the following are automatically instrumented:
731
+
732
+ - **OpenAI** - Chat completions, embeddings, assistants
733
+ - **Anthropic** - Claude messages
734
+ - **Azure OpenAI** - All Azure OpenAI endpoints
735
+ - **Cohere** - Chat, generate, embed
736
+ - **Google Generative AI** - Gemini models
737
+ - **Bedrock** - AWS Bedrock runtime
738
+ - **LangChain** - Chains, agents, retrievers
739
+ - **LlamaIndex** - Queries, retrievers
740
+
741
+ ### Shutdown and Cleanup
742
+
743
+ Always shut down gracefully to flush pending spans:
744
+
745
+ ```javascript
746
+ // Graceful shutdown
747
+ process.on('SIGTERM', async () => {
748
+ await Foil.shutdown();
749
+ process.exit(0);
750
+ });
751
+
752
+ // Or flush manually
753
+ await Foil.flush();
754
+ ```
755
+
756
+ ### Combining with Manual Tracing
757
+
758
+ You can use OpenTelemetry auto-instrumentation alongside manual Foil tracing:
759
+
760
+ ```javascript
761
+ const { Foil } = require('@foil/foil-js/otel');
762
+ const { createFoilTracer, SpanKind } = require('@foil/foil-js');
763
+
764
+ // Set up OTEL for auto-instrumentation
765
+ Foil.init({ apiKey: API_KEY });
766
+
767
+ // Create a manual tracer for custom spans
768
+ const tracer = createFoilTracer({
769
+ apiKey: API_KEY,
770
+ agentName: 'hybrid-agent',
771
+ });
772
+
773
+ await tracer.trace(async (ctx) => {
774
+ // Manual custom span
775
+ const span = await ctx.startSpan(SpanKind.CUSTOM, 'process-result', {
776
+ input: { action: 'validate' },
777
+ });
778
+
779
+ // Auto-instrumented OpenAI call (via OpenLLMetry)
780
+ const openai = new OpenAI();
781
+ const response = await openai.chat.completions.create({
782
+ model: 'gpt-4',
783
+ messages: [{ role: 'user', content: 'Hello!' }],
784
+ });
785
+
786
+ await span.end({ output: { validated: true } });
787
+
788
+ return response;
789
+ }, { name: 'hybrid-trace' });
790
+ ```
791
+
792
+ ## Experiments (A/B Testing)
793
+
794
+ Get variant assignments for experiments:
795
+
796
+ ```javascript
797
+ const foil = new Foil({ apiKey: API_KEY });
798
+
799
+ // Get assigned variant for a user
800
+ const assignment = await foil.getExperimentVariant('experiment_123', userId);
801
+
802
+ if (assignment.inExperiment) {
803
+ console.log(assignment.variantName); // 'control' or 'treatment'
804
+ console.log(assignment.config); // { model: 'gpt-4', temperature: 0.7 }
805
+ }
806
+ ```
807
+
808
+ ## Low-Level API
809
+
810
+ For more control, use the Foil client directly:
811
+
812
+ ```javascript
813
+ const Foil = require('@foil/foil-js');
814
+
815
+ const foil = new Foil({
816
+ apiKey: 'your-api-key',
817
+ baseUrl: 'https://api.foil.dev/api',
818
+ });
819
+
820
+ // Manual span management
821
+ const traceId = foil.createTraceId();
822
+ const spanId = foil.createSpanId();
823
+
824
+ await foil.startSpan({
825
+ spanId,
826
+ traceId,
827
+ name: 'gpt-4',
828
+ agentName: 'my-agent',
829
+ spanKind: 'llm',
830
+ input: messages,
831
+ });
832
+
833
+ // ... do work ...
834
+
835
+ await foil.endSpan({
836
+ spanId,
837
+ traceId,
838
+ agentName: 'my-agent',
839
+ output: response,
840
+ tokens: { prompt: 100, completion: 50, total: 150 },
841
+ });
842
+
843
+ // Get trace data
844
+ const trace = await foil.getTrace(traceId);
845
+
846
+ // List traces
847
+ const traces = await foil.listTraces({
848
+ agentName: 'my-agent',
849
+ limit: 10,
850
+ from: '2024-01-01T00:00:00Z',
851
+ });
852
+ ```
853
+
854
+ ## Debug Mode
855
+
856
+ Enable debug logging to see trace activity:
857
+
858
+ ```javascript
859
+ // Via environment variable
860
+ process.env.FOIL_DEBUG = 'true';
861
+
862
+ // Or via tracer options
863
+ const tracer = createFoilTracer({
864
+ apiKey: API_KEY,
865
+ agentName: 'my-agent',
866
+ debug: true,
867
+ });
868
+ ```
869
+
870
+ Output:
871
+ ```
872
+ [Foil] ━━━ TRACE START: weather-query ━━━ {"traceId":"abc12345"}
873
+ [Foil] ▶ START [llm] gpt-4 {"spanId":"def67890"}
874
+ [Foil] ■ END [llm] gpt-4 {"spanId":"def67890","duration":"1234ms","tokens":18}
875
+ [Foil] ━━━ TRACE END: weather-query ━━━ {"traceId":"abc12345","duration":"1250ms"}
876
+ ```
877
+
878
+ ## API Reference
879
+
880
+ ### Tracer Methods
881
+
882
+ #### `createFoilTracer(options)`
883
+
884
+ Create a tracer instance.
885
+
886
+ | Option | Type | Required | Description |
887
+ |--------|------|----------|-------------|
888
+ | `apiKey` | string | Yes | Your Foil API key |
889
+ | `agentName` | string | Yes | Unique name for your agent |
890
+ | `baseUrl` | string | No | API base URL (default: api.getfoil.ai) |
891
+ | `debug` | boolean | No | Enable debug logging |
892
+
893
+ #### `tracer.trace(fn, options)`
894
+
895
+ Execute a function within a traced context.
896
+
897
+ | Option | Type | Description |
898
+ |--------|------|-------------|
899
+ | `name` | string | Name for the root span |
900
+ | `traceId` | string | Custom trace ID (auto-generated if not provided) |
901
+ | `sessionId` | string | Session ID for conversation tracking |
902
+ | `input` | any | Input to record (supports multimodal content) |
903
+ | `properties` | object | Custom properties |
904
+
905
+ #### `ctx.startSpan(spanKind, name, attributes)`
906
+
907
+ Start a new span within a trace context.
908
+
909
+ | Attribute | Type | Description |
910
+ |-----------|------|-------------|
911
+ | `input` | any | Input to the operation (supports multimodal) |
912
+ | `properties` | object | Custom properties |
913
+ | `parentSpanId` | string | Explicit parent span ID |
914
+
915
+ #### `span.end(result)`
916
+
917
+ End a span with results.
918
+
919
+ | Field | Type | Description |
920
+ |-------|------|-------------|
921
+ | `output` | any | Output from the operation |
922
+ | `tokens` | object | Token usage `{ prompt, completion, total }` |
923
+ | `error` | object | Error info `{ type, message }` |
924
+ | `ttft` | number | Time to first token (ms) |
925
+
926
+ ### Foil Client Methods
927
+
928
+ #### Semantic Search
929
+
930
+ | Method | Description |
931
+ |--------|-------------|
932
+ | `semanticSearch(query, options)` | Search spans using natural language |
933
+ | `findSimilarTraces(traceId, options)` | Find traces similar to a given trace |
934
+ | `getSemanticSearchStatus(agentId?)` | Get embedding statistics |
935
+
936
+ #### Custom Evaluations
937
+
938
+ | Method | Description |
939
+ |--------|-------------|
940
+ | `getEvaluationTemplates()` | List available evaluation templates |
941
+ | `getAgentEvaluations(agentId)` | List evaluations for an agent |
942
+ | `getEvaluation(agentId, evaluationId)` | Get evaluation details |
943
+ | `createEvaluation(agentId, data)` | Create a custom evaluation |
944
+ | `updateEvaluation(agentId, evaluationId, data)` | Update an evaluation |
945
+ | `deleteEvaluation(agentId, evaluationId)` | Delete an evaluation |
946
+ | `testEvaluation(agentId, evaluationId, data)` | Test with sample data |
947
+ | `cloneEvaluationTemplate(agentId, templateId)` | Clone a template |
948
+ | `addEvaluationExample(agentId, evaluationId, data)` | Add few-shot example |
949
+ | `removeEvaluationExample(agentId, evaluationId, exampleId)` | Remove example |
950
+ | `getEvaluationAnalytics(agentId, evaluationId)` | Get evaluation metrics |
951
+
952
+ #### Media
953
+
954
+ | Method | Description |
955
+ |--------|-------------|
956
+ | `uploadMedia(file, options)` | Upload media for multimodal content |
957
+ | `getMedia(mediaId, options)` | Get media information |
958
+ | `getMediaUrl(mediaId, content)` | Get presigned download URL |
959
+ | `batchMediaInfo(mediaIds)` | Get info for multiple media |
960
+
961
+ #### Signals
962
+
963
+ | Method | Description |
964
+ |--------|-------------|
965
+ | `recordSignal(data)` | Record a signal |
966
+ | `recordSignalBatch(signals)` | Record multiple signals |
967
+ | `getTraceSignals(traceId)` | Get signals for a trace |
968
+
969
+ #### Traces
970
+
971
+ | Method | Description |
972
+ |--------|-------------|
973
+ | `startSpan(data)` | Start a span |
974
+ | `endSpan(data)` | End a span |
975
+ | `getTrace(traceId)` | Get trace with all spans |
976
+ | `listTraces(options)` | List traces with filters |
977
+
978
+ #### Experiments
979
+
980
+ | Method | Description |
981
+ |--------|-------------|
982
+ | `getExperimentVariant(experimentId, identifier)` | Get variant assignment |
983
+
984
+ ### Content Helpers
985
+
986
+ #### `content(...parts)` / `ContentBlock`
987
+
988
+ Create multimodal content arrays.
989
+
990
+ ```javascript
991
+ // Using content() helper
992
+ const input = content(
993
+ 'Analyze this:',
994
+ ContentBlock.media(mediaId, { category: MediaCategory.IMAGE }),
995
+ 'What do you see?'
996
+ );
997
+
998
+ // Manual content block creation
999
+ const blocks = [
1000
+ ContentBlock.text('Hello'),
1001
+ ContentBlock.media(mediaId, { category: MediaCategory.IMAGE, filename: 'photo.jpg' }),
1002
+ ];
1003
+ ```
1004
+
1005
+ ## Examples
1006
+
1007
+ See the `examples/` directory for complete working examples:
1008
+
1009
+ - `tracer-test.js` - Basic tracing setup
1010
+ - `openai-agent-test.js` - OpenAI integration
1011
+ - `multimodal-test.js` - Multimodal content handling
1012
+ - `semantic-search-example.js` - Semantic search usage
1013
+ - `custom-evaluations-example.js` - Custom evaluations
1014
+ - `otel-integration-example.js` - OpenTelemetry integration with multiple approaches
1015
+
1016
+ Run examples:
1017
+
1018
+ ```bash
1019
+ npm run example:tracer
1020
+ npm run example:openai
1021
+ npm run example:multimodal
1022
+ npm run example:semantic-search
1023
+ npm run example:evaluations
1024
+ npm run example:otel
1025
+ ```
1026
+
1027
+ ## License
1028
+
1029
+ MIT