2025-06-30 16:56:48 +00:00
using System.Text ;
using System.Text.Json ;
2026-05-08 15:37:34 +00:00
using System.Runtime.CompilerServices ;
2025-06-30 16:56:48 +00:00
2026-08-10 19:36:01 +00:00
using AIStudio.Settings ;
2026-08-10 16:21:31 +00:00
using AIStudio.Tools.Rust ;
2025-05-02 21:09:50 +00:00
namespace AIStudio.Tools.Services ;
2026-08-10 16:21:31 +00:00
public sealed record ArbitraryFileDataSegment ( string Content , int TokenCount ) ;
2025-05-02 21:09:50 +00:00
public sealed partial class RustService
{
2025-06-30 19:51:02 +00:00
public async Task < string > ReadArbitraryFileData ( string path , int maxChunks , bool extractImages = false )
2025-05-02 21:09:50 +00:00
{
2025-06-30 19:51:02 +00:00
var streamId = Guid . NewGuid ( ) . ToString ( ) ;
2026-08-10 16:21:31 +00:00
var requestUri = $"/retrieval/fs/extract?path={Uri.EscapeDataString(path)}&stream_id={streamId}&extract_images={extractImages}&include_token_count=false" ;
2025-06-30 16:56:48 +00:00
var request = new HttpRequestMessage ( HttpMethod . Get , requestUri ) ;
var response = await this . http . SendAsync ( request , HttpCompletionOption . ResponseHeadersRead ) ;
2025-05-02 21:09:50 +00:00
if ( ! response . IsSuccessStatusCode )
2026-05-12 18:31:08 +00: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 21:09:50 +00:00
return string . Empty ;
2026-05-12 18:31:08 +00:00
}
2025-05-02 21:09:50 +00:00
2025-06-30 16:56:48 +00:00
var resultBuilder = new StringBuilder ( ) ;
2025-07-01 16:34:14 +00:00
try
2025-06-30 16:56:48 +00:00
{
2025-07-01 16:34:14 +00: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 16:56:48 +00:00
{
2025-07-01 16:34:14 +00: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 16:56:48 +00:00
{
2025-07-01 16:34:14 +00: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 15:37:34 +00:00
if ( this . TryLogSseErrorMessage ( jsonContent , path ) )
continue ;
2025-07-01 16:34:14 +00:00
this . logger ? . LogError ( "Failed to deserialize SSE event: {JsonContent}" , jsonContent ) ;
2025-06-30 16:56:48 +00:00
}
}
2025-07-01 16:34:14 +00: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 16:56:48 +00:00
}
return resultBuilder . ToString ( ) ;
2025-05-02 21:09:50 +00:00
}
2026-05-08 15:37:34 +00:00
public async IAsyncEnumerable < string > StreamArbitraryFileData ( string path , bool extractImages = false , [ EnumeratorCancellation ] CancellationToken token = default )
2026-08-10 16:21:31 +00:00
{
2026-08-10 19:36:01 +00:00
await foreach ( var segment in this . StreamArbitraryFileDataCore ( path , extractImages , false , string . Empty , token ) )
2026-08-10 16:21:31 +00:00
yield return segment . Content ;
}
public async IAsyncEnumerable < ArbitraryFileDataSegment > StreamArbitraryFileDataWithTokenCounts (
string path ,
2026-08-10 19:36:01 +00:00
EmbeddingProvider embeddingProvider ,
2026-08-10 16:21:31 +00:00
[EnumeratorCancellation] CancellationToken token = default )
{
2026-08-10 19:36:01 +00:00
await foreach ( var segment in this . StreamArbitraryFileDataCore ( path , false , true , embeddingProvider . TokenizerPath , token ) )
2026-08-10 16:21:31 +00:00
{
2026-08-10 19:36:01 +00: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 16:21:31 +00:00
2026-08-10 19:36:01 +00:00
yield return new ( segment . Content , segment . TokenCount . Value ) ;
2026-08-10 16:21:31 +00:00
}
}
private async IAsyncEnumerable < ( string Content , int? TokenCount ) > StreamArbitraryFileDataCore (
string path ,
bool extractImages ,
bool includeTokenCount ,
2026-08-10 19:36:01 +00:00
string tokenizerPath ,
2026-08-10 16:21:31 +00:00
[EnumeratorCancellation] CancellationToken token )
2026-05-08 15:37:34 +00:00
{
var streamId = Guid . NewGuid ( ) . ToString ( ) ;
2026-08-10 19:36:01 +00: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 15:37:34 +00: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 16:21:31 +00: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 15:37:34 +00:00
yield break ;
2026-08-10 16:21:31 +00:00
}
2026-05-08 15:37:34 +00: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 16:21:31 +00: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 15:37:34 +00:00
continue ;
2026-08-10 16:21:31 +00:00
}
2026-05-08 15:37:34 +00: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 16:21:31 +00:00
yield return ( content , sseEvent . TokenCount ) ;
2026-05-08 15:37:34 +00:00
}
}
finally
{
finalContentChunk = ContentStreamSseHandler . Clear ( streamId ) ;
}
if ( ! string . IsNullOrWhiteSpace ( finalContentChunk ) )
2026-08-10 16:21:31 +00:00
yield return ( finalContentChunk , null ) ;
2026-05-08 15:37:34 +00: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 ;
}
}
}