< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Langfuse.LangfuseScoreApiClient
Assembly: NexusLabs.Needlr.AgentFramework.Langfuse
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Langfuse/LangfuseScoreApiClient.cs
Line coverage
91%
Covered lines: 34
Uncovered lines: 3
Coverable lines: 37
Total lines: 86
Line coverage: 91.8%
Branch coverage
83%
Covered branches: 5
Total branches: 6
Branch coverage: 83.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)75%44100%
CreateAsync()100%2283.33%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Langfuse/LangfuseScoreApiClient.cs

#LineLine coverage
 1using System.Net.Http.Json;
 2using System.Text.Json;
 3
 4namespace NexusLabs.Needlr.AgentFramework.Langfuse;
 5
 6/// <summary>
 7/// Posts scores to the Langfuse public Scores API (<c>POST /api/public/scores</c>) using HTTP
 8/// Basic authentication. This is the low-level transport; mapping and failure handling live in
 9/// <see cref="LangfuseScoreRecorder"/>.
 10/// </summary>
 11/// <remarks>
 12/// A score may be ingested before its trace exists; Langfuse links the two by trace id once the
 13/// trace is received. The underlying <see cref="HttpClient"/> is owned by the caller and disposed
 14/// with it.
 15/// </remarks>
 16internal sealed class LangfuseScoreApiClient
 17{
 118    private static readonly JsonSerializerOptions SerializerOptions = new()
 119    {
 120        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 121        DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
 122    };
 23
 24    private readonly HttpClient _httpClient;
 25    private readonly Uri _scoresEndpoint;
 26
 1927    public LangfuseScoreApiClient(HttpClient httpClient, Uri scoresEndpoint, string authorizationHeaderValue)
 28    {
 1929        ArgumentNullException.ThrowIfNull(httpClient);
 1930        ArgumentNullException.ThrowIfNull(scoresEndpoint);
 1931        ArgumentException.ThrowIfNullOrWhiteSpace(authorizationHeaderValue);
 32
 1933        _httpClient = httpClient;
 1934        _scoresEndpoint = scoresEndpoint;
 35
 1936        if (_httpClient.DefaultRequestHeaders.Authorization is null)
 37        {
 1938            var space = authorizationHeaderValue.IndexOf(' ');
 1939            _httpClient.DefaultRequestHeaders.Authorization = space > 0
 1940                ? new System.Net.Http.Headers.AuthenticationHeaderValue(
 1941                    authorizationHeaderValue[..space],
 1942                    authorizationHeaderValue[(space + 1)..])
 1943                : new System.Net.Http.Headers.AuthenticationHeaderValue(authorizationHeaderValue);
 44        }
 1945    }
 46
 47    /// <summary>
 48    /// Sends a single score to Langfuse.
 49    /// </summary>
 50    /// <param name="score">The score to ingest.</param>
 51    /// <param name="cancellationToken">A cancellation token.</param>
 52    /// <exception cref="LangfuseException">The request failed or returned a non-success status.</exception>
 53    public async Task CreateAsync(LangfuseScore score, CancellationToken cancellationToken)
 54    {
 1455        ArgumentNullException.ThrowIfNull(score);
 56
 57        HttpResponseMessage response;
 58        try
 59        {
 1460            response = await _httpClient
 1461                .PostAsJsonAsync(_scoresEndpoint, score, SerializerOptions, cancellationToken)
 1462                .ConfigureAwait(false);
 1463        }
 064        catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
 65        {
 066            throw new LangfuseException(
 067                $"Failed to send score '{score.Name}' to Langfuse at '{_scoresEndpoint}'.", ex);
 68        }
 69
 1470        using (response)
 71        {
 1472            if (response.IsSuccessStatusCode)
 73            {
 1174                return;
 75            }
 76
 377            var body = await response.Content
 378                .ReadAsStringAsync(cancellationToken)
 379                .ConfigureAwait(false);
 80
 381            throw new LangfuseException(
 382                $"Langfuse rejected score '{score.Name}' with status {(int)response.StatusCode} " +
 383                $"({response.ReasonPhrase}): {body}");
 84        }
 1185    }
 86}