Refactor image conversion to return success status and base64 content

This commit is contained in:
Thorsten Sommer 2025-12-29 17:31:28 +01:00
parent 839ba3aaf0
commit 011b3a8d04
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
6 changed files with 31 additions and 17 deletions

View File

@ -159,7 +159,9 @@ public sealed class AgentDataSourceSelection (ILogger<AgentDataSourceSelection>
ContentText text => text.Text, ContentText text => text.Text,
// Image prompts may be empty, e.g., when the image is too large: // Image prompts may be empty, e.g., when the image is too large:
ContentImage image => await image.AsBase64(token), ContentImage image => await image.TryAsBase64(token) is (success: true, { } base64Image)
? base64Image
: string.Empty,
// Other content types are not supported yet: // Other content types are not supported yet:
_ => string.Empty, _ => string.Empty,

View File

@ -219,7 +219,9 @@ public sealed class AgentRetrievalContextValidation (ILogger<AgentRetrievalConte
ContentText text => text.Text, ContentText text => text.Text,
// Image prompts may be empty, e.g., when the image is too large: // Image prompts may be empty, e.g., when the image is too large:
ContentImage image => await image.AsBase64(token), ContentImage image => await image.TryAsBase64(token) is (success: true, { } base64Image)
? base64Image
: string.Empty,
// Other content types are not supported yet: // Other content types are not supported yet:
_ => string.Empty, _ => string.Empty,

View File

@ -238,7 +238,7 @@ public sealed record ChatThread
{ {
var (contentData, contentType) = block.Content switch var (contentData, contentType) = block.Content switch
{ {
ContentImage image => (await image.AsBase64(token), Tools.ERIClient.DataModel.ContentType.IMAGE), ContentImage image => (await image.TryAsBase64(token) is (success: true, { } base64Image) ? base64Image : string.Empty, Tools.ERIClient.DataModel.ContentType.IMAGE),
ContentText text => (text.Text, Tools.ERIClient.DataModel.ContentType.TEXT), ContentText text => (text.Text, Tools.ERIClient.DataModel.ContentType.TEXT),
_ => (string.Empty, Tools.ERIClient.DataModel.ContentType.UNKNOWN), _ => (string.Empty, Tools.ERIClient.DataModel.ContentType.UNKNOWN),

View File

@ -12,21 +12,25 @@ public static class IImageSourceExtensions
/// <remarks> /// <remarks>
/// The images are directly converted to base64 strings. The maximum /// The images are directly converted to base64 strings. The maximum
/// size of the image is around 10 MB. If the image is larger, the method /// size of the image is around 10 MB. If the image is larger, the method
/// returns an empty string. /// returns an empty string.<br/>
/// /// <br/>
/// As of now, this method does no sort of image processing. LLMs usually /// As of now, this method does no sort of image processing. LLMs usually
/// do not work with arbitrary image sizes. In the future, we might have /// do not work with arbitrary image sizes. In the future, we might have
/// to resize the images before sending them to the model. /// to resize the images before sending them to the model.<br/>
/// <br/>
/// Note as well that this method returns just the base64 string without
/// any data URI prefix (like "data:image/png;base64,"). The caller has
/// to take care of that if needed.
/// </remarks> /// </remarks>
/// <param name="image">The image source.</param> /// <param name="image">The image source.</param>
/// <param name="token">The cancellation token.</param> /// <param name="token">The cancellation token.</param>
/// <returns>The image content as a base64 string; might be empty.</returns> /// <returns>The image content as a base64 string; might be empty.</returns>
public static async Task<string> AsBase64(this IImageSource image, CancellationToken token = default) public static async Task<(bool success, string base64Content)> TryAsBase64(this IImageSource image, CancellationToken token = default)
{ {
switch (image.SourceType) switch (image.SourceType)
{ {
case ContentImageSource.BASE64: case ContentImageSource.BASE64:
return image.Source; return (success: true, image.Source);
case ContentImageSource.URL: case ContentImageSource.URL:
{ {
@ -39,14 +43,15 @@ public static class IImageSourceExtensions
if(lengthBytes > 10_000_000) if(lengthBytes > 10_000_000)
{ {
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The image at the URL is too large (>10 MB). Skipping the image."))); await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The image at the URL is too large (>10 MB). Skipping the image.")));
return string.Empty; return (success: false, string.Empty);
} }
var bytes = await response.Content.ReadAsByteArrayAsync(token); var bytes = await response.Content.ReadAsByteArrayAsync(token);
return Convert.ToBase64String(bytes); return (success: true, Convert.ToBase64String(bytes));
} }
return string.Empty; await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("Failed to download the image from the URL. Skipping the image.")));
return (success: false, string.Empty);
} }
case ContentImageSource.LOCAL_PATH: case ContentImageSource.LOCAL_PATH:
@ -57,17 +62,18 @@ public static class IImageSourceExtensions
if(length > 10_000_000) if(length > 10_000_000)
{ {
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The local image file is too large (>10 MB). Skipping the image."))); await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The local image file is too large (>10 MB). Skipping the image.")));
return string.Empty; return (success: false, string.Empty);
} }
var bytes = await File.ReadAllBytesAsync(image.Source, token); var bytes = await File.ReadAllBytesAsync(image.Source, token);
return Convert.ToBase64String(bytes); return (success: true, Convert.ToBase64String(bytes));
} }
return string.Empty; await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The local image file does not exist. Skipping the image.")));
return (success: false, string.Empty);
default: default:
return string.Empty; return (success: false, string.Empty);
} }
} }
} }

View File

@ -74,7 +74,9 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
LatestUserPrompt = lastUserPrompt switch LatestUserPrompt = lastUserPrompt switch
{ {
ContentText text => text.Text, ContentText text => text.Text,
ContentImage image => await image.AsBase64(token), ContentImage image => await image.TryAsBase64(token) is (success: true, { } base64Image)
? base64Image
: string.Empty,
_ => string.Empty _ => string.Empty
}, },

View File

@ -81,7 +81,9 @@ public static class IRetrievalContextExtensions
sb.AppendLine(); sb.AppendLine();
sb.AppendLine("Matched image content as base64-encoded data:"); sb.AppendLine("Matched image content as base64-encoded data:");
sb.AppendLine("````"); sb.AppendLine("````");
sb.AppendLine(await imageContext.AsBase64(token)); sb.AppendLine(await imageContext.TryAsBase64(token) is (success: true, { } base64Image)
? base64Image
: string.Empty);
sb.AppendLine("````"); sb.AppendLine("````");
break; break;