2025-06-30 18:56:48 +02:00
using System.Text ;
using System.Text.Json ;
2026-05-08 17:37:34 +02:00
using System.Runtime.CompilerServices ;
2025-06-30 18:56:48 +02:00
2026-08-10 21:36:01 +02:00
using AIStudio.Settings ;
2026-08-10 18:21:31 +02:00
using AIStudio.Tools.Rust ;
2025-05-02 23:09:50 +02:00
namespace AIStudio.Tools.Services ;
public sealed partial class RustService
{
2025-06-30 21:51:02 +02:00
public async Task < string > ReadArbitraryFileData ( string path , int maxChunks , bool extractImages = false )
2025-05-02 23:09:50 +02:00
{
2025-06-30 21:51:02 +02:00
var streamId = Guid . NewGuid (). ToString ();
2026-08-10 18:21:31 +02:00
var requestUri = $"/retrieval/fs/extract?path={Uri.EscapeDataString(path)}&stream_id={streamId}&extract_images={extractImages}&include_token_count=false" ;
2025-06-30 18:56:48 +02:00
var request = new HttpRequestMessage ( HttpMethod . Get , requestUri );
var response = await this . http . SendAsync ( request , HttpCompletionOption . ResponseHeadersRead );
2025-05-02 23:09:50 +02:00
if (! response . IsSuccessStatusCode )
2026-05-12 20:31:08 +02:00
{
var responseBody = await response . Content . ReadAsStringAsync ();
this . logger ?. LogError (
"Failed to read arbitrary file data from Rust runtime. Status: {StatusCode}, reason: '{ReasonPhrase}', path: '{Path}', body: '{Body}'" ,
response . StatusCode ,
response . ReasonPhrase ,
path ,
responseBody );
2025-05-02 23:09:50 +02:00
return string . Empty ;
2026-05-12 20:31:08 +02:00
}
2025-05-02 23:09:50 +02:00
2025-06-30 18:56:48 +02:00
var resultBuilder = new StringBuilder ();
2025-07-01 18:34:14 +02:00
try
2025-06-30 18:56:48 +02:00
{
2025-07-01 18:34:14 +02:00
await using var stream = await response . Content . ReadAsStreamAsync ();
using var reader = new StreamReader ( stream );
var chunkCount = 0 ;
while (! reader . EndOfStream && chunkCount < maxChunks )
2025-06-30 18:56:48 +02:00
{
2025-07-01 18:34:14 +02:00
var line = await reader . ReadLineAsync ();
if ( string . IsNullOrWhiteSpace ( line ))
continue ;
if (! line . StartsWith ( "data:" , StringComparison . InvariantCulture ))
continue ;
var jsonContent = line [ 5. .];
try
2025-06-30 18:56:48 +02:00
{
2025-07-01 18:34:14 +02:00
var sseEvent = JsonSerializer . Deserialize < ContentStreamSseEvent >( jsonContent );
if ( sseEvent is not null )
{
var content = ContentStreamSseHandler . ProcessEvent ( sseEvent , extractImages );
if ( content is not null )
resultBuilder . AppendLine ( content );
chunkCount ++;
}
}
catch ( JsonException )
{
2026-05-08 17:37:34 +02:00
if ( this . TryLogSseErrorMessage ( jsonContent , path ))
continue ;
2025-07-01 18:34:14 +02:00
this . logger ?. LogError ( "Failed to deserialize SSE event: {JsonContent}" , jsonContent );
2025-06-30 18:56:48 +02:00
}
}
2025-07-01 18:34:14 +02:00
}
catch ( Exception e )
{
this . logger ?. LogError ( e , "Error reading file data from stream: {Path}" , path );
}
finally
{
var finalContentChunk = ContentStreamSseHandler . Clear ( streamId );
if (! string . IsNullOrWhiteSpace ( finalContentChunk ))
resultBuilder . AppendLine ( finalContentChunk );
2025-06-30 18:56:48 +02:00
}
return resultBuilder . ToString ();
2025-05-02 23:09:50 +02:00
}
2026-05-08 17:37:34 +02:00
public async IAsyncEnumerable < string > StreamArbitraryFileData ( string path , bool extractImages = false , [ EnumeratorCancellation ] CancellationToken token = default )
2026-08-10 18:21:31 +02:00
{
2026-08-10 21:36:01 +02:00
await foreach ( var segment in this . StreamArbitraryFileDataCore ( path , extractImages , false , string . Empty , token ))
2026-08-10 18:21:31 +02:00
yield return segment . Content ;
}
public async IAsyncEnumerable < ArbitraryFileDataSegment > StreamArbitraryFileDataWithTokenCounts (
string path ,
2026-08-10 21:36:01 +02:00
EmbeddingProvider embeddingProvider ,
2026-08-10 18:21:31 +02:00
[EnumeratorCancellation] CancellationToken token = default )
{
2026-08-10 21:36:01 +02:00
await foreach ( var segment in this . StreamArbitraryFileDataCore ( path , false , true , embeddingProvider . TokenizerPath , token ))
2026-08-10 18:21:31 +02:00
{
2026-08-10 21:36:01 +02:00
if ( segment . TokenCount is null )
throw new InvalidOperationException ( $"Rust did not return a token count for an extracted segment from '{path}' using provider '{embeddingProvider.Name}'." );
2026-08-10 18:21:31 +02:00
2026-08-10 21:36:01 +02:00
yield return new ( segment . Content , segment . TokenCount . Value );
2026-08-10 18:21:31 +02:00
}
}
private async IAsyncEnumerable <( string Content , int? TokenCount )> StreamArbitraryFileDataCore (
string path ,
bool extractImages ,
bool includeTokenCount ,
2026-08-10 21:36:01 +02:00
string tokenizerPath ,
2026-08-10 18:21:31 +02:00
[EnumeratorCancellation] CancellationToken token )
2026-05-08 17:37:34 +02:00
{
var streamId = Guid . NewGuid (). ToString ();
2026-08-10 21:36:01 +02:00
var requestUri = $"/retrieval/fs/extract?path={Uri.EscapeDataString(path)}&stream_id={streamId}&extract_images={extractImages}&include_token_count={includeTokenCount}&tokenizer_path={Uri.EscapeDataString(tokenizerPath)}" ;
2026-05-08 17:37:34 +02:00
using var request = new HttpRequestMessage ( HttpMethod . Get , requestUri );
using var response = await this . http . SendAsync ( request , HttpCompletionOption . ResponseHeadersRead , token );
if (! response . IsSuccessStatusCode )
2026-08-10 18:21:31 +02:00
{
var responseBody = await response . Content . ReadAsStringAsync ( token );
this . logger ?. LogError (
"Failed to stream arbitrary file data from Rust runtime. Status: {StatusCode}, reason: '{ReasonPhrase}', path: '{Path}', body: '{Body}'" ,
response . StatusCode ,
response . ReasonPhrase ,
path ,
responseBody );
if ( includeTokenCount )
throw new InvalidOperationException ( $"Rust could not extract and count '{path}'. HTTP {(int)response.StatusCode} ({response.ReasonPhrase}): {responseBody}" );
2026-05-08 17:37:34 +02:00
yield break ;
2026-08-10 18:21:31 +02:00
}
2026-05-08 17:37:34 +02:00
string? finalContentChunk = null ;
try
{
await using var stream = await response . Content . ReadAsStreamAsync ( token );
using var reader = new StreamReader ( stream );
while (! reader . EndOfStream && ! token . IsCancellationRequested )
{
var line = await reader . ReadLineAsync ( token );
if ( string . IsNullOrWhiteSpace ( line ))
continue ;
if (! line . StartsWith ( "data:" , StringComparison . InvariantCulture ))
continue ;
var jsonContent = line [ 5. .];
ContentStreamSseEvent ? sseEvent = null ;
try
{
sseEvent = JsonSerializer . Deserialize < ContentStreamSseEvent >( jsonContent );
}
catch ( JsonException )
{
if ( this . TryLogSseErrorMessage ( jsonContent , path ))
2026-08-10 18:21:31 +02:00
{
if ( includeTokenCount )
throw new InvalidOperationException ( $"Rust could not extract and count a segment from '{path}'. See the runtime log for details." );
2026-05-08 17:37:34 +02:00
continue ;
2026-08-10 18:21:31 +02:00
}
2026-05-08 17:37:34 +02:00
this . logger ?. LogError ( "Failed to deserialize SSE event: {JsonContent}" , jsonContent );
}
if ( sseEvent is null )
continue ;
var content = ContentStreamSseHandler . ProcessEvent ( sseEvent , extractImages );
if (! string . IsNullOrWhiteSpace ( content ))
2026-08-10 18:21:31 +02:00
yield return ( content , sseEvent . TokenCount );
2026-05-08 17:37:34 +02:00
}
}
finally
{
finalContentChunk = ContentStreamSseHandler . Clear ( streamId );
}
if (! string . IsNullOrWhiteSpace ( finalContentChunk ))
2026-08-10 18:21:31 +02:00
yield return ( finalContentChunk , null );
2026-05-08 17:37:34 +02:00
}
private bool TryLogSseErrorMessage ( string jsonContent , string path )
{
try
{
var errorMessage = JsonSerializer . Deserialize < string >( jsonContent );
if ( string . IsNullOrWhiteSpace ( errorMessage ))
return false ;
this . logger ?. LogError ( "Rust retrieval stream error for '{Path}': {ErrorMessage}" , path , errorMessage );
return true ;
}
catch ( JsonException )
{
return false ;
}
}
}