using System.Text; using System.Text.Json; using System.Text.Json.Serialization; namespace Svrnty.Sample.AI; /// /// Simple HTTP client for sending traces directly to Langfuse ingestion API /// public class LangfuseHttpClient { private readonly HttpClient _httpClient; private readonly string _publicKey; private readonly string _secretKey; private readonly bool _enabled; public LangfuseHttpClient(HttpClient httpClient, IConfiguration configuration) { _httpClient = httpClient; _publicKey = configuration["Langfuse:PublicKey"] ?? ""; _secretKey = configuration["Langfuse:SecretKey"] ?? ""; _enabled = !string.IsNullOrEmpty(_publicKey) && !string.IsNullOrEmpty(_secretKey); _ = Console.Out.WriteLineAsync($"[Langfuse] Initialized: Enabled={_enabled}, PublicKey={(_publicKey.Length > 0 ? "present" : "missing")}, SecretKey={(_secretKey.Length > 0 ? "present" : "missing")}"); } public bool IsEnabled => _enabled; public async Task CreateTraceAsync(string name, string userId = "system") { return new LangfuseTrace(this, name, userId); } internal async Task SendBatchAsync(List events) { // File-based debug logging try { await File.AppendAllTextAsync("/tmp/langfuse_debug.log", $"{DateTime.UtcNow:O} [SendBatchAsync] Called: Enabled={_enabled}, Events={events.Count}\n"); } catch { } if (!_enabled || events.Count == 0) return; try { var batch = new { batch = events }; var json = JsonSerializer.Serialize(batch, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); _ = Console.Out.WriteLineAsync($"[Langfuse] Sending {events.Count} events to {_httpClient.BaseAddress}/api/public/ingestion"); var request = new HttpRequestMessage(HttpMethod.Post, "/api/public/ingestion") { Content = new StringContent(json, Encoding.UTF8, "application/json") }; // Basic Auth with public/secret keys var authBytes = Encoding.UTF8.GetBytes($"{_publicKey}:{_secretKey}"); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue( "Basic", Convert.ToBase64String(authBytes)); var response = await _httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); _ = Console.Out.WriteLineAsync($"[Langfuse] Successfully sent batch, status: {response.StatusCode}"); } catch (Exception ex) { // Log but don't throw - tracing shouldn't break the application _ = Console.Out.WriteLineAsync($"[Langfuse] Failed to send trace: {ex.Message}"); _ = Console.Out.WriteLineAsync($"[Langfuse] Stack trace: {ex.StackTrace}"); } } } /// /// Represents a Langfuse trace that can contain multiple observations /// public class LangfuseTrace { private readonly LangfuseHttpClient _client; private readonly string _traceId; private readonly List _events = new(); private string? _input; private string? _output; private Dictionary? _metadata; internal LangfuseTrace(LangfuseHttpClient client, string name, string userId) { _client = client; _traceId = Guid.NewGuid().ToString(); _events.Add(new LangfuseEvent { Id = _traceId, Type = "trace-create", Timestamp = DateTime.UtcNow, Body = new Dictionary { ["id"] = _traceId, ["name"] = name, ["userId"] = userId, ["timestamp"] = DateTime.UtcNow } }); } public string TraceId => _traceId; public void SetInput(object input) { _input = input is string s ? s : JsonSerializer.Serialize(input); } public void SetOutput(object output) { _output = output is string s ? s : JsonSerializer.Serialize(output); } public void SetMetadata(Dictionary metadata) { _metadata = metadata; } public LangfuseSpan CreateSpan(string name) { return new LangfuseSpan(this, name); } public LangfuseGeneration CreateGeneration(string name, string model = "qwen2.5-coder:7b") { return new LangfuseGeneration(this, name, model); } internal void AddEvent(LangfuseEvent evt) { _events.Add(evt); } public async Task FlushAsync() { // File-based debug logging try { await File.AppendAllTextAsync("/tmp/langfuse_debug.log", $"{DateTime.UtcNow:O} [FlushAsync] Called: Events={_events.Count}, HasInput={_input != null}, HasOutput={_output != null}, Enabled={_client.IsEnabled}\n"); } catch { } // Update trace with final input/output if (_input != null || _output != null || _metadata != null) { var updateBody = new Dictionary { ["id"] = _traceId }; if (_input != null) updateBody["input"] = _input; if (_output != null) updateBody["output"] = _output; if (_metadata != null) updateBody["metadata"] = _metadata; _events.Add(new LangfuseEvent { Id = Guid.NewGuid().ToString(), Type = "trace-create", // Langfuse uses same type for updates Timestamp = DateTime.UtcNow, Body = updateBody }); } await _client.SendBatchAsync(_events); } } /// /// Represents a span (operation) within a trace /// public class LangfuseSpan : IDisposable { private readonly LangfuseTrace _trace; private readonly string _spanId; private readonly DateTime _startTime; private object? _output; private Dictionary? _metadata; internal LangfuseSpan(LangfuseTrace trace, string name) { _trace = trace; _spanId = Guid.NewGuid().ToString(); _startTime = DateTime.UtcNow; _trace.AddEvent(new LangfuseEvent { Id = _spanId, Type = "span-create", Timestamp = _startTime, Body = new Dictionary { ["id"] = _spanId, ["traceId"] = trace.TraceId, ["name"] = name, ["startTime"] = _startTime } }); } public void SetOutput(object output) { _output = output; } public void SetMetadata(Dictionary metadata) { _metadata = metadata; } public void Dispose() { var updateBody = new Dictionary { ["id"] = _spanId, ["endTime"] = DateTime.UtcNow }; if (_output != null) updateBody["output"] = _output is string s ? s : JsonSerializer.Serialize(_output); if (_metadata != null) updateBody["metadata"] = _metadata; _trace.AddEvent(new LangfuseEvent { Id = Guid.NewGuid().ToString(), Type = "span-update", Timestamp = DateTime.UtcNow, Body = updateBody }); } } /// /// Represents an LLM generation within a trace /// public class LangfuseGeneration : IDisposable { private readonly LangfuseTrace _trace; private readonly string _generationId; private readonly DateTime _startTime; private readonly string _model; private object? _input; private object? _output; private Dictionary? _metadata; internal LangfuseGeneration(LangfuseTrace trace, string name, string model) { _trace = trace; _generationId = Guid.NewGuid().ToString(); _startTime = DateTime.UtcNow; _model = model; _trace.AddEvent(new LangfuseEvent { Id = _generationId, Type = "generation-create", Timestamp = _startTime, Body = new Dictionary { ["id"] = _generationId, ["traceId"] = trace.TraceId, ["name"] = name, ["model"] = model, ["startTime"] = _startTime } }); } public void SetInput(object input) { _input = input; } public void SetOutput(object output) { _output = output; } public void SetMetadata(Dictionary metadata) { _metadata = metadata; } public void Dispose() { var updateBody = new Dictionary { ["id"] = _generationId, ["endTime"] = DateTime.UtcNow }; if (_input != null) updateBody["input"] = _input is string s ? s : JsonSerializer.Serialize(_input); if (_output != null) updateBody["output"] = _output is string o ? o : JsonSerializer.Serialize(_output); if (_metadata != null) updateBody["metadata"] = _metadata; _trace.AddEvent(new LangfuseEvent { Id = Guid.NewGuid().ToString(), Type = "generation-update", Timestamp = DateTime.UtcNow, Body = updateBody }); } } /// /// Internal event format for Langfuse ingestion API /// internal class LangfuseEvent { [JsonPropertyName("id")] public string Id { get; set; } = ""; [JsonPropertyName("type")] public string Type { get; set; } = ""; [JsonPropertyName("timestamp")] public DateTime Timestamp { get; set; } [JsonPropertyName("body")] public Dictionary Body { get; set; } = new(); }