chore: import upstream snapshot with attribution
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:24:08 +08:00
commit 0d3cb498a3
5438 changed files with 1316560 additions and 0 deletions
@@ -0,0 +1,242 @@
# integration-opentelemetry/javascript (OpenTelemetry Tracing Example)
This example demonstrates how to use OpenTelemetry to trace the internal operations of your LLM providers during Promptfoo evaluations.
## Quick Start
```bash
npx promptfoo@latest init --example integration-opentelemetry/javascript
cd integration-opentelemetry/javascript
npm install
npx promptfoo@latest eval
npx promptfoo@latest view
```
To run the trajectory assertion variant from this directory, use:
```bash
npx promptfoo@latest eval -c promptfooconfig.trajectory.yaml --no-cache
```
## Environment Variables
This example requires no API keys - it uses a simulated provider that demonstrates tracing patterns.
## Overview
Promptfoo's OpenTelemetry integration allows you to:
- Trace internal operations of your providers without a custom SDK
- Use standard OpenTelemetry libraries in any language
- Send traces to any OpenTelemetry-compatible backend
- Correlate traces with specific test cases and evaluations
## How It Works
1. **OTLP receiver starts automatically** - Promptfoo ensures the receiver is ready before evaluations begin
2. **Promptfoo generates a trace context** for each test case evaluation
3. **The trace context is passed to providers** via the `traceparent` field
4. **Providers create child spans** using standard OpenTelemetry SDKs
5. **Traces are sent to Promptfoo's OTLP endpoint** (port 4318 by default)
6. **Promptfoo correlates traces** with evaluations for analysis
## Files in This Example
| File | Description |
| --------------------------- | ----------------------------------------------------- |
| `promptfooconfig.yaml` | Evaluation config with tracing enabled and assertions |
| `provider-simple-traced.js` | Simulated RAG provider with comprehensive tracing |
| `trace-assertions.js` | Custom JavaScript assertion for trace validation |
| `package.json` | OpenTelemetry dependencies (v2.x API) |
## Tracing Configuration
Enable tracing in your `promptfooconfig.yaml`:
```yaml
tracing:
enabled: true
otlp:
http:
enabled: true
port: 4318
host: '0.0.0.0'
```
## Instrumenting Your Provider
The provider receives trace context from Promptfoo via the `traceparent` field. Here's the pattern used in this example:
```javascript
const { trace, context, SpanStatusCode } = require('@opentelemetry/api');
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-node');
const { resourceFromAttributes } = require('@opentelemetry/resources');
const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions');
// Initialize OpenTelemetry (v2.x API)
const exporter = new OTLPTraceExporter({
url: 'http://localhost:4318/v1/traces',
});
const provider = new NodeTracerProvider({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: 'my-provider',
}),
spanProcessors: [new BatchSpanProcessor(exporter)],
});
provider.register();
const tracer = trace.getTracer('my-provider');
module.exports = {
async callApi(prompt, promptfooContext) {
// Parse trace context from Promptfoo
if (promptfooContext?.traceparent) {
const matches = promptfooContext.traceparent.match(
/^(\d{2})-([a-f0-9]{32})-([a-f0-9]{16})-(\d{2})$/,
);
if (matches) {
const [, , traceId, parentId, traceFlags] = matches;
// Create parent context
const parentCtx = trace.setSpanContext(context.active(), {
traceId,
spanId: parentId,
traceFlags: parseInt(traceFlags, 16),
isRemote: true,
});
// Run operations within parent context
return context.with(parentCtx, async () => {
const span = tracer.startSpan('my_operation');
try {
// Your provider logic here...
span.setStatus({ code: SpanStatusCode.OK });
return { output: 'result' };
} catch (error) {
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
});
}
}
return { output: 'result without tracing' };
},
};
```
## Trace-Based Assertions
This example demonstrates several trace assertion types:
```yaml
assert:
# Count spans matching a pattern
- type: trace-span-count
value:
pattern: 'retrieve_document_*'
min: 3
max: 3
# Check span duration
- type: trace-span-duration
value:
pattern: 'rag_agent_workflow'
max: 5000 # milliseconds
# Check for error spans
- type: trace-error-spans
value:
max_count: 0
```
The trajectory-specific config at `promptfooconfig.trajectory.yaml` adds:
- `trajectory:tool-used`
- `trajectory:tool-args-match`
- `trajectory:tool-sequence`
- `trajectory:step-count`
Promptfoo accepts generic tool span attributes such as `tool.name` and `tool.arguments`, and it also recognizes Vercel AI SDK telemetry attributes such as `ai.toolCall.name`, `ai.toolCall.args`, `ai.toolCall.arguments`, and `ai.toolCall.input`.
## Viewing Traces
After running an evaluation, view traces in the web UI:
```bash
npx promptfoo@latest view
```
Click on any test result to see the "Trace Timeline" section showing:
- Hierarchical span visualization
- Duration bars showing relative timing
- Status indicators (OK/ERROR)
- Span attributes and events
## Environment Variables
Configure OpenTelemetry using standard environment variables:
```bash
# Custom endpoint (defaults to Promptfoo's receiver)
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
# Headers for authentication with external collectors
export OTEL_EXPORTER_OTLP_HEADERS="api-key=your-key"
# Enable tracing via environment variable
export PROMPTFOO_TRACING_ENABLED=true
```
## Forward to External Collectors
Send traces to Jaeger, Honeycomb, or other OTLP-compatible backends:
```yaml
tracing:
enabled: true
forwarding:
enabled: true
endpoint: 'http://jaeger:4318'
headers:
'api-key': '${JAEGER_API_KEY}'
```
## Troubleshooting
### Context Naming Conflicts
If you see `context.active is not a function`, the OpenTelemetry `context` API conflicts with Promptfoo's context parameter. Rename the parameter:
```javascript
async callApi(prompt, promptfooContext) {
// Use promptfooContext for Promptfoo's context
// Use context from @opentelemetry/api for tracing
}
```
### Traces Not Appearing
1. Verify `tracing.enabled: true` in config
2. Check OTLP receiver is running (look for port 4318 in logs)
3. Ensure trace context is properly parsed from `promptfooContext.traceparent`
4. Call `spanProcessor.forceFlush()` before returning from provider
## Dependencies
This example uses OpenTelemetry v2.x packages:
| Package | Version | Purpose |
| ----------------------------------------- | -------- | ------------------------ |
| `@opentelemetry/api` | ^1.9.0 | Core tracing API |
| `@opentelemetry/sdk-trace-node` | ^2.0.0 | Node.js tracer provider |
| `@opentelemetry/exporter-trace-otlp-http` | ^0.200.0 | OTLP HTTP exporter |
| `@opentelemetry/resources` | ^2.0.0 | Resource attributes |
| `@opentelemetry/semantic-conventions` | ^1.28.0 | Standard attribute names |
@@ -0,0 +1,18 @@
{
"name": "@promptfoo/opentelemetry-tracing-example",
"version": "1.0.0",
"license": "MIT",
"private": true,
"description": "OpenTelemetry tracing integration with Promptfoo evaluations",
"scripts": {
"eval": "promptfoo eval",
"view": "promptfoo view"
},
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/sdk-trace-node": "^2.5.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
"@opentelemetry/resources": "^2.5.0",
"@opentelemetry/semantic-conventions": "^1.39.0"
}
}
@@ -0,0 +1,63 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: Trajectory assertions with OpenTelemetry traces
# Promptfoo accepts generic tool span attributes like tool.name/tool.arguments
# and Vercel AI SDK telemetry attributes like ai.toolCall.name/ai.toolCall.args.
prompts:
- 'Explain how {{topic}} works in simple terms'
providers:
- file://provider-simple-traced.js
tests:
- vars:
topic: 'quantum computing'
metadata:
tracingEnabled: true
testCaseId: 'trajectory-test-case-1'
assert:
- type: trajectory:tool-used
value:
pattern: 'search_*'
min: 3
max: 3
- type: trajectory:tool-used
value: compose_answer
- type: trajectory:tool-args-match
value:
name: search_corpus
args:
query: quantum computing classical computing
- type: trajectory:tool-sequence
value:
mode: exact
steps:
- search_corpus
- search_corpus
- search_corpus
- compose_answer
- type: trajectory:step-count
value:
type: reasoning
min: 4
max: 4
- type: trajectory:step-count
value:
pattern: 'retrieve_document_*'
min: 3
max: 3
tracing:
enabled: true
otlp:
http:
enabled: true
port: 4318
host: '0.0.0.0'
acceptFormats: ['json']
@@ -0,0 +1,132 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: OpenTelemetry tracing with trace-based assertions
providers:
- file://provider-simple-traced.js
prompts:
- 'Explain how {{topic}} works in simple terms'
tests:
- vars:
topic: 'quantum computing'
metadata:
tracingEnabled: true
testCaseId: 'test-case-1'
assert:
# Original javascript assertion
- type: javascript
value: file://trace-assertions.js
# Ensure all expected spans are present
- type: trace-span-count
value:
pattern: 'rag_agent_workflow'
min: 1
max: 1
# Ensure we retrieve exactly 3 documents
- type: trace-span-count
value:
pattern: 'retrieve_document_*'
min: 3
max: 3
# Ensure all reasoning steps occur
- type: trace-span-count
value:
pattern: 'reasoning_*'
min: 3
# Ensure the overall workflow completes quickly
- type: trace-span-duration
value:
pattern: 'rag_agent_workflow'
max: 5000 # 5 seconds max
# Ensure individual operations don't take too long
- type: trace-span-duration
value:
pattern: '*'
max: 1000 # No single span should exceed 1 second
# Ensure no errors occur
- type: trace-error-spans
value:
max_count: 0
- vars:
topic: 'machine learning'
metadata:
tracingEnabled: true
testCaseId: 'test-case-2'
assert:
# Original javascript assertion
- type: javascript
value: file://trace-assertions.js
# Same trace assertions as above
- type: trace-span-count
value:
pattern: 'rag_agent_workflow'
min: 1
max: 1
- type: trace-span-count
value:
pattern: 'retrieve_document_*'
min: 3
max: 3
- type: trace-span-count
value:
pattern: 'reasoning_*'
min: 3
- type: trace-span-duration
value:
pattern: 'rag_agent_workflow'
max: 5000
- type: trace-span-duration
value:
pattern: '*'
max: 1000
- type: trace-error-spans
value:
max_count: 0
# Default assertions that apply to all test cases
defaultTest:
assert:
# Monitor 95th percentile latency (metric only, won't fail tests)
- type: trace-span-duration
value:
pattern: '*'
max: 2000
percentile: 95
weight: 0 # This makes it a metric-only assertion
metric: p95_latency
# Ensure retrieval operations are fast
- type: trace-span-duration
value:
pattern: 'retrieve_document_*'
max: 300 # Each document retrieval should be under 300ms
# Allow up to 5% error rate (more forgiving for production)
- type: trace-error-spans
value:
max_percentage: 5
pattern: '*'
metric: error_rate
# Tracing configuration
tracing:
enabled: true
otlp:
http:
enabled: true
port: 4318
acceptFormats: ['json']
@@ -0,0 +1,416 @@
// provider-simple-traced.js
// RAG/Agent provider with intricate OpenTelemetry tracing
const { trace, context, SpanStatusCode } = require('@opentelemetry/api');
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base'); // Use BatchSpanProcessor
const { resourceFromAttributes } = require('@opentelemetry/resources');
const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } = require('@opentelemetry/semantic-conventions');
// Configure OTLP exporter
const exporterUrl = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces';
console.log('[Provider] Configuring OTLP exporter with URL:', exporterUrl);
const exporter = new OTLPTraceExporter({
url: exporterUrl,
});
// Use BatchSpanProcessor for better timing handling
const spanProcessor = new BatchSpanProcessor(exporter, {
maxQueueSize: 100,
maxExportBatchSize: 10,
scheduledDelayMillis: 500, // Wait 500ms before exporting
exportTimeoutMillis: 30000,
});
// Initialize OpenTelemetry with span processor in constructor (v2.x API)
const provider = new NodeTracerProvider({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: 'simple-traced-provider',
[ATTR_SERVICE_VERSION]: '1.0.0',
}),
spanProcessors: [spanProcessor],
});
provider.register();
// Get a tracer
const tracer = trace.getTracer('simple-traced-provider', '1.0.0');
// Fixed helper function that properly manages span lifecycle
async function runInSpan(spanOrName, attributesOrFn, maybeFn) {
let span;
let fn;
let attributes = {};
// Handle overloaded parameters
if (typeof spanOrName === 'string') {
// Called with (name, attributes, fn) or (name, fn)
if (typeof attributesOrFn === 'function') {
fn = attributesOrFn;
} else {
attributes = attributesOrFn || {};
fn = maybeFn;
}
span = tracer.startSpan(spanOrName, { attributes });
} else {
// Called with (span, fn) - original pattern
span = spanOrName;
fn = attributesOrFn;
}
const ctx = trace.setSpan(context.active(), span);
try {
const result = await context.with(ctx, fn);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
throw error;
} finally {
span.end();
}
}
// Provider implementation
class SimpleTracedProvider {
id() {
return 'simple-traced-provider';
}
async callApi(prompt, promptfooContext) {
console.log('[Provider] Called with:', {
traceparent: promptfooContext?.traceparent,
evaluationId: promptfooContext?.evaluationId,
testCaseId: promptfooContext?.testCaseId,
});
// Check if we have trace context from Promptfoo
if (promptfooContext?.traceparent) {
// Parse W3C trace context
const matches = promptfooContext.traceparent.match(
/^(\d{2})-([a-f0-9]{32})-([a-f0-9]{16})-(\d{2})$/,
);
if (matches) {
const [, _version, traceId, parentId, traceFlags] = matches;
console.log('[Provider] Using trace context:', { traceId, parentId });
// Create parent context from Promptfoo's trace
const parentCtx = trace.setSpanContext(context.active(), {
traceId,
spanId: parentId,
traceFlags: Number.parseInt(traceFlags, 16),
isRemote: true,
});
// Run our operations within the parent context
return context.with(parentCtx, () => this._tracedCallApi(prompt, promptfooContext));
}
}
console.log('[Provider] No trace context, running without tracing');
return this._untracedCallApi(prompt, promptfooContext);
}
async _tracedCallApi(prompt, promptfooContext) {
// Use the improved runInSpan for the main workflow
return runInSpan(
'rag_agent_workflow',
{
'promptfoo.evaluation_id': promptfooContext.evaluationId,
'promptfoo.test_case_id': promptfooContext.testCaseId,
'prompt.text': prompt,
'prompt.length': prompt.length,
'agent.type': 'rag_assistant',
'agent.version': '2.0',
},
async () => {
const span = trace.getSpan(context.active());
const startTime = Date.now();
let totalTokens = 0;
let userIntent;
const documents = [];
// Step 1: Query Analysis
await runInSpan(
'query_analysis',
{
'step.type': 'preprocessing',
'model.name': 'gpt-3.5-turbo',
},
async () => {
const span = trace.getSpan(context.active());
span.addEvent('analyzing_user_intent');
const analysisDelay = 250 + Math.random() * 100;
await new Promise((resolve) => setTimeout(resolve, analysisDelay));
userIntent = {
type: prompt.toLowerCase().includes('compare')
? 'comparison'
: prompt.toLowerCase().includes('explain')
? 'explanation'
: 'general',
entities: ['quantum computing', 'classical computing'],
complexity: 'medium',
};
span.setAttributes({
'intent.type': userIntent.type,
'intent.entities': JSON.stringify(userIntent.entities),
'intent.complexity': userIntent.complexity,
'tokens.used': 120,
});
totalTokens += 120;
},
);
// Step 2: Document Retrieval
await runInSpan(
'document_retrieval',
{
'retrieval.method': 'vector_similarity',
'retrieval.index': 'technical_docs',
},
async () => {
const retrievalSpan = trace.getSpan(context.active());
// Simulate multiple retrieval attempts
for (let i = 0; i < 3; i++) {
await runInSpan(
`retrieve_document_${i}`,
{
'document.index': i,
'search.query': userIntent.entities.join(' '),
'tool.name': 'search_corpus',
'tool.arguments': JSON.stringify({
query: userIntent.entities.join(' '),
document_index: i,
}),
},
async () => {
const docSpan = trace.getSpan(context.active());
const retrievalDelay = 150 + i * 50 + Math.random() * 50;
await new Promise((resolve) => setTimeout(resolve, retrievalDelay));
const doc = {
id: `doc_${i + 1}`,
title: `Technical Document ${i + 1}`,
relevance_score: 0.95 - i * 0.1,
chunk_count: 5,
source: i === 0 ? 'arxiv' : i === 1 ? 'wikipedia' : 'textbook',
};
docSpan.setAttributes({
'document.id': doc.id,
'document.title': doc.title,
'document.relevance_score': doc.relevance_score,
'document.source': doc.source,
});
docSpan.addEvent('document_retrieved', {
chunk_count: doc.chunk_count,
processing_time_ms: retrievalDelay,
});
documents.push(doc);
},
);
}
retrievalSpan.setAttributes({
'retrieval.document_count': documents.length,
'retrieval.top_score': Math.max(...documents.map((d) => d.relevance_score)),
});
},
);
// Step 3: Context Augmentation
await runInSpan(
'context_augmentation',
{
'augmentation.strategy': 'rerank_and_merge',
},
async () => {
const span = trace.getSpan(context.active());
const augmentationDelay = 180 + Math.random() * 70;
await new Promise((resolve) => setTimeout(resolve, augmentationDelay));
span.addEvent('reranking_documents', {
original_count: documents.length,
strategy: 'cross_encoder',
});
span.addEvent('merging_contexts', {
merge_strategy: 'weighted_concatenation',
max_context_length: 4096,
});
span.setAttributes({
'context.final_length': 3500,
'context.document_count': 2,
'tokens.used': 250,
});
totalTokens += 250;
},
);
// Step 4: Reasoning Chain
await runInSpan(
'reasoning_chain',
{
'reasoning.type': 'chain_of_thought',
'model.name': 'gpt-4',
},
async () => {
const reasoningSpan = trace.getSpan(context.active());
// Simulate multiple reasoning steps
const reasoningSteps = [
{ step: 'identify_key_concepts', duration: 320, tokens: 180 },
{ step: 'analyze_relationships', duration: 450, tokens: 220 },
{ step: 'synthesize_answer', duration: 580, tokens: 350 },
];
for (const step of reasoningSteps) {
await runInSpan(`reasoning_${step.step}`, async () => {
const stepSpan = trace.getSpan(context.active());
await new Promise((resolve) => setTimeout(resolve, step.duration));
stepSpan.addEvent(`${step.step}_completed`, {
processing_time_ms: step.duration,
confidence_score: 0.85 + Math.random() * 0.1,
});
stepSpan.setAttributes({
'step.name': step.step,
'step.duration_ms': step.duration,
'step.tokens': step.tokens,
});
totalTokens += step.tokens;
});
}
reasoningSpan.setAttributes({
'reasoning.total_steps': reasoningSteps.length,
'reasoning.total_tokens': reasoningSteps.reduce((sum, s) => sum + s.tokens, 0),
});
},
);
// Step 5: Response Generation
let response;
await runInSpan(
'response_generation',
{
'generation.type': 'augmented_response',
'model.name': 'gpt-4',
'tool.name': 'compose_answer',
'tool.arguments': JSON.stringify({
citation_count: documents.length,
tone: 'explanatory',
}),
},
async () => {
const span = trace.getSpan(context.active());
const generationDelay = 750 + Math.random() * 200;
await new Promise((resolve) => setTimeout(resolve, generationDelay));
response = {
text:
`Based on my analysis of ${documents.length} technical documents, here's a comprehensive explanation:\n\n` +
`${userIntent.entities.join(' and ')} are fascinating topics in computer science. ` +
`After analyzing multiple sources including arxiv papers and textbooks, I can provide the following insights:\n\n` +
`1. Core Concepts: The fundamental principles involve...\n` +
`2. Key Differences: When comparing these technologies...\n` +
`3. Practical Applications: In real-world scenarios...\n\n` +
`This synthesis is based on recent research and established knowledge in the field.`,
citations: documents.map((d) => ({
id: d.id,
title: d.title,
relevance: d.relevance_score,
})),
confidence: 0.92,
};
span.setAttributes({
'response.length': response.text.length,
'response.citations_count': response.citations.length,
'response.confidence': response.confidence,
'tokens.used': 450,
});
span.addEvent('response_finalized', {
word_count: response.text.split(' ').length,
paragraph_count: response.text.split('\n\n').length,
});
totalTokens += 450;
},
);
// Add final span attributes
span.setAttributes({
'workflow.total_duration_ms': Date.now() - startTime,
'workflow.total_steps': 5,
'workflow.total_tokens': totalTokens,
'workflow.success': true,
'response.confidence': response.confidence,
});
span.addEvent('workflow_completed', {
total_processing_time_ms: Date.now() - startTime,
documents_used: documents.length,
reasoning_steps: 3,
});
// Force flush to ensure spans are sent
try {
console.log('[Provider] Flushing spans...');
await spanProcessor.forceFlush();
console.log('[Provider] Spans exported successfully');
} catch (error) {
console.error('[Provider] Failed to flush spans:', error.message);
}
return {
output: response.text,
tokenUsage: {
total: totalTokens,
prompt: Math.floor(totalTokens * 0.4),
completion: Math.floor(totalTokens * 0.6),
},
metadata: {
citations: response.citations,
confidence: response.confidence,
workflow_duration_ms: Date.now() - startTime,
},
};
},
);
}
async _untracedCallApi(prompt, promptfooContext) {
// Simple implementation without tracing
await new Promise((resolve) => setTimeout(resolve, 100));
const topic = prompt.toLowerCase().includes('quantum')
? 'quantum computing'
: 'machine learning';
return {
output: `Here's a simple explanation of ${topic}: It's a fascinating field that involves...`,
tokenUsage: {
total: 50,
prompt: 30,
completion: 20,
},
};
}
}
module.exports = SimpleTracedProvider;
@@ -0,0 +1,103 @@
module.exports = (output, context) => {
// Check if trace data is available
if (!context.trace) {
// Tracing not enabled, skip trace-based checks
return true;
}
const { spans } = context.trace;
// Check for errors in any span
const errorSpans = spans.filter((s) => s.statusCode >= 400);
if (errorSpans.length > 0) {
return {
pass: false,
score: 0,
reason: `Found ${errorSpans.length} error spans`,
};
}
// Calculate total trace duration
if (spans.length > 0) {
const duration =
Math.max(...spans.map((s) => s.endTime || 0)) - Math.min(...spans.map((s) => s.startTime));
if (duration > 5000) {
// 5 seconds
return {
pass: false,
score: 0,
reason: `Trace took too long: ${duration}ms`,
};
}
}
// Check for specific operations - look for HTTP/API calls
const apiCalls = spans.filter((s) => s.name.toLowerCase().includes('http'));
if (apiCalls.length > 10) {
return {
pass: false,
score: 0,
reason: `Too many API calls: ${apiCalls.length}`,
};
}
// Verify all expected RAG steps are present
const expectedSteps = [
'query_analysis',
'document_retrieval',
'context_augmentation',
'reasoning_chain',
'response_generation',
];
const missingSteps = expectedSteps.filter((step) => !spans.some((s) => s.name === step));
if (missingSteps.length > 0) {
return {
pass: false,
score: 0,
reason: `Missing RAG workflow steps: ${missingSteps.join(', ')}`,
};
}
// Check for the main workflow span (optional - may be missing due to timing)
const workflowSpan = spans.find((s) => s.name === 'rag_agent_workflow');
if (workflowSpan) {
// Validate workflow attributes if present
const attrs = workflowSpan.attributes;
if (!attrs['workflow.success'] || attrs['workflow.success'] !== true) {
return {
pass: false,
score: 0,
reason: 'Workflow did not complete successfully',
};
}
}
// Check document retrieval performance
const retrievalSpans = spans.filter((s) => s.name.startsWith('retrieve_document_'));
if (retrievalSpans.length < 3) {
return {
pass: false,
score: 0,
reason: `Expected 3 document retrievals, found ${retrievalSpans.length}`,
};
}
// Check reasoning chain has expected sub-steps
const reasoningSteps = spans.filter((s) => s.name.startsWith('reasoning_'));
if (reasoningSteps.length < 3) {
return {
pass: false,
score: 0,
reason: `Expected at least 3 reasoning steps, found ${reasoningSteps.length}`,
};
}
// All checks passed
return {
pass: true,
score: 1,
reason: 'Trace validation successful',
};
};