Skip to content

Commit e9485ca

Browse files
Added support for images (#609)
1 parent ed4c7d2 commit e9485ca

File tree

80 files changed

+1297
-492
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+1297
-492
lines changed

app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,9 @@ public async Task<List<SelectedDataSource>> PerformSelectionAsync(IProvider prov
159159
ContentText text => text.Text,
160160

161161
// Image prompts may be empty, e.g., when the image is too large:
162-
ContentImage image => await image.AsBase64(token),
162+
ContentImage image => await image.TryAsBase64(token) is (success: true, { } base64Image)
163+
? base64Image
164+
: string.Empty,
163165

164166
// Other content types are not supported yet:
165167
_ => string.Empty,

app/MindWork AI Studio/Agents/AgentRetrievalContextValidation.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ public async Task<RetrievalContextValidationResult> ValidateRetrievalContextAsyn
219219
ContentText text => text.Text,
220220

221221
// Image prompts may be empty, e.g., when the image is too large:
222-
ContentImage image => await image.AsBase64(token),
222+
ContentImage image => await image.TryAsBase64(token) is (success: true, { } base64Image)
223+
? base64Image
224+
: string.Empty,
223225

224226
// Other content types are not supported yet:
225227
_ => string.Empty,

app/MindWork AI Studio/Assistants/AssistantBase.razor.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,13 @@ protected Guid CreateChatThread(Guid workspaceId, string name)
217217
return chatId;
218218
}
219219

220-
protected DateTimeOffset AddUserRequest(string request, bool hideContentFromUser = false)
220+
protected DateTimeOffset AddUserRequest(string request, bool hideContentFromUser = false, params List<FileAttachment> attachments)
221221
{
222222
var time = DateTimeOffset.Now;
223223
this.lastUserPrompt = new ContentText
224224
{
225225
Text = request,
226+
FileAttachments = attachments,
226227
};
227228

228229
this.chatThread!.Blocks.Add(new ContentBlock

app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ else
103103
@T("Documents for the analysis")
104104
</MudText>
105105

106-
<AttachDocuments Name="Document Analysis Files" @bind-DocumentPaths="@this.loadedDocumentPaths" CatchAllDocuments="true" UseSmallForm="false"/>
106+
<AttachDocuments Name="Document Analysis Files" @bind-DocumentPaths="@this.loadedDocumentPaths" CatchAllDocuments="true" UseSmallForm="false" Provider="@this.providerSettings"/>
107107

108108
</ExpansionPanel>
109109
</MudExpansionPanels>

app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs

Lines changed: 85 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Text;
2+
13
using AIStudio.Chat;
24
using AIStudio.Dialogs;
35
using AIStudio.Dialogs.Settings;
@@ -34,19 +36,21 @@ You are a policy‑bound analysis agent. Follow these instructions exactly.
3436
3537
DOCUMENTS: the only content you may analyze.
3638
39+
Maybe, there are image files attached. IMAGES may contain important information. Use them as part of your analysis.
40+
3741
{this.GetDocumentTaskDescription()}
3842
3943
# Scope and precedence
4044
41-
Use only information explicitly contained in DOCUMENTS and/or POLICY_*.
45+
Use only information explicitly contained in DOCUMENTS, IMAGES, and/or POLICY_*.
4246
You may paraphrase but must not add facts, assumptions, or outside knowledge.
4347
Content decisions are governed by POLICY_ANALYSIS_RULES; formatting is governed by POLICY_OUTPUT_RULES.
4448
If there is a conflict between DOCUMENTS and POLICY_*, follow POLICY_ANALYSIS_RULES for analysis and POLICY_OUTPUT_RULES for formatting. Do not invent reconciliations.
4549
4650
# Process
4751
4852
1) Read POLICY_ANALYSIS_RULES and POLICY_OUTPUT_RULES end to end.
49-
2) Extract only the information from DOCUMENTS that POLICY_ANALYSIS_RULES permits.
53+
2) Extract only the information from DOCUMENTS and IMAGES that POLICY_ANALYSIS_RULES permits.
5054
3) Perform the analysis strictly according to POLICY_ANALYSIS_RULES.
5155
4) Produce the final answer strictly according to POLICY_OUTPUT_RULES.
5256
@@ -74,16 +78,33 @@ Do not quote or summarize POLICY_* unless required by POLICY_OUTPUT_RULES.
7478
# Self‑check before sending
7579
7680
Verify the answer matches POLICY_OUTPUT_RULES exactly.
77-
Verify every statement is attributable to DOCUMENTS or POLICY_*.
81+
Verify every statement is attributable to DOCUMENTS, IMAGES, or POLICY_*.
7882
Remove any text not required by POLICY_OUTPUT_RULES.
7983
8084
{this.PromptGetActivePolicy()}
8185
""";
8286

83-
private string GetDocumentTaskDescription() =>
84-
this.loadedDocumentPaths.Count > 1
85-
? $"Your task is to analyze {this.loadedDocumentPaths.Count} DOCUMENTS. Different DOCUMENTS are divided by a horizontal rule in markdown formatting followed by the name of the document."
86-
: "Your task is to analyze a single document.";
87+
private string GetDocumentTaskDescription()
88+
{
89+
var numDocuments = this.loadedDocumentPaths.Count(x => x is { Exists: true, IsImage: false });
90+
var numImages = this.loadedDocumentPaths.Count(x => x is { Exists: true, IsImage: true });
91+
92+
return (numDocuments, numImages) switch
93+
{
94+
(0, 1) => "Your task is to analyze a single image file attached as a document.",
95+
(0, > 1) => $"Your task is to analyze {numImages} image file(s) attached as documents.",
96+
97+
(1, 0) => "Your task is to analyze a single DOCUMENT.",
98+
(1, 1) => "Your task is to analyze a single DOCUMENT and 1 image file attached as a document.",
99+
(1, > 1) => $"Your task is to analyze a single DOCUMENT and {numImages} image file(s) attached as documents.",
100+
101+
(> 0, 0) => $"Your task is to analyze {numDocuments} DOCUMENTS. Different DOCUMENTS are divided by a horizontal rule in markdown formatting followed by the name of the document.",
102+
(> 0, 1) => $"Your task is to analyze {numDocuments} DOCUMENTS and 1 image file attached as a document. Different DOCUMENTS are divided by a horizontal rule in Markdown formatting followed by the name of the document.",
103+
(> 0, > 0) => $"Your task is to analyze {numDocuments} DOCUMENTS and {numImages} image file(s) attached as documents. Different DOCUMENTS are divided by a horizontal rule in Markdown formatting followed by the name of the document.",
104+
105+
_ => "Your task is to analyze a single DOCUMENT."
106+
};
107+
}
87108

88109
protected override IReadOnlyList<IButtonData> FooterButtons => [];
89110

@@ -327,37 +348,68 @@ private async Task<string> PromptLoadDocumentsContent()
327348
if (this.loadedDocumentPaths.Count == 0)
328349
return string.Empty;
329350

330-
var documentSections = new List<string>();
331-
var count = 1;
351+
var documents = this.loadedDocumentPaths.Where(n => n is { Exists: true, IsImage: false }).ToList();
352+
var sb = new StringBuilder();
353+
354+
if (documents.Count > 0)
355+
{
356+
sb.AppendLine("""
357+
# DOCUMENTS:
358+
359+
""");
360+
}
332361

333-
foreach (var fileAttachment in this.loadedDocumentPaths)
362+
var numDocuments = 1;
363+
foreach (var document in documents)
334364
{
335-
if (fileAttachment.IsForbidden)
365+
if (document.IsForbidden)
336366
{
337-
this.Logger.LogWarning($"Skipping forbidden file: '{fileAttachment.FilePath}'.");
367+
this.Logger.LogWarning($"Skipping forbidden file: '{document.FilePath}'.");
338368
continue;
339369
}
340-
341-
var fileContent = await this.RustService.ReadArbitraryFileData(fileAttachment.FilePath, int.MaxValue);
342-
343-
documentSections.Add($"""
344-
## DOCUMENT {count}:
345-
File path: {fileAttachment.FilePath}
346-
Content:
347-
```
348-
{fileContent}
349-
```
350-
351-
---
352-
""");
353-
count++;
370+
371+
var fileContent = await this.RustService.ReadArbitraryFileData(document.FilePath, int.MaxValue);
372+
sb.AppendLine($"""
373+
374+
## DOCUMENT {numDocuments}:
375+
File path: {document.FilePath}
376+
Content:
377+
```
378+
{fileContent}
379+
```
380+
381+
---
382+
383+
""");
384+
numDocuments++;
354385
}
355386

356-
return $"""
357-
# DOCUMENTS:
387+
var numImages = this.loadedDocumentPaths.Count(x => x is { IsImage: true, Exists: true });
388+
if (numImages > 0)
389+
{
390+
if (documents.Count == 0)
391+
{
392+
sb.AppendLine($"""
393+
394+
There are {numImages} image file(s) attached as documents.
395+
Please consider them as documents as well and use them to
396+
answer accordingly.
397+
398+
""");
399+
}
400+
else
401+
{
402+
sb.AppendLine($"""
403+
404+
Additionally, there are {numImages} image file(s) attached.
405+
Please consider them as documents as well and use them to
406+
answer accordingly.
407+
408+
""");
409+
}
410+
}
358411

359-
{string.Join("\n", documentSections)}
360-
""";
412+
return sb.ToString();
361413
}
362414

363415
private async Task Analyze()
@@ -370,7 +422,9 @@ private async Task Analyze()
370422
this.CreateChatThread();
371423

372424
var userRequest = this.AddUserRequest(
373-
$"{await this.PromptLoadDocumentsContent()}", hideContentFromUser:true);
425+
await this.PromptLoadDocumentsContent(),
426+
hideContentFromUser: true,
427+
this.loadedDocumentPaths.Where(n => n is { Exists: true, IsImage: true }).ToList());
374428

375429
await this.AddAIResponseAsync(userRequest);
376430
}

app/MindWork AI Studio/Assistants/I18N/allTexts.lua

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,6 +1513,12 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4188329028"] = "No, kee
15131513
-- Export Chat to Microsoft Word
15141514
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T861873672"] = "Export Chat to Microsoft Word"
15151515

1516+
-- The local image file does not exist. Skipping the image.
1517+
UI_TEXT_CONTENT["AISTUDIO::CHAT::IIMAGESOURCEEXTENSIONS::T255679918"] = "The local image file does not exist. Skipping the image."
1518+
1519+
-- Failed to download the image from the URL. Skipping the image.
1520+
UI_TEXT_CONTENT["AISTUDIO::CHAT::IIMAGESOURCEEXTENSIONS::T2996654916"] = "Failed to download the image from the URL. Skipping the image."
1521+
15161522
-- The local image file is too large (>10 MB). Skipping the image.
15171523
UI_TEXT_CONTENT["AISTUDIO::CHAT::IIMAGESOURCEEXTENSIONS::T3219823625"] = "The local image file is too large (>10 MB). Skipping the image."
15181524

@@ -2968,6 +2974,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTCHECKDIALOG::T1373123357"] = "Markdo
29682974
-- Load file
29692975
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTCHECKDIALOG::T2129302565"] = "Load file"
29702976

2977+
-- Image View
2978+
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTCHECKDIALOG::T2199753423"] = "Image View"
2979+
29712980
-- See how we load your file. Review the content before we process it further.
29722981
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTCHECKDIALOG::T3271853346"] = "See how we load your file. Review the content before we process it further."
29732982

@@ -2986,6 +2995,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTCHECKDIALOG::T652739927"] = "This is
29862995
-- File Path
29872996
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTCHECKDIALOG::T729508546"] = "File Path"
29882997

2998+
-- The specified file could not be found. The file have been moved, deleted, renamed, or is otherwise inaccessible.
2999+
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTCHECKDIALOG::T973777830"] = "The specified file could not be found. The file have been moved, deleted, renamed, or is otherwise inaccessible."
3000+
29893001
-- Embedding Name
29903002
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGMETHODDIALOG::T1427271797"] = "Embedding Name"
29913003

@@ -3436,12 +3448,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::REVIEWATTACHMENTSDIALOG::T1746160064"] = "He
34363448
-- There aren't any file attachments available right now.
34373449
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::REVIEWATTACHMENTSDIALOG::T2111340711"] = "There aren't any file attachments available right now."
34383450

3451+
-- Document Preview
3452+
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::REVIEWATTACHMENTSDIALOG::T285154968"] = "Document Preview"
3453+
34393454
-- The file was deleted, renamed, or moved.
34403455
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::REVIEWATTACHMENTSDIALOG::T3083729256"] = "The file was deleted, renamed, or moved."
34413456

34423457
-- Your attached file.
34433458
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::REVIEWATTACHMENTSDIALOG::T3154198222"] = "Your attached file."
34443459

3460+
-- Preview what we send to the AI.
3461+
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::REVIEWATTACHMENTSDIALOG::T3160778981"] = "Preview what we send to the AI."
3462+
3463+
-- Close
3464+
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::REVIEWATTACHMENTSDIALOG::T3448155331"] = "Close"
3465+
34453466
-- Your attached files
34463467
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::REVIEWATTACHMENTSDIALOG::T3909191077"] = "Your attached files"
34473468

@@ -6058,9 +6079,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T29289275
60586079
-- Images are not supported yet
60596080
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T298062956"] = "Images are not supported yet"
60606081

6082+
-- Images are not supported at this place
6083+
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T305247150"] = "Images are not supported at this place"
6084+
60616085
-- Executables are not allowed
60626086
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T4167762413"] = "Executables are not allowed"
60636087

6088+
-- Images are not supported by the selected provider and model
6089+
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T999194030"] = "Images are not supported by the selected provider and model"
6090+
60646091
-- The hostname is not a valid HTTP(S) URL.
60656092
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1013354736"] = "The hostname is not a valid HTTP(S) URL."
60666093

app/MindWork AI Studio/Chat/ChatThread.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ public void Remove(IContent content, bool removeForRegenerate = false)
238238
{
239239
var (contentData, contentType) = block.Content switch
240240
{
241-
ContentImage image => (await image.AsBase64(token), Tools.ERIClient.DataModel.ContentType.IMAGE),
241+
ContentImage image => (await image.TryAsBase64(token) is (success: true, { } base64Image) ? base64Image : string.Empty, Tools.ERIClient.DataModel.ContentType.IMAGE),
242242
ContentText text => (text.Text, Tools.ERIClient.DataModel.ContentType.TEXT),
243243

244244
_ => (string.Empty, Tools.ERIClient.DataModel.ContentType.UNKNOWN),

app/MindWork AI Studio/Chat/ContentImage.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatMo
4747
InitialRemoteWait = this.InitialRemoteWait,
4848
IsStreaming = this.IsStreaming,
4949
SourceType = this.SourceType,
50+
Sources = [..this.Sources],
51+
FileAttachments = [..this.FileAttachments],
5052
};
5153

5254
#endregion

app/MindWork AI Studio/Chat/ContentText.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,14 @@ public async Task<string> PrepareTextContentForAI()
195195
sb.AppendLine(await Program.RUST_SERVICE.ReadArbitraryFileData(document.FilePath, int.MaxValue));
196196
sb.AppendLine("````");
197197
}
198+
199+
var numImages = this.FileAttachments.Count(x => x is { IsImage: true, Exists: true });
200+
if (numImages > 0)
201+
{
202+
sb.AppendLine();
203+
sb.AppendLine($"Additionally, there are {numImages} image file(s) attached to this message. ");
204+
sb.AppendLine("Please consider them as part of the message content and use them to answer accordingly.");
205+
}
198206
}
199207
}
200208
}

app/MindWork AI Studio/Chat/FileAttachment.cs

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Text.Json.Serialization;
2+
13
using AIStudio.Tools.Rust;
24

35
namespace AIStudio.Chat;
@@ -9,22 +11,47 @@ namespace AIStudio.Chat;
911
/// <param name="FileName">The name of the file, including extension.</param>
1012
/// <param name="FilePath">The full path to the file, including the filename and extension.</param>
1113
/// <param name="FileSizeBytes">The size of the file in bytes.</param>
12-
public readonly record struct FileAttachment(FileAttachmentType Type, string FileName, string FilePath, long FileSizeBytes)
14+
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
15+
[JsonDerivedType(typeof(FileAttachment), typeDiscriminator: "file")]
16+
[JsonDerivedType(typeof(FileAttachmentImage), typeDiscriminator: "image")]
17+
public record FileAttachment(FileAttachmentType Type, string FileName, string FilePath, long FileSizeBytes)
1318
{
1419
/// <summary>
15-
/// Gets a value indicating whether the file still exists on the file system.
20+
/// Gets a value indicating whether the file type is forbidden and should not be attached.
1621
/// </summary>
17-
public bool Exists => File.Exists(this.FilePath);
22+
/// <remarks>
23+
/// The state is determined once during construction and does not change.
24+
/// </remarks>
25+
public bool IsForbidden { get; } = Type == FileAttachmentType.FORBIDDEN;
1826

1927
/// <summary>
20-
/// Gets a value indicating whether the file type is forbidden and should not be attached.
28+
/// Gets a value indicating whether the file type is valid and allowed to be attached.
2129
/// </summary>
22-
public bool IsForbidden => this.Type == FileAttachmentType.FORBIDDEN;
30+
/// <remarks>
31+
/// The state is determined once during construction and does not change.
32+
/// </remarks>
33+
public bool IsValid { get; } = Type != FileAttachmentType.FORBIDDEN;
2334

2435
/// <summary>
25-
/// Gets a value indicating whether the file type is valid and allowed to be attached.
36+
/// Gets a value indicating whether the file type is an image.
2637
/// </summary>
27-
public bool IsValid => this.Type != FileAttachmentType.FORBIDDEN;
38+
/// <remarks>
39+
/// The state is determined once during construction and does not change.
40+
/// </remarks>
41+
public bool IsImage { get; } = Type == FileAttachmentType.IMAGE;
42+
43+
/// <summary>
44+
/// Gets the file path for loading the file from the web browser-side (Blazor).
45+
/// </summary>
46+
public string FilePathAsUrl { get; } = FileHandler.CreateFileUrl(FilePath);
47+
48+
/// <summary>
49+
/// Gets a value indicating whether the file still exists on the file system.
50+
/// </summary>
51+
/// <remarks>
52+
/// This property checks the file system each time it is accessed.
53+
/// </remarks>
54+
public bool Exists => File.Exists(this.FilePath);
2855

2956
/// <summary>
3057
/// Creates a FileAttachment from a file path by automatically determining the type,
@@ -38,7 +65,13 @@ public static FileAttachment FromPath(string filePath)
3865
var fileSize = File.Exists(filePath) ? new FileInfo(filePath).Length : 0;
3966
var type = DetermineFileType(filePath);
4067

41-
return new FileAttachment(type, fileName, filePath, fileSize);
68+
return type switch
69+
{
70+
FileAttachmentType.DOCUMENT => new FileAttachment(type, fileName, filePath, fileSize),
71+
FileAttachmentType.IMAGE => new FileAttachmentImage(fileName, filePath, fileSize),
72+
73+
_ => new FileAttachment(type, fileName, filePath, fileSize),
74+
};
4275
}
4376

4477
/// <summary>

0 commit comments

Comments
 (0)