Skip to main content

langfuse_streaming/
langfuse_streaming.rs

1#![allow(clippy::uninlined_format_args)]
2//! Streaming chat completions with Langfuse observability.
3//!
4//! This example demonstrates how to use streaming responses with the Langfuse interceptor
5//! for real-time observability and tracing.
6//!
7//! ## Setup
8//!
9//! Before running this example, set the following environment variables:
10//! - `OPENAI_API_KEY`: Your `OpenAI` API key
11//! - `LANGFUSE_PUBLIC_KEY`: Your Langfuse public key (starts with "pk-lf-")
12//! - `LANGFUSE_SECRET_KEY`: Your Langfuse secret key (starts with "sk-lf-")
13//! - `LANGFUSE_HOST` (optional): Langfuse API host (defaults to <https://cloud.langfuse.com>)
14//!
15//! ## Running the example
16//!
17//! ```bash
18//! cargo run --example langfuse_streaming
19//! ```
20
21use futures::StreamExt;
22use openai_ergonomic::{Client, LangfuseConfig, LangfuseInterceptor, LangfuseState, Result};
23use opentelemetry::{global, trace::TracerProvider};
24use opentelemetry_langfuse::ExporterBuilder;
25use opentelemetry_sdk::{
26    runtime::Tokio,
27    trace::{span_processor_with_async_runtime::BatchSpanProcessor, SdkTracerProvider, Span},
28};
29
30#[tokio::main]
31async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
32    // Initialize tracing for logging
33    tracing_subscriber::fmt()
34        .with_env_filter(
35            tracing_subscriber::EnvFilter::from_default_env()
36                .add_directive("openai_ergonomic=debug".parse()?),
37        )
38        .init();
39
40    println!("šŸš€ Initializing OpenAI client with Langfuse streaming observability...\n");
41
42    // 1. Build Langfuse exporter from environment variables
43    let exporter = ExporterBuilder::from_env()?.build()?;
44
45    // 2. Create tracer provider with batch processor
46    let provider = SdkTracerProvider::builder()
47        .with_span_processor(BatchSpanProcessor::builder(exporter, Tokio).build())
48        .build();
49
50    // Set as global provider
51    global::set_tracer_provider(provider.clone());
52
53    // 3. Get tracer and create interceptor
54    let tracer = provider.tracer("openai-ergonomic");
55    let langfuse_interceptor = LangfuseInterceptor::new(tracer, LangfuseConfig::new());
56
57    // 4. Create the OpenAI client and add the Langfuse interceptor
58    let client = Client::from_env()?
59        .with_interceptor(Box::new(langfuse_interceptor))
60        .build();
61
62    println!("āœ… Client initialized successfully!");
63    println!("šŸ“Š Streaming traces will be sent to Langfuse for monitoring\n");
64
65    // Example 1: Basic streaming with tracing
66    println!("=== Example 1: Basic Streaming ===");
67    basic_streaming(&client).await?;
68
69    // Example 2: Streaming with parameters
70    println!("\n=== Example 2: Streaming with Parameters ===");
71    streaming_with_parameters(&client).await?;
72
73    // Example 3: Collect full content
74    println!("\n=== Example 3: Collect Full Content ===");
75    collect_content(&client).await?;
76
77    println!("\nāœ… Done! Check your Langfuse dashboard to see the streaming traces.");
78    println!("   - Look for traces with operation names 'chat' or 'responses'");
79    println!("   - Each trace includes:");
80    println!("     • before_request: Initial request details");
81    println!("     • on_stream_chunk: Each chunk as it arrives (real-time)");
82    println!("     • on_stream_end: Final token usage and duration");
83
84    // Give spawned interceptor tasks time to complete
85    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
86
87    // Shutdown the tracer provider to flush all spans
88    println!("\nā³ Flushing spans to Langfuse...");
89    provider.shutdown()?;
90
91    Ok(())
92}
93
94async fn basic_streaming(client: &Client<LangfuseState<Span>>) -> Result<()> {
95    println!("Question: Tell me a short joke");
96
97    let builder = client.chat().user("Tell me a short joke");
98
99    let mut stream = client.send_chat_stream(builder).await?;
100
101    print!("Response: ");
102    let mut chunk_count = 0;
103    while let Some(chunk) = stream.next().await {
104        let chunk = chunk?;
105        if let Some(content) = chunk.content() {
106            print!("{}", content);
107            chunk_count += 1;
108        }
109    }
110    println!(
111        "\n(Received {} chunks, all traced to Langfuse)",
112        chunk_count
113    );
114
115    Ok(())
116}
117
118async fn streaming_with_parameters(client: &Client<LangfuseState<Span>>) -> Result<()> {
119    println!("Question: Write a creative tagline for a bakery");
120
121    let builder = client
122        .chat()
123        .user("Write a creative tagline for a bakery")
124        .temperature(0.9)
125        .max_tokens(50);
126
127    let mut stream = client.send_chat_stream(builder).await?;
128
129    print!("Response: ");
130    let mut chunk_count = 0;
131    while let Some(chunk) = stream.next().await {
132        let chunk = chunk?;
133        if let Some(content) = chunk.content() {
134            print!("{}", content);
135            chunk_count += 1;
136        }
137    }
138    println!(
139        "\n(Received {} chunks, all traced to Langfuse)",
140        chunk_count
141    );
142
143    Ok(())
144}
145
146async fn collect_content(client: &Client<LangfuseState<Span>>) -> Result<()> {
147    println!("Question: What is the capital of France?");
148
149    let builder = client.chat().user("What is the capital of France?");
150
151    let mut stream = client.send_chat_stream(builder).await?;
152
153    // Manually collect content (interceptor hooks are still called for each chunk)
154    let mut content = String::new();
155    while let Some(chunk) = stream.next().await {
156        let chunk = chunk?;
157        if let Some(text) = chunk.content() {
158            content.push_str(text);
159        }
160    }
161    println!("Full response: {}", content);
162    println!("(All chunks were traced to Langfuse during collection)");
163
164    Ok(())
165}