|
2 | 2 | // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. |
3 | 3 | // See the LICENSE file in the project root for more information |
4 | 4 |
|
5 | | -using System.Buffers; |
6 | 5 | using System.Diagnostics; |
7 | 6 | using System.IO.Pipelines; |
8 | | -using System.Runtime.CompilerServices; |
9 | 7 | using System.Text; |
10 | 8 | using System.Text.Json; |
| 9 | +using Elastic.Documentation.Api.Core; |
| 10 | +using Elastic.Documentation.Api.Core.AskAi; |
11 | 11 | using Microsoft.Extensions.Logging; |
12 | 12 |
|
13 | | -namespace Elastic.Documentation.Api.Core.AskAi; |
14 | | - |
15 | | -/// <summary> |
16 | | -/// Represents a parsed Server-Sent Event (SSE) |
17 | | -/// </summary> |
18 | | -/// <param name="EventType">The event type from the "event:" field, or null if not specified</param> |
19 | | -/// <param name="Data">The accumulated data from all "data:" fields</param> |
20 | | -public record SseEvent(string? EventType, string Data); |
| 13 | +namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi; |
21 | 14 |
|
22 | 15 | /// <summary> |
23 | 16 | /// Base class for stream transformers that handles common streaming logic |
@@ -137,7 +130,7 @@ protected virtual async Task ProcessStreamAsync(PipeReader reader, PipeWriter wr |
137 | 130 | _ = activity?.SetParentId(parentActivity.Id); |
138 | 131 |
|
139 | 132 | List<MessagePart> outputMessageParts = []; |
140 | | - await foreach (var sseEvent in ParseSseEventsAsync(reader, cancellationToken)) |
| 133 | + await foreach (var sseEvent in SseParser.ParseAsync(reader, cancellationToken)) |
141 | 134 | { |
142 | 135 | AskAiEvent? transformedEvent; |
143 | 136 | try |
@@ -277,82 +270,4 @@ protected async Task WriteEventAsync(AskAiEvent? transformedEvent, PipeWriter wr |
277 | 270 | throw; // Re-throw to be handled by caller |
278 | 271 | } |
279 | 272 | } |
280 | | - |
281 | | - /// <summary> |
282 | | - /// Parse Server-Sent Events (SSE) from a PipeReader following the W3C SSE specification. |
283 | | - /// This method handles the standard SSE format with event:, data:, and comment lines. |
284 | | - /// </summary> |
285 | | - private static async IAsyncEnumerable<SseEvent> ParseSseEventsAsync( |
286 | | - PipeReader reader, |
287 | | - [EnumeratorCancellation] Cancel cancellationToken |
288 | | - ) |
289 | | - { |
290 | | - string? currentEvent = null; |
291 | | - var dataBuilder = new StringBuilder(); |
292 | | - |
293 | | - while (!cancellationToken.IsCancellationRequested) |
294 | | - { |
295 | | - var result = await reader.ReadAsync(cancellationToken); |
296 | | - var buffer = result.Buffer; |
297 | | - |
298 | | - // Process all complete lines in the buffer |
299 | | - while (TryReadLine(ref buffer, out var line)) |
300 | | - { |
301 | | - // SSE comment line - skip |
302 | | - if (line.Length > 0 && line[0] == ':') |
303 | | - continue; |
304 | | - |
305 | | - // Event type line |
306 | | - if (line.StartsWith("event:", StringComparison.Ordinal)) |
307 | | - currentEvent = line[6..].Trim(); |
308 | | - // Data line |
309 | | - else if (line.StartsWith("data:", StringComparison.Ordinal)) |
310 | | - _ = dataBuilder.Append(line[5..].Trim()); |
311 | | - // Empty line - marks end of event |
312 | | - else if (string.IsNullOrEmpty(line)) |
313 | | - { |
314 | | - if (dataBuilder.Length <= 0) |
315 | | - continue; |
316 | | - yield return new SseEvent(currentEvent, dataBuilder.ToString()); |
317 | | - currentEvent = null; |
318 | | - _ = dataBuilder.Clear(); |
319 | | - } |
320 | | - } |
321 | | - |
322 | | - // Tell the PipeReader how much of the buffer we consumed |
323 | | - reader.AdvanceTo(buffer.Start, buffer.End); |
324 | | - |
325 | | - // Stop reading if there's no more data coming |
326 | | - if (!result.IsCompleted) |
327 | | - continue; |
328 | | - |
329 | | - // Yield any remaining event that hasn't been terminated with an empty line |
330 | | - if (dataBuilder.Length > 0) |
331 | | - yield return new SseEvent(currentEvent, dataBuilder.ToString()); |
332 | | - break; |
333 | | - } |
334 | | - } |
335 | | - |
336 | | - /// <summary> |
337 | | - /// Try to read a single line from the buffer |
338 | | - /// </summary> |
339 | | - private static bool TryReadLine(ref ReadOnlySequence<byte> buffer, out string line) |
340 | | - { |
341 | | - // Look for a line ending |
342 | | - var position = buffer.PositionOf((byte)'\n'); |
343 | | - |
344 | | - if (position == null) |
345 | | - { |
346 | | - line = string.Empty; |
347 | | - return false; |
348 | | - } |
349 | | - |
350 | | - // Extract the line (excluding the \n) |
351 | | - var lineSlice = buffer.Slice(0, position.Value); |
352 | | - line = Encoding.UTF8.GetString(lineSlice).TrimEnd('\r'); |
353 | | - |
354 | | - // Skip past the line + \n |
355 | | - buffer = buffer.Slice(buffer.GetPosition(1, position.Value)); |
356 | | - return true; |
357 | | - } |
358 | 273 | } |
0 commit comments