diff --git a/examples/src/otel_trace.ts b/examples/src/otel_trace.ts new file mode 100644 index 000000000..f9049ba1c --- /dev/null +++ b/examples/src/otel_trace.ts @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: 2026 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import { + type JobContext, + ServerOptions, + cli, + defineAgent, + inference, + llm, + log, + logMetrics, + stt, + telemetry, + tts, + voice, +} from '@livekit/agents'; +import * as openai from '@livekit/agents-plugin-openai'; +import { type Attributes } from '@opentelemetry/api'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { BatchSpanProcessor, NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; +import { fileURLToPath } from 'node:url'; +import { z } from 'zod'; + +// This example shows how to trace the agent session with OpenTelemetry. +// It exports spans over OTLP/HTTP, so it works with any OTLP-compatible backend +// (Langfuse, Jaeger, Grafana Tempo, Honeycomb, etc.). To enable tracing, set the trace +// provider with `telemetry.setTracerProvider` at the module level or inside the entrypoint +// before `AgentSession.start()`. +// +// Configure the destination either by passing `url`/`headers` to `setupOtel`, or by leaving +// them unset and exporting the standard OTLP environment variables: +// OTEL_EXPORTER_OTLP_ENDPOINT=https://my-collector.example.com +// OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer +// +// Worked example - Langfuse: the endpoint is `/api/public/otel` and auth +// is a base64-encoded `Authorization: Basic` header built from the public/secret keys: +// const auth = Buffer.from(`${publicKey}:${secretKey}`).toString('base64'); +// setupOtel({ +// url: `${host.replace(/\/$/, '')}/api/public/otel`, +// headers: { Authorization: `Basic ${auth}`, 'x-langfuse-ingestion-version': '4' }, +// }); +// Refer to their docs for latest instructions: https://langfuse.com/integrations/native/opentelemetry#opentelemetry-endpoint +function setupOtel(options?: { + metadata?: Attributes; + url?: string; + headers?: Record; +}): NodeTracerProvider { + const traceExporter = new OTLPTraceExporter({ + url: options?.url, + headers: options?.headers, + }); + const traceProvider = new NodeTracerProvider({ + spanProcessors: [new BatchSpanProcessor(traceExporter)], + }); + + traceProvider.register(); + telemetry.setTracerProvider(traceProvider, { metadata: options?.metadata }); + return traceProvider; +} + +const logger = log().child({ example: 'otel-trace-example' }); + +const lookupWeather = llm.tool({ + name: 'lookupWeather', + description: 'Called when the user asks for weather related information.', + parameters: z.object({ + location: z.string().describe('The location they are asking for'), + }), + execute: async ({ location }) => { + logger.info({ location }, 'Looking up weather'); + return 'sunny with a temperature of 70 degrees.'; + }, +}); + +class Kelly extends voice.Agent { + constructor() { + super({ + instructions: 'Your name is Kelly.', + tools: [ + lookupWeather, + llm.tool({ + name: 'transferToAlloy', + description: 'Transfer the call to Alloy.', + parameters: z.object({}), + execute: async () => { + logger.info('Transferring the call to Alloy'); + return llm.handoff({ agent: new Alloy(), returns: 'Transfer complete.' }); + }, + }), + ], + }); + } + + async onEnter() { + logger.info('Kelly is entering the session'); + this.session.generateReply(); + } +} + +class Alloy extends voice.Agent { + constructor() { + super({ + instructions: 'Your name is Alloy.', + llm: new openai.realtime.RealtimeModel({ voice: 'alloy' }), + tools: [ + lookupWeather, + llm.tool({ + name: 'transferToKelly', + description: 'Transfer the call to Kelly.', + parameters: z.object({}), + execute: async () => { + logger.info('Transferring the call to Kelly'); + return llm.handoff({ agent: new Kelly(), returns: 'Transfer complete.' }); + }, + }), + ], + }); + } + + async onEnter() { + logger.info('Alloy is entering the session'); + this.session.generateReply(); + } +} + +export default defineAgent({ + entry: async (ctx: JobContext) => { + // Set up the OpenTelemetry tracer. + const traceProvider = setupOtel({ + // Metadata is set as attributes on all spans created by the tracer; some backends have + // their own grouping conventions (e.g. Langfuse uses `langfuse.session.id` or `session.id`). + metadata: { + 'session.id': ctx.room.name, + }, + }); + + // Optional: add a shutdown callback to flush the trace before process exit. + ctx.addShutdownCallback(async () => { + await traceProvider.forceFlush(); + }); + + const session = new voice.AgentSession({ + llm: new llm.FallbackAdapter({ + llms: [ + new inference.LLM({ model: 'openai/gpt-4.1-mini' }), + new inference.LLM({ model: 'google/gemini-2.5-flash' }), + ], + }), + stt: new stt.FallbackAdapter({ + sttInstances: [ + new inference.STT({ model: 'deepgram/nova-3' }), + new inference.STT({ model: 'cartesia/ink-whisper' }), + ], + }), + tts: new tts.FallbackAdapter({ + ttsInstances: [ + new inference.TTS({ model: 'cartesia/sonic-3' }), + new inference.TTS({ model: 'rime/arcana' }), + ], + }), + }); + + session.on(voice.AgentSessionEventTypes.MetricsCollected, (ev) => { + logMetrics(ev.metrics); + }); + + await session.start({ + agent: new Kelly(), + room: ctx.room, + }); + }, +}); + +cli.runApp(new ServerOptions({ agent: fileURLToPath(import.meta.url) }));