using System.Text.Json; namespace AIStudio.Provider.OpenAI; /// /// Reads the sources a provider puts into its Chat Completions stream. /// /// /// Where those sit differs per provider: OpenAI announces them on annotation lines of their own, /// Perplexity puts its search results into the very line that carries the text. The plain text /// path reads both through the provider's own stream line types, and so does this -- otherwise /// the tool calling rounds would be the one place where a citation link goes missing. /// public static class ChatCompletionSourceReader { private const string DONE = "[DONE]"; /// /// Reads whatever sources one line of the stream announced. /// /// The event to read. /// The provider's delta stream line type. /// The provider's annotation stream line type. /// The sources of this line, empty when it announced none. public static IList Read(ServerSentEvent serverSentEvent) where TDelta : IResponseStreamLine where TAnnotation : IAnnotationStreamLine { if (serverSentEvent.Data.Length is 0 || serverSentEvent.Data is DONE) return []; // // The same split the plain text path makes, and for the same reason: a line is either an // annotation line or a delta line, and reading it as both would count its sources twice. // var annotationSupported = typeof(TAnnotation) != typeof(NoResponsesAnnotationStreamLine) && typeof(TAnnotation) != typeof(NoChatCompletionAnnotationStreamLine); if (annotationSupported && serverSentEvent.Line.Contains(""" "annotations":[ """, StringComparison.InvariantCulture)) { var annotationLine = TryDeserialize(serverSentEvent.Data); return annotationLine is not null && annotationLine.ContainsSources() ? annotationLine.GetSources() : []; } var deltaLine = TryDeserialize(serverSentEvent.Data); return deltaLine is not null && deltaLine.ContainsSources() ? deltaLine.GetSources() : []; } private static T? TryDeserialize(string json) { try { return JsonSerializer.Deserialize(json, ProviderJsonOptions.OPTIONS); } catch (JsonException) { return default; } } }