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;
}
}
}