mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-23 16:35:00 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71ae288020 | ||
|
|
a654594aea | ||
|
|
be26ccbaae | ||
|
|
3e4596786a |
No files matched your search
@@ -5,7 +5,6 @@ startup.env
|
||||
libpdfium.dylib
|
||||
libpdfium.so
|
||||
libpdfium.dll
|
||||
pdfium.dll
|
||||
|
||||
# Ignore qdrant database:
|
||||
qdrant-aarch64-apple-darwin
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ knowledge.
|
||||
RAG is available as a beta preview feature. Architecture:
|
||||
- **External Retrieval Interface (ERI)** - Contract for integrating external data sources
|
||||
- **Data Sources** - Local files and external data via ERI servers
|
||||
- **Two ways to search** - By default, the chat model searches the data sources itself through the tool `semantic_search`, whenever a question calls for it. The classic process (`AISrcSelWithRetCtxVal`) searches them with every message instead, when the user chose so per chat (`DataSourceOptions.RetrievalMode`) or whenever the tool cannot be offered. `ToolRegistry.GetEffectiveRetrievalModeAsync` decides between the two; pass its answer to `DataSourceService`, because the agents only count as providers that see the data when they actually run. Both ways decide where a chat may still send data once their passages reached it through `DataSourceRequirements.Of`, never with a check of their own: a way which demands less leaves the other one open. See "Searching Data Sources" in `documentation/Tools.md`.
|
||||
- **Two ways to search** - By default, the chat model searches the data sources itself through the tool `semantic_search`, whenever a question calls for it. The classic process (`AISrcSelWithRetCtxVal`) searches them with every message instead, when the user chose so per chat (`DataSourceOptions.RetrievalMode`) or whenever the tool cannot be offered. `ToolRegistry.GetEffectiveRetrievalModeAsync` decides between the two; pass its answer to `DataSourceService`, because the agents only count as providers that see the data when they actually run. See "Searching Data Sources" in `documentation/Tools.md`.
|
||||
- **Agents** - AI agents select data sources and validate retrieval quality, in the classic process only
|
||||
- **Embedding providers** - Support for various embedding models
|
||||
- **Vector database** - Qdrant Edge, embedded in the Rust runtime; see "Databases" below
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@using AIStudio.Settings.DataModel
|
||||
@using AIStudio.Tools.Rust
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-3">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">
|
||||
@T("Input")
|
||||
</MudText>
|
||||
|
||||
@@ -18,37 +18,40 @@
|
||||
</MudJustifiedText>
|
||||
}
|
||||
|
||||
<MudTextField T="string" @bind-Text="@this.filePatterns" Validation="@this.ValidateFilePatterns" Immediate="@true" Disabled="@this.isProcessingBatch" Label="@T("File patterns")" HelperText="@T("Which files should be processed? Separate multiple patterns with a semicolon, e.g., *.pdf;*.docx. Use :documents: for every document and readable text file, :text: for readable text files only, or * for every file.")" AdornmentIcon="@Icons.Material.Filled.FilterAlt" Adornment="Adornment.Start" Variant="Variant.Outlined" Margin="Margin.Normal" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
<RestoreDefaultButton Text="@T("Restore default patterns")" Disabled="@this.isProcessingBatch" OnClick="@this.RestoreDefaultFilePatterns" Class="mb-1"/>
|
||||
<div class="mt-6">
|
||||
<FilePatternSelection>
|
||||
<AllowedContent>
|
||||
<MudTextField T="string" @bind-Text="@this.filePatterns" Validation="@this.ValidateFilePatterns" Immediate="@true" Disabled="@this.isProcessingBatch" Label="@T("Files to include")" AdornmentIcon="@Icons.Material.Filled.Restore" Adornment="Adornment.End" OnAdornmentClick="@this.RestoreDefaultFilePatterns" AdornmentAriaLabel="@T("Restore default patterns")" Lines="1" AutoGrow="@true" MaxLines="0" Class="pattern-text-field" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
</AllowedContent>
|
||||
</FilePatternSelection>
|
||||
</div>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
|
||||
@T("Supported audio and video files are transcribed automatically without an additional dialog. Each transcript is stored next to its media file as '<media-file>.transcript.md' and reused when an interrupted run is continued.")
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
|
||||
@T("Images in JPG, PNG, WebP, GIF, BMP, or TIFF format are given to the model as images, so you need a model that understands images. If the selected model cannot read images, remove the image patterns, such as *.jpg or *.png, before you start.")
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudStack Class="mb-3">
|
||||
<MudButton OnClick="@this.ToggleExpertSettings" Variant="Variant.Text" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Tune">
|
||||
@(this.showExpertSettings ? T("Hide Expert Settings") : T("Show Expert Settings"))
|
||||
</MudButton>
|
||||
<MudDivider/>
|
||||
<MudCollapse Expanded="@this.showExpertSettings" Class="@this.GetExpertStyles">
|
||||
<MudTextSwitch Label="@T("Include hidden files and folders?")" Disabled="@this.isProcessingBatch" Value="@this.includeHiddenFilesAndFolders" ValueChanged="@(v => this.includeHiddenFilesAndFolders = v)" LabelOn="@T("Yes, process hidden files and folders as well")" LabelOff="@T("No, ignore hidden files and folders")"/>
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">@T("Hidden files and folders")</MudText>
|
||||
<MudTextSwitch Label="@T("Include hidden files and folders?")" Disabled="@this.isProcessingBatch" Value="@this.includeHiddenFilesAndFolders" ValueChanged="@(v => this.includeHiddenFilesAndFolders = v)" LabelOn="@T("Yes, include hidden files and folders")" LabelOff="@T("No, ignore hidden files and folders")"/>
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
|
||||
@T("Hidden are files and folders whose names start with a dot, such as .git or .env, and those hidden by the operating system. A file pattern which starts with a dot, such as .gitignore, selects such files anyway.")
|
||||
@T("Files and folders are hidden if their names start with a dot or the operating system marks them as hidden. If you choose 'No', hidden items are skipped. To explicitly include a file such as .gitignore, enter its name under 'Files to include'. Using generic patterns such as *, :text: or :documents: still skips hidden files. Exclusion rules always take priority; you can change them in the fields below.")
|
||||
</MudJustifiedText>
|
||||
|
||||
@* Without an icon: in a field of several lines, it floats in the middle of the text. *@
|
||||
<MudTextField T="string" @bind-Text="@this.excludePatterns" Validation="@this.ValidateExcludePatterns" Immediate="@true" Disabled="@this.isProcessingBatch" Label="@T("Excluded files and folders")" Lines="@FileExclusions.DEFAULT_LINE_COUNT" AutoGrow="@true" MaxLines="@FileExclusions.MAX_LINE_COUNT" HelperText="@T("These files and folders are left out, even when a file pattern selects them. Separate entries with a semicolon or a line break. End an entry with a slash to exclude every folder of that name, e.g., node_modules/. Other entries name files, e.g., *.min.js.")" Variant="Variant.Outlined" Margin="Margin.Normal" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
<RestoreDefaultButton Text="@T("Restore default")" Disabled="@this.isProcessingBatch" OnClick="@this.RestoreDefaultExcludePatterns" Class="mb-1"/>
|
||||
<div class="mt-4">
|
||||
<FileExclusionEditor @bind-Text="@this.excludePatterns" Disabled="@this.isProcessingBatch" Label="@T("Excluded files and folders")" ResetButtonText="@T("Restore default")"/>
|
||||
</div>
|
||||
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2 mt-4">@T("File size limit")</MudText>
|
||||
<MudNumericField T="int" @bind-Value="@this.maxFileSizeMegabytes" Min="@FolderFileSelection.SMALLEST_MAX_FILE_SIZE_MEGABYTES" Immediate="@true" Disabled="@this.isProcessingBatch" Label="@T("Ignore files larger than")" HelperText="@T("Matching files above this size are left out, e.g., images of virtual machines.")" Adornment="Adornment.End" AdornmentText="MB" Variant="Variant.Outlined" Margin="Margin.Normal"/>
|
||||
</MudCollapse>
|
||||
</MudStack>
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-3 mt-6">
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Instructions")
|
||||
</MudText>
|
||||
|
||||
@@ -134,7 +137,7 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-3 mt-6">
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Output")
|
||||
</MudText>
|
||||
|
||||
@@ -189,7 +192,7 @@ else
|
||||
@T("We always write a semicolon-separated log named log.csv, which lists every document with its processing time, the model, the status, and the details of any error. When you start another run on the same output folder, we ask you whether to continue that run: documents which failed or are missing in the log are then processed again. When no output folder is selected, everything is written to the subfolder 'ai-results' within the input folder.")
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-3 mt-6">
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Processing pace")
|
||||
</MudText>
|
||||
|
||||
@@ -230,7 +233,7 @@ else
|
||||
|
||||
@if (this.fileResults.Count > 0)
|
||||
{
|
||||
<MudText Typo="Typo.h5" Class="mb-3 mt-6">
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Progress")
|
||||
</MudText>
|
||||
|
||||
|
||||
+6
-80
@@ -1,90 +1,18 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.Media;
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Assistants.BatchProcessing;
|
||||
|
||||
public partial class AssistantBatchProcessing
|
||||
{
|
||||
/// <summary>
|
||||
/// What one file of the batch gives to the model: either text or an image.
|
||||
/// Loads a document through the Rust content stream or resolves a persistent
|
||||
/// transcript for an audio or video file.
|
||||
/// </summary>
|
||||
private readonly record struct BatchInput(string Text, FileAttachmentImage? Image);
|
||||
|
||||
/// <summary>
|
||||
/// Loads a document through the Rust content stream, resolves a persistent
|
||||
/// transcript for an audio or video file, or prepares an image for the model.
|
||||
/// </summary>
|
||||
/// <returns>The input for the model, or <c>null</c> when the file already got its final status.</returns>
|
||||
private async Task<BatchInput?> LoadInputContentAsync(BatchProcessingFileResult fileResult, CancellationToken token)
|
||||
private Task<string?> LoadInputContentAsync(BatchProcessingFileResult fileResult, CancellationToken token)
|
||||
{
|
||||
if (IsImage(fileResult.FilePath))
|
||||
return await this.LoadImageAsync(fileResult, token) is { } image ? new BatchInput(string.Empty, image) : null;
|
||||
|
||||
var text = IsTranscribableMedia(fileResult.FilePath)
|
||||
? await this.LoadMediaTranscriptAsync(fileResult, token)
|
||||
: await this.LoadDocumentContentAsync(fileResult, token);
|
||||
|
||||
return text is null ? null : new BatchInput(text, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares an image for the model before the request is built.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The request itself would only skip an image it cannot send, with a message, and ask the model
|
||||
/// anyway. The model would then answer about nothing, and the file would count as done, so that
|
||||
/// continuing the run would skip it as well. Preparing the image here lets it fail as a file
|
||||
/// instead, with the reason, e.g., an image format AI Studio cannot decode. The request then finds
|
||||
/// the prepared image in the cache of ModelImageService, so it is not prepared twice. A failed
|
||||
/// image is processed again when the run is continued, e.g., with a model that can read images.
|
||||
///
|
||||
/// How large an image may be needs no check of its own either: the preparation scales it down
|
||||
/// until it fits the limit of the provider, and fails only when even that is not enough.
|
||||
///
|
||||
/// How many images a model accepts needs no check of its own: every file gets a thread of its own,
|
||||
/// see CallAIAsync, so each request carries exactly one image, in every round with the model. A
|
||||
/// limit of one image is therefore always met, and SupportsImageInput already refuses a limit of zero.
|
||||
///
|
||||
/// Unlike the text of documents and transcripts, an image does not pass the prompt injection
|
||||
/// filter of the runtime, which reads text only. Images in a chat do not either. Instructions
|
||||
/// within an image therefore meet only the safeguards behind the filter: the system prompt tells
|
||||
/// the model to ignore them, a thread which read internal content, such as a mailbox, may no
|
||||
/// longer send data out, and without free address choice, its default, the Read Web Page tool
|
||||
/// refuses an address which appears only in the image.
|
||||
/// </remarks>
|
||||
private async Task<FileAttachmentImage?> LoadImageAsync(BatchProcessingFileResult fileResult, CancellationToken token)
|
||||
{
|
||||
if (!this.ProviderSettings.SupportsImageInput())
|
||||
{
|
||||
this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, T("The selected model cannot read images. Select a model with image support to process this image."));
|
||||
return null;
|
||||
}
|
||||
|
||||
PreparedModelImage prepared;
|
||||
FileAttachmentImage image;
|
||||
try
|
||||
{
|
||||
image = new FileAttachmentImage(fileResult.FileName, fileResult.FilePath, new FileInfo(fileResult.FilePath).Length);
|
||||
prepared = await this.ModelImageService.PrepareAsync(image, this.ProviderSettings, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
this.FinishFileResult(fileResult, BatchProcessingFileStatus.CANCELED, T("The batch run was canceled."));
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, string.Format(T("Was not able to read the file: {0}"), e.Message), e);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (prepared.IsPrepared)
|
||||
return image;
|
||||
|
||||
this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, prepared.ErrorCode.ToUserMessage(fileResult.FileName));
|
||||
return null;
|
||||
return IsTranscribableMedia(fileResult.FilePath)
|
||||
? this.LoadMediaTranscriptAsync(fileResult, token)
|
||||
: this.LoadDocumentContentAsync(fileResult, token);
|
||||
}
|
||||
|
||||
private async Task<string?> LoadDocumentContentAsync(BatchProcessingFileResult fileResult, CancellationToken token)
|
||||
@@ -181,7 +109,5 @@ public partial class AssistantBatchProcessing
|
||||
|
||||
private static bool IsTranscribableMedia(string filePath) => PersistentMediaTranscript.IsMediaPath(filePath);
|
||||
|
||||
private static bool IsImage(string filePath) => FileTypes.IsAllowedPath(filePath, FileTypes.IMAGE);
|
||||
|
||||
private static bool HasReusableTranscript(string mediaFilePath) => MediaSidecarTranscripts.HasReusableTranscript(mediaFilePath);
|
||||
}
|
||||
+6
-16
@@ -56,8 +56,6 @@ public partial class AssistantBatchProcessing
|
||||
# Task description
|
||||
You are a batch document processing agent. Each request contains exactly one DOCUMENT.
|
||||
Your task is to process this DOCUMENT strictly according to the instructions below.
|
||||
A DOCUMENT may also be an image attached to the request. Then, the content of the
|
||||
DOCUMENT is what the image shows, including any text within it.
|
||||
|
||||
# Scope and precedence
|
||||
Use only information explicitly contained in the DOCUMENT and the instructions.
|
||||
@@ -90,20 +88,11 @@ public partial class AssistantBatchProcessing
|
||||
""";
|
||||
}
|
||||
|
||||
private static string BuildImageUserPrompt(string fileName)
|
||||
{
|
||||
return $"""
|
||||
# DOCUMENT
|
||||
File name: {fileName}
|
||||
Content: The DOCUMENT is the image attached to this request.
|
||||
""";
|
||||
}
|
||||
|
||||
/// <param name="fileName">The name of the document being processed.</param>
|
||||
/// <param name="input">The text or the image handed to the model.</param>
|
||||
/// <param name="fileContent">The content handed to the model.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The answer of the model, and which tools it used to get there.</returns>
|
||||
private async Task<(string Answer, string UsedTools)> CallAIAsync(string fileName, BatchInput input, CancellationToken token)
|
||||
private async Task<(string Answer, string UsedTools)> CallAIAsync(string fileName, string fileContent, CancellationToken token)
|
||||
{
|
||||
//
|
||||
// Every file of the batch gets the tools the user picked for the job. The batch builds its
|
||||
@@ -131,9 +120,10 @@ public partial class AssistantBatchProcessing
|
||||
};
|
||||
|
||||
this.AssignRuntimeIdentity(chatThread);
|
||||
var userPrompt = input.Image is null
|
||||
? new ContentText { Text = BuildUserPrompt(fileName, input.Text) }
|
||||
: new ContentText { Text = BuildImageUserPrompt(fileName), FileAttachments = [input.Image] };
|
||||
var userPrompt = new ContentText
|
||||
{
|
||||
Text = BuildUserPrompt(fileName, fileContent),
|
||||
};
|
||||
|
||||
chatThread.Blocks.Add(new ContentBlock
|
||||
{
|
||||
|
||||
+3
-38
@@ -1,8 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Assistants.BatchProcessing;
|
||||
|
||||
public partial class AssistantBatchProcessing
|
||||
@@ -44,7 +42,6 @@ public partial class AssistantBatchProcessing
|
||||
}
|
||||
|
||||
this.PrepareFileResults(resolvedOutputDirectory, files, previousLog, previousResults);
|
||||
await this.WarnAboutUnreadableImagesAsync();
|
||||
await this.CheckpointAssistantSession();
|
||||
await this.RunBatchAsync(resolvedOutputDirectory);
|
||||
}
|
||||
@@ -91,27 +88,6 @@ public partial class AssistantBatchProcessing
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tells the user before the first file when the images of the run will fail, because the
|
||||
/// selected model cannot read images.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Counts only the images which this run still processes. Images done by an earlier run of a
|
||||
/// continued batch keep their results and do not fail.
|
||||
/// </remarks>
|
||||
private async Task WarnAboutUnreadableImagesAsync()
|
||||
{
|
||||
var numImages = this.fileResults.Count(fileResult => fileResult.Status is not BatchProcessingFileStatus.DONE && IsImage(fileResult.FilePath));
|
||||
if (numImages == 0 || this.ProviderSettings.SupportsImageInput())
|
||||
return;
|
||||
|
||||
var warning = numImages == 1
|
||||
? T("The selected model cannot read images, so the image among the selected files will fail. All other files are processed as usual.")
|
||||
: string.Format(T("The selected model cannot read images, so the {0} images among the selected files will fail. All other files are processed as usual."), numImages);
|
||||
|
||||
await this.MessageBus.SendWarning(new(Icons.Material.Filled.ImageNotSupported, warning));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes all documents which are not restored from a previous run.
|
||||
/// </summary>
|
||||
@@ -212,14 +188,14 @@ public partial class AssistantBatchProcessing
|
||||
/// </remarks>
|
||||
private async Task ProcessOneFileAsync(BatchProcessingFileResult fileResult, string resolvedOutputDirectory, CancellationToken token)
|
||||
{
|
||||
var input = await this.LoadInputContentAsync(fileResult, token);
|
||||
if (input is null)
|
||||
var fileContent = await this.LoadInputContentAsync(fileResult, token);
|
||||
if (fileContent is null)
|
||||
return;
|
||||
|
||||
string aiAnswer;
|
||||
try
|
||||
{
|
||||
(aiAnswer, fileResult.UsedTools) = await this.CallAIAsync(fileResult.FileName, input.Value, token);
|
||||
(aiAnswer, fileResult.UsedTools) = await this.CallAIAsync(fileResult.FileName, fileContent, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -241,17 +217,6 @@ public partial class AssistantBatchProcessing
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// The request reads the image anew in every round with the model, e.g., after each tool
|
||||
// call, and quietly leaves out an image which is gone by then. The answer would be about
|
||||
// nothing and must not count as a result, so that continuing the run processes the image again:
|
||||
//
|
||||
if (input.Value.Image is { Exists: false })
|
||||
{
|
||||
this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, T("The image was no longer available while the model was asked about it."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(aiAnswer))
|
||||
{
|
||||
this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, T("The AI answer was empty."));
|
||||
|
||||
+43
-15
@@ -1,4 +1,3 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.Media;
|
||||
|
||||
namespace AIStudio.Assistants.BatchProcessing;
|
||||
@@ -83,18 +82,39 @@ public partial class AssistantBatchProcessing
|
||||
private IReadOnlyList<string> FindInputFiles(string resolvedOutputDirectory)
|
||||
{
|
||||
var selection = new FolderFileSelection(FileNamePatterns.Parse(this.filePatterns), FileExclusions.Parse(this.excludePatterns), this.includeSubdirectories, this.includeHiddenFilesAndFolders, this.maxFileSizeMegabytes);
|
||||
var outputFolder = new BatchProcessingOutputFolder(this.inputDirectory, resolvedOutputDirectory, this.ResolveResultsFileName());
|
||||
var files = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
this.numTooLargeFiles = 0;
|
||||
|
||||
var normalizedInputDirectory = TrimDirectorySeparator(Path.GetFullPath(this.inputDirectory));
|
||||
var normalizedOutputDirectory = TrimDirectorySeparator(Path.GetFullPath(resolvedOutputDirectory));
|
||||
|
||||
// When the output folder is a folder of its own, we skip everything
|
||||
// inside it. When it is the input folder itself, we must not skip the
|
||||
// whole folder: we would not find any document at all. We then skip
|
||||
// our own output artifacts instead.
|
||||
var isOutputSeparateFolder = !string.Equals(normalizedInputDirectory, normalizedOutputDirectory, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// The separator is essential: without it, an output folder named 'out'
|
||||
// would also exclude a document named 'output-notes.md':
|
||||
var outputDirectoryPrefix = normalizedOutputDirectory + Path.DirectorySeparatorChar;
|
||||
|
||||
foreach (var file in selection.EnumerateCandidates(
|
||||
this.inputDirectory,
|
||||
includeDirectory: outputFolder.Enters,
|
||||
includeDirectory: directory => !isOutputSeparateFolder
|
||||
|| !string.Equals(TrimDirectorySeparator(Path.GetFullPath(directory)), normalizedOutputDirectory, StringComparison.OrdinalIgnoreCase),
|
||||
reportInaccessibleDirectory: (directory, exception) =>
|
||||
throw new IOException($"Could not access directory '{directory}'.", exception)))
|
||||
{
|
||||
var normalizedFile = Path.GetFullPath(file);
|
||||
if (!selection.Selects(normalizedFile) || IsTranscriptArtifact(normalizedFile) || outputFolder.IsOutputOfRun(normalizedFile))
|
||||
if (!selection.Selects(normalizedFile) || IsTranscriptArtifact(normalizedFile))
|
||||
continue;
|
||||
|
||||
if (isOutputSeparateFolder)
|
||||
{
|
||||
if (normalizedFile.StartsWith(outputDirectoryPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
}
|
||||
else if (this.IsOwnOutputArtifact(normalizedFile))
|
||||
continue;
|
||||
|
||||
//
|
||||
@@ -116,6 +136,25 @@ public partial class AssistantBatchProcessing
|
||||
return [.. files];
|
||||
}
|
||||
|
||||
private static string TrimDirectorySeparator(string path) => path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a file is an output artifact of this assistant. We need
|
||||
/// this when the output folder is the input folder: without it, the results
|
||||
/// of a previous run would be processed as documents.
|
||||
/// </summary>
|
||||
private bool IsOwnOutputArtifact(string filePath)
|
||||
{
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
if (string.Equals(fileName, LOG_FILENAME, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
if (string.Equals(fileName, this.ResolveResultsFileName(), StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
return fileName.EndsWith(RESULT_FILE_SUFFIX, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks for persistent or interrupted media transcript artifacts. They
|
||||
/// always live beside their source file, independently of the output folder.
|
||||
@@ -178,17 +217,6 @@ public partial class AssistantBatchProcessing
|
||||
return null;
|
||||
}
|
||||
|
||||
//
|
||||
// A model that cannot read images would get the text of the request alone, without the
|
||||
// image, and answer about nothing. Such images fail one by one, see LoadImageAsync, while the
|
||||
// documents next to them are processed. A run of nothing but images does not start at all:
|
||||
//
|
||||
if (files.All(IsImage) && !this.ProviderSettings.SupportsImageInput())
|
||||
{
|
||||
this.AddInputIssue(T("All selected files are images, but the selected model cannot read images. Select a model with image support."));
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(resolvedOutputDirectory);
|
||||
|
||||
@@ -15,13 +15,10 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
|
||||
[Inject]
|
||||
private PandocAvailabilityService PandocAvailability { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ModelImageService ModelImageService { get; init; } = null!;
|
||||
|
||||
private const string DEFAULT_OUTPUT_DIRECTORY_NAME = "ai-results";
|
||||
private const string DEFAULT_RESULTS_FILENAME = "batch-results.csv";
|
||||
private const string CSV_EXTENSION = ".csv";
|
||||
internal const string RESULT_FILE_SUFFIX = "_result";
|
||||
private const string RESULT_FILE_SUFFIX = "_result";
|
||||
private const string TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
private const char LOG_SEPARATOR = ';';
|
||||
|
||||
@@ -29,7 +26,7 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
|
||||
/// The name of the log file. It is fixed, so that a later batch run finds
|
||||
/// the log of a previous run and can continue it.
|
||||
/// </summary>
|
||||
internal const string LOG_FILENAME = "log.csv";
|
||||
private const string LOG_FILENAME = "log.csv";
|
||||
|
||||
protected override Tools.Components Component => Tools.Components.BATCH_PROCESSING_ASSISTANT;
|
||||
|
||||
@@ -171,8 +168,6 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
|
||||
|
||||
private void RestoreDefaultFilePatterns() => this.filePatterns = DataBatchProcessing.DEFAULT_FILE_PATTERNS;
|
||||
|
||||
private void RestoreDefaultExcludePatterns() => this.excludePatterns = FileExclusions.DEFAULT_PATTERNS;
|
||||
|
||||
private void ToggleExpertSettings() => this.showExpertSettings = !this.showExpertSettings;
|
||||
|
||||
private string GetExpertStyles => this.showExpertSettings ? "border-2 border-dashed rounded pa-2" : string.Empty;
|
||||
@@ -293,13 +288,13 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
|
||||
var minimumLevel = this.GetMinimumConfidenceLevel();
|
||||
var policyProvider = this.SettingsManager.GetPreselectedProvider(this.Component, this.selectedPolicy.PreselectedProvider);
|
||||
if (policyProvider != Settings.Provider.NONE
|
||||
&& this.SettingsManager.IsProviderUsable(policyProvider.UsedLLMProvider, minimumLevel))
|
||||
&& policyProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
|
||||
this.ProviderSettings = policyProvider;
|
||||
else
|
||||
{
|
||||
var fallbackProvider = this.SettingsManager.GetPreselectedProvider(this.Component, usePreselectionBeforeCurrentProvider: true);
|
||||
this.ProviderSettings = fallbackProvider != Settings.Provider.NONE
|
||||
&& this.SettingsManager.IsProviderUsable(fallbackProvider.UsedLLMProvider, minimumLevel)
|
||||
&& fallbackProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel
|
||||
? fallbackProvider
|
||||
: Settings.Provider.NONE;
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
namespace AIStudio.Assistants.BatchProcessing;
|
||||
|
||||
/// <summary>
|
||||
/// Tells the search for documents which of the files it finds a batch run wrote itself.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A run writes its result files, its log, and its results table directly into the output folder.
|
||||
/// What the search must leave out therefore depends on where this folder lies. Below the input
|
||||
/// folder, the search leaves out all of it. When it is the input folder itself, leaving out the
|
||||
/// whole folder would leave no document at all, so the search leaves out what a run writes, by
|
||||
/// name. Anywhere else, above the input folder included, the search never comes across the output
|
||||
/// folder and leaves out nothing.
|
||||
/// </remarks>
|
||||
/// <param name="inputDirectory">The folder the run reads its documents from.</param>
|
||||
/// <param name="outputDirectory">The folder the run writes into.</param>
|
||||
/// <param name="resultsFileName">The file name of the results table.</param>
|
||||
internal sealed class BatchProcessingOutputFolder(string inputDirectory, string outputDirectory, string resultsFileName)
|
||||
{
|
||||
private readonly string fullOutputDirectory = Path.GetFullPath(outputDirectory);
|
||||
|
||||
private readonly bool isInputFolder = IsSameFolder(inputDirectory, outputDirectory);
|
||||
|
||||
//
|
||||
// The same folder with a separator at its end would count as lying below itself:
|
||||
//
|
||||
private readonly bool liesBelowInputFolder = !IsSameFolder(inputDirectory, outputDirectory) && PathTools.IsInsideDirectory(inputDirectory, outputDirectory);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the search enters a subfolder of the input folder: every one but the output folder.
|
||||
/// </summary>
|
||||
/// <param name="directory">The subfolder.</param>
|
||||
public bool Enters(string directory) => !this.liesBelowInputFolder || !IsSameFolder(directory, this.fullOutputDirectory);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a file the search found was written by a run instead of being a document.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The full path of the file.</param>
|
||||
public bool IsOutputOfRun(string filePath)
|
||||
{
|
||||
//
|
||||
// Checked with the separator after the name of the folder: otherwise, an output folder
|
||||
// named 'out' would also leave out a document named 'output-notes.md':
|
||||
//
|
||||
if (this.liesBelowInputFolder)
|
||||
return PathTools.IsInsideDirectory(this.fullOutputDirectory, filePath);
|
||||
|
||||
//
|
||||
// A run writes into the output folder itself, never into one of its subfolders:
|
||||
//
|
||||
return this.isInputFolder
|
||||
&& Path.GetDirectoryName(Path.GetFullPath(filePath)) is { } fileDirectory
|
||||
&& IsSameFolder(fileDirectory, this.fullOutputDirectory)
|
||||
&& this.IsOwnOutputArtifact(filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a file is an output artifact of the assistant. We need
|
||||
/// this when the output folder is the input folder: without it, the results
|
||||
/// of a previous run would be processed as documents.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A result file is recognized by its name without the extension, e.g., report_result.md or
|
||||
/// report_result_2.docx, cf. AssistantBatchProcessing.CreateResultFileName. Its extension does
|
||||
/// not count, because the user may have chosen another file format since a previous run.
|
||||
/// </remarks>
|
||||
private bool IsOwnOutputArtifact(string filePath)
|
||||
{
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
if (string.Equals(fileName, AssistantBatchProcessing.LOG_FILENAME, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(fileName, resultsFileName, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
var stem = Path.GetFileNameWithoutExtension(fileName);
|
||||
var suffixIndex = stem.LastIndexOf(AssistantBatchProcessing.RESULT_FILE_SUFFIX, StringComparison.OrdinalIgnoreCase);
|
||||
if (suffixIndex < 0)
|
||||
return false;
|
||||
|
||||
//
|
||||
// A second result of the same name gets a counter, e.g., report_result_2.md:
|
||||
//
|
||||
var counter = stem[(suffixIndex + AssistantBatchProcessing.RESULT_FILE_SUFFIX.Length)..];
|
||||
return counter.Length is 0 || (counter.Length > 1 && counter[0] is '_' && counter[1..].All(char.IsAsciiDigit));
|
||||
}
|
||||
|
||||
private static bool IsSameFolder(string firstDirectory, string secondDirectory) => string.Equals(TrimDirectorySeparator(Path.GetFullPath(firstDirectory)), TrimDirectorySeparator(Path.GetFullPath(secondDirectory)), PathTools.COMPARISON);
|
||||
|
||||
private static string TrimDirectorySeparator(string path) => path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
@attribute [Route(Routes.ASSISTANT_META_ASSISTANT)]
|
||||
@using AIStudio.Agents.AssistantAudit
|
||||
@using AIStudio.Tools.PluginSystem.Assistants.DataModel
|
||||
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogAssistantBuilder>
|
||||
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.NoSettingsPanel>
|
||||
|
||||
<PreviewBeta ApplyInnerScrollingFix="true"/>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
|
||||
namespace AIStudio.Assistants.Builder;
|
||||
|
||||
public partial class AssistantBuilder : AssistantBaseCore<SettingsDialogAssistantBuilder>
|
||||
public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
||||
{
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
@@ -82,6 +82,8 @@ public partial class AssistantBuilder : AssistantBaseCore<SettingsDialogAssistan
|
||||
// The result is the Lua code of a plugin, and a notice below it would make that code invalid:
|
||||
protected override bool AddsAIGeneratedContentDisclosure => false;
|
||||
|
||||
protected override bool HasSettingsPanel => false;
|
||||
|
||||
protected override Func<string> Result2Copy => () => !string.IsNullOrWhiteSpace(this.generatedLuaAssistant) ? this.generatedLuaAssistant : this.generatedAssistantSpec;
|
||||
|
||||
private BuilderStep step = BuilderStep.DESCRIBE;
|
||||
@@ -249,6 +251,8 @@ public partial class AssistantBuilder : AssistantBaseCore<SettingsDialogAssistan
|
||||
this.launcherDataSourceIds = [];
|
||||
this.launcherToolIds = [];
|
||||
this.selectedAssistantComponents = [];
|
||||
this.selectedOutputLanguage = CommonLanguages.AS_IS;
|
||||
this.customOutputLanguage = string.Empty;
|
||||
this.allowGeneratedAssistantProfiles = true;
|
||||
this.extraRules = string.Empty;
|
||||
this.exampleRequest = string.Empty;
|
||||
@@ -256,25 +260,9 @@ public partial class AssistantBuilder : AssistantBaseCore<SettingsDialogAssistan
|
||||
this.reviewNotes = string.Empty;
|
||||
this.generatedLuaAssistant = string.Empty;
|
||||
this.ResetInstallFlow();
|
||||
|
||||
if (!this.MightPreselectValues())
|
||||
{
|
||||
this.selectedOutputLanguage = CommonLanguages.AS_IS;
|
||||
this.customOutputLanguage = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool MightPreselectValues()
|
||||
{
|
||||
if (this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectOptions)
|
||||
{
|
||||
this.selectedOutputLanguage = this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedOutputLanguage;
|
||||
this.customOutputLanguage = this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedOtherOutputLanguage;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
protected override bool MightPreselectValues() => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state)
|
||||
|
||||
+3
-3
@@ -619,7 +619,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
||||
{
|
||||
// Keep the current provider if it still satisfies the minimum confidence:
|
||||
if (this.ProviderSettings != Settings.Provider.NONE &&
|
||||
this.SettingsManager.IsProviderUsable(this.ProviderSettings.UsedLLMProvider, minimumLevel))
|
||||
this.ProviderSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
|
||||
{
|
||||
this.CurrentProfile = this.ResolveProfileSelection();
|
||||
return;
|
||||
@@ -628,7 +628,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
||||
|
||||
// Try to apply the policy preselection:
|
||||
var policyProvider = this.SettingsManager.GetProviderById(this.selectedPolicy.PreselectedProvider);
|
||||
if (policyProvider != Settings.Provider.NONE && this.SettingsManager.IsProviderUsable(policyProvider.UsedLLMProvider, minimumLevel))
|
||||
if (policyProvider != Settings.Provider.NONE && policyProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
|
||||
{
|
||||
this.ProviderSettings = policyProvider;
|
||||
this.CurrentProfile = this.ResolveProfileSelection();
|
||||
@@ -637,7 +637,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
||||
|
||||
var fallbackProvider = this.SettingsManager.GetPreselectedProvider(this.Component, this.ProviderSettings.Id);
|
||||
if (fallbackProvider != Settings.Provider.NONE &&
|
||||
!this.SettingsManager.IsProviderUsable(fallbackProvider.UsedLLMProvider, minimumLevel))
|
||||
fallbackProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level < minimumLevel)
|
||||
fallbackProvider = Settings.Provider.NONE;
|
||||
|
||||
this.ProviderSettings = fallbackProvider;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -146,15 +146,15 @@ public sealed record ChatThread
|
||||
|
||||
/// <summary>
|
||||
/// Where this chat may still send data, because of the internal content it holds: mails, a
|
||||
/// search of the wiki, an internal web page, or passages of a data source.
|
||||
/// search of the wiki, or an internal web page.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The third thing the thread ratchets, next to the data security and the required provider
|
||||
/// confidence: once internal content is in the thread, every tool which runs afterwards could
|
||||
/// carry it out, in a search query or in the address of a web page. It only ever gets stricter,
|
||||
/// and a later change of the settings of a mailbox, a data source, or Read Web Page does not
|
||||
/// loosen it either, because the content was read under the old one. A chat which holds no
|
||||
/// internal content demands nothing.
|
||||
/// and a later change of the settings of a mailbox or of Read Web Page does not loosen it
|
||||
/// either, because the content was read under the old one. A chat which holds no internal
|
||||
/// content demands nothing.
|
||||
/// </remarks>
|
||||
[JsonInclude]
|
||||
public OutboundDataRequirement RequiredOutboundDataRestriction { get; private set; } = OutboundDataRequirement.NONE;
|
||||
|
||||
@@ -10,8 +10,6 @@ public static class ChatThreadExtensions
|
||||
/// Checks if the specified provider is allowed for the chat thread.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A provider the organization prohibited is never allowed, whatever the chat thread.<br/><br/>
|
||||
///
|
||||
/// We don't check if the provider is allowed to use the data sources of the chat thread.
|
||||
/// That kind of check is done when the available data sources are resolved.<br/><br/>
|
||||
///
|
||||
@@ -25,16 +23,11 @@ public static class ChatThreadExtensions
|
||||
/// <returns>True, when the provider is allowed for the chat thread. False, otherwise.</returns>
|
||||
public static bool IsLLMProviderAllowed<T>(this ChatThread? chatThread, T provider)
|
||||
{
|
||||
var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
||||
|
||||
// A provider the organization prohibited is allowed in no chat, not even in a new one:
|
||||
if (settingsManager.IsProviderProhibited(GetLLMProvider(provider)))
|
||||
return false;
|
||||
|
||||
// No chat thread available means we have a new chat. That's fine:
|
||||
if (chatThread is null)
|
||||
return true;
|
||||
|
||||
var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
||||
var providerConfidence = provider switch
|
||||
{
|
||||
IProvider p => p.GetConfidenceLevel(settingsManager),
|
||||
@@ -82,12 +75,4 @@ public static class ChatThreadExtensions
|
||||
false => chatThread.DataSecurity is not DataSourceSecurity.SELF_HOSTED,
|
||||
};
|
||||
}
|
||||
|
||||
private static LLMProviders GetLLMProvider<T>(T provider) => provider switch
|
||||
{
|
||||
IProvider p => p.Provider,
|
||||
AIStudio.Settings.Provider p => p.UsedLLMProvider,
|
||||
|
||||
_ => LLMProviders.NONE,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Tools.Validation;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
@@ -52,6 +53,23 @@ public sealed class ContentImage : IContent, IImageSource
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ContentImage from a local file path.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path to the image file.</param>
|
||||
/// <returns>A new ContentImage instance if the file is valid, null otherwise.</returns>
|
||||
public static async Task<ContentImage?> CreateFromFileAsync(string filePath)
|
||||
{
|
||||
if (!await FileExtensionValidation.IsImageExtensionValidWithNotifyAsync(filePath))
|
||||
return null;
|
||||
|
||||
return new ContentImage
|
||||
{
|
||||
SourceType = ContentImageSource.LOCAL_PATH,
|
||||
Source = filePath,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The type of the image source.
|
||||
/// </summary>
|
||||
|
||||
@@ -149,7 +149,7 @@ public sealed class ContentText : IContent
|
||||
|
||||
if(!chatThread.IsLLMProviderAllowed(provider))
|
||||
{
|
||||
LOGGER.LogError("The provider is not allowed for this chat thread due to a prohibition of the organization, data security, or confidence-level requirements. Skipping the AI process.");
|
||||
LOGGER.LogError("The provider is not allowed for this chat thread due to data security or confidence-level requirements. Skipping the AI process.");
|
||||
await this.CompleteWithoutStreaming();
|
||||
return chatThread;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Provider.SelfHosted;
|
||||
using AIStudio.Tools.MIME;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
@@ -8,40 +7,72 @@ public static class IImageSourceExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(IImageSourceExtensions).Namespace, nameof(IImageSourceExtensions));
|
||||
|
||||
/// <summary>
|
||||
/// The largest image, in bytes, we send anywhere: no image prepared for a model is larger, and
|
||||
/// TryAsBase64 skips every larger file.
|
||||
/// </summary>
|
||||
public const long MAX_IMAGE_SIZE_BYTES = 10_000_000;
|
||||
|
||||
/// <summary>
|
||||
/// The largest image file, in bytes, which we send through the given provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The smallest of the limits we know: the one of a self-hosted engine, the one of the provider,
|
||||
/// and MAX_IMAGE_SIZE_BYTES, our own. A provider or engine which states no limit gets ours.
|
||||
/// </remarks>
|
||||
/// <param name="provider">The configured provider the image is sent through.</param>
|
||||
/// <returns>The size limit of one image file.</returns>
|
||||
public static long MaxImageSizeBytesFor(AIStudio.Settings.Provider provider)
|
||||
public static MIMEType DetermineMimeType(this IImageSource image)
|
||||
{
|
||||
var engineLimit = provider.UsedLLMProvider is LLMProviders.SELF_HOSTED ? provider.Host.GetMaxImageSizeBytes() : null;
|
||||
var providerLimit = provider.UsedLLMProvider.GetMaxImageSizeBytes();
|
||||
return Math.Min(Math.Min(engineLimit ?? MAX_IMAGE_SIZE_BYTES, providerLimit ?? MAX_IMAGE_SIZE_BYTES), MAX_IMAGE_SIZE_BYTES);
|
||||
switch (image.SourceType)
|
||||
{
|
||||
case ContentImageSource.BASE64:
|
||||
{
|
||||
// Try to detect the mime type from the base64 string:
|
||||
var base64Data = image.Source;
|
||||
if (base64Data.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var mimeEnd = base64Data.IndexOf(';');
|
||||
if (mimeEnd > 5)
|
||||
return Builder.FromTextRepresentation(base64Data[5..mimeEnd]);
|
||||
}
|
||||
|
||||
// Fallback:
|
||||
return Builder.Create().UseApplication().UseSubtype(ApplicationSubtype.OCTET_STREAM).Build();
|
||||
}
|
||||
|
||||
case ContentImageSource.URL:
|
||||
{
|
||||
// Try to detect the mime type from the URL extension:
|
||||
var uri = new Uri(image.Source);
|
||||
var extension = Path.GetExtension(uri.AbsolutePath).ToLowerInvariant();
|
||||
return DeriveMIMETypeFromExtension(extension);
|
||||
}
|
||||
|
||||
case ContentImageSource.LOCAL_PATH:
|
||||
{
|
||||
var extension = Path.GetExtension(image.Source).ToLowerInvariant();
|
||||
return DeriveMIMETypeFromExtension(extension);
|
||||
}
|
||||
|
||||
default:
|
||||
return Builder.Create().UseApplication().UseSubtype(ApplicationSubtype.OCTET_STREAM).Build();
|
||||
}
|
||||
}
|
||||
|
||||
private static MIMEType DeriveMIMETypeFromExtension(string extension)
|
||||
{
|
||||
var imageBuilder = Builder.Create().UseImage();
|
||||
return extension switch
|
||||
{
|
||||
".png" => imageBuilder.UseSubtype(ImageSubtype.PNG).Build(),
|
||||
".jpg" or ".jpeg" => imageBuilder.UseSubtype(ImageSubtype.JPEG).Build(),
|
||||
".gif" => imageBuilder.UseSubtype(ImageSubtype.GIF).Build(),
|
||||
".webp" => imageBuilder.UseSubtype(ImageSubtype.WEBP).Build(),
|
||||
".tiff" or ".tif" => imageBuilder.UseSubtype(ImageSubtype.TIFF).Build(),
|
||||
".heic" or ".heif" => imageBuilder.UseSubtype(ImageSubtype.HEIC).Build(),
|
||||
|
||||
_ => Builder.Create().UseApplication().UseSubtype(ApplicationSubtype.OCTET_STREAM).Build()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the image content as a base64 string.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is for the images inside the content of a chat, which go to external data sources and
|
||||
/// agents as they are. Attached images, which go to a model, take ModelImageService instead: it
|
||||
/// scales them down and encodes them anew within the limit of the provider.<br/>
|
||||
/// <br/>
|
||||
/// 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
|
||||
/// returns an empty string.<br/>
|
||||
/// <br/>
|
||||
/// 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
|
||||
/// 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.
|
||||
@@ -66,7 +97,7 @@ public static class IImageSourceExtensions
|
||||
{
|
||||
// Read the length of the content:
|
||||
var lengthBytes = response.Content.Headers.ContentLength;
|
||||
if(lengthBytes > MAX_IMAGE_SIZE_BYTES)
|
||||
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.")));
|
||||
return (success: false, string.Empty);
|
||||
@@ -85,7 +116,7 @@ public static class IImageSourceExtensions
|
||||
{
|
||||
// Read the content length:
|
||||
var length = new FileInfo(image.Source).Length;
|
||||
if(length > MAX_IMAGE_SIZE_BYTES)
|
||||
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.")));
|
||||
return (success: false, string.Empty);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Security;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
@@ -16,16 +14,14 @@ public static class ListContentBlockExtensions
|
||||
/// <param name="roleTransformer">A function that transforms each content block into a message result asynchronously.</param>
|
||||
/// <param name="provider">The configured provider, whose model is being written to.</param>
|
||||
/// <param name="textSubContentFactory">A factory function to create text sub-content.</param>
|
||||
/// <param name="imageSubContentFactory">A factory function to create image sub-content from an image prepared for the model.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <param name="imageSubContentFactory">A factory function to create image sub-content.</param>
|
||||
/// <returns>An asynchronous task that resolves to a list of transformed results.</returns>
|
||||
public static async Task<IList<IMessageBase>> BuildMessagesAsync(
|
||||
this List<ContentBlock> blocks,
|
||||
AIStudio.Settings.Provider provider,
|
||||
Func<ChatRole, string> roleTransformer,
|
||||
Func<string, ISubContent> textSubContentFactory,
|
||||
Func<PreparedModelImage, ISubContent> imageSubContentFactory,
|
||||
CancellationToken token = default)
|
||||
Func<FileAttachmentImage, Task<ISubContent>> imageSubContentFactory)
|
||||
{
|
||||
//
|
||||
// Asked through the configured provider, so that what a person set in their expert settings
|
||||
@@ -63,7 +59,7 @@ public static class ListContentBlockExtensions
|
||||
|
||||
// Text prompt with images as attachments, and the provider/model can process images:
|
||||
case ContentText text when block.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace(text.Text) && text.FileAttachments.ContainsImages():
|
||||
messageTaskList.Add(CreateMultimodalMessageAsync(block, text));
|
||||
messageTaskList.Add(CreateMultimodalMessageAsync(block, text, textSubContentFactory, imageSubContentFactory));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -85,41 +81,34 @@ public static class ListContentBlockExtensions
|
||||
}
|
||||
|
||||
// Local function to create a multimodal message asynchronously.
|
||||
Task<IMessageBase> CreateMultimodalMessageAsync(ContentBlock block, ContentText text)
|
||||
Task<IMessageBase> CreateMultimodalMessageAsync(
|
||||
ContentBlock block,
|
||||
ContentText text,
|
||||
Func<string, ISubContent> innerTextSubContentFactory,
|
||||
Func<FileAttachmentImage, Task<ISubContent>> innerImageSubContentFactory)
|
||||
{
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
var images = await Task.WhenAll(text.FileAttachments
|
||||
var imagesTasks = text.FileAttachments
|
||||
.Where(x => x is { IsImage: true, Exists: true })
|
||||
.Cast<FileAttachmentImage>()
|
||||
.Select(CreateImageSubContentAsync));
|
||||
.Select(innerImageSubContentFactory)
|
||||
.ToList();
|
||||
|
||||
Task.WaitAll(imagesTasks);
|
||||
var images = imagesTasks.Select(t => t.Result).ToList();
|
||||
|
||||
return new MultimodalMessage
|
||||
{
|
||||
Role = roleTransformer(block.Role),
|
||||
Content =
|
||||
[
|
||||
textSubContentFactory(await text.PrepareTextContentForAI(reportingScope)),
|
||||
..images.OfType<ISubContent>(),
|
||||
innerTextSubContentFactory(await text.PrepareTextContentForAI(reportingScope)),
|
||||
..images,
|
||||
]
|
||||
} as IMessageBase;
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Local function to prepare one image for the model. An image which cannot be prepared
|
||||
// stays out of the message, and the user learns why. It used to go along as an image
|
||||
// without data, which the providers refuse together with the whole request.
|
||||
//
|
||||
async Task<ISubContent?> CreateImageSubContentAsync(FileAttachmentImage image)
|
||||
{
|
||||
var prepared = await Program.SERVICE_PROVIDER.GetRequiredService<ModelImageService>().PrepareAsync(image, provider, token);
|
||||
if (prepared.IsPrepared)
|
||||
return imageSubContentFactory(prepared);
|
||||
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, prepared.ErrorCode.ToUserMessage(image.FileName)));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -169,16 +158,20 @@ public static class ListContentBlockExtensions
|
||||
Text = text,
|
||||
};
|
||||
|
||||
private static ISubContent DirectImageSubContentFactory(PreparedModelImage image) => new SubContentImageUrl
|
||||
private static async Task<ISubContent> DirectImageSubContentFactory(FileAttachmentImage attachment) => new SubContentImageUrl
|
||||
{
|
||||
ImageUrl = image.ToDataUrl(),
|
||||
ImageUrl = await attachment.TryAsBase64() is (true, var base64Content)
|
||||
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
|
||||
: string.Empty,
|
||||
};
|
||||
|
||||
private static ISubContent NestedImageSubContentFactory(PreparedModelImage image) => new SubContentImageUrlNested
|
||||
private static async Task<ISubContent> NestedImageSubContentFactory(FileAttachmentImage attachment) => new SubContentImageUrlNested
|
||||
{
|
||||
ImageUrl = new SubContentImageUrlData
|
||||
{
|
||||
Url = image.ToDataUrl(),
|
||||
Url = await attachment.TryAsBase64() is (true, var base64Content)
|
||||
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
|
||||
: string.Empty,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
|
||||
@if (!this.ChatThread.IsLLMProviderAllowed(this.Provider))
|
||||
{
|
||||
<MudTooltip Text="@(this.SettingsManager.IsProviderProhibited(this.Provider.UsedLLMProvider) ? T("The use of this provider has been prohibited by your organization. Please select a different provider.") : T("The selected provider is not allowed in this chat due to data security or confidence-level requirements."))" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudTooltip Text="@T("The selected provider is not allowed in this chat due to data security or confidence-level requirements.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Error"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@@ -901,84 +901,6 @@ public partial class ChatComponent : MSGComponentBase
|
||||
this.selectedToolIds = await this.ToolRegistry.FilterSelectableToolIdsAsync(Tools.Components.CHAT, templateToolIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes over the profile and provider which the chat template preselects, wherever it preselects one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A preselection, not a limit — the user changes both in the chat as usual. A template which
|
||||
/// says nothing, or names a provider which is gone or may not be used for chats, leaves the
|
||||
/// current choice in place, so the chat starts as it would without the template. A template
|
||||
/// whose profile is gone starts with the default profile of the chat settings instead. What the
|
||||
/// previous template preselected is not carried over, see SettingsManager.ChooseChatTemplateProfile.
|
||||
/// The caller tells the parent about a new provider.
|
||||
/// </remarks>
|
||||
/// <param name="previousChatTemplate">The chat template the current profile and provider were chosen with; the current one when the template does not change.</param>
|
||||
/// <returns>True when the provider changed.</returns>
|
||||
private bool ApplyChatTemplateProfileAndProvider(ChatTemplate previousChatTemplate)
|
||||
{
|
||||
this.currentProfile = this.SettingsManager.ChooseChatTemplateProfile(this.currentChatTemplate, previousChatTemplate, this.currentProfile);
|
||||
|
||||
var provider = this.SettingsManager.ChooseChatTemplateProvider(this.currentChatTemplate, previousChatTemplate, this.Provider);
|
||||
if (provider == this.Provider)
|
||||
return false;
|
||||
|
||||
this.Provider = provider;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The workspace a chat which has no thread yet gets with its first content.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The first content is the first message, a media import, or a draft kept when the user leaves
|
||||
/// the page. A chat without a thread belongs to no workspace, so the workspace of its chat
|
||||
/// template fills in, opened or created by its name. That happens only here, when the chat gets
|
||||
/// something to keep: merely opening the chat page or trying out a template creates no
|
||||
/// workspace, and a workspace the user deleted stays gone until a chat with that template
|
||||
/// actually starts. Without a usable workspace of the template, the chat stays without one.
|
||||
/// </remarks>
|
||||
/// <returns>The ID of the workspace, or Guid.Empty for none.</returns>
|
||||
private async Task<Guid> ResolveWorkspaceOfFirstThreadAsync()
|
||||
{
|
||||
if (this.currentWorkspaceId != Guid.Empty)
|
||||
return this.currentWorkspaceId;
|
||||
|
||||
if (this.SettingsManager.GetChatTemplateWorkspaceName(this.currentChatTemplate) is not { } workspaceName)
|
||||
return Guid.Empty;
|
||||
|
||||
return await WorkspaceBehaviour.ResolveOrCreateChatTemplateWorkspaceAsync(workspaceName, this.currentChatTemplate.GetSafeName());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hands a thread which this component just created for a chat without one to the parent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The thread is a parameter of this component, and the parent hands over its own whenever it
|
||||
/// renders. So the parent hears about the new thread before anything else may render it, see
|
||||
/// StartNewChat. Only afterward, the header shows the workspace of the thread, since showing a
|
||||
/// workspace name renders the parent as well.
|
||||
/// </remarks>
|
||||
private async Task PublishNewChatThreadAsync()
|
||||
{
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
|
||||
// The header shows the workspace of the new thread from now on, e.g., the one of its chat template:
|
||||
if (this.ChatThread is not null && this.ChatThread.WorkspaceId != Guid.Empty)
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the open chat is in the workspace which the given chat template names.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Such a workspace was the choice of the template rather than of the user, so a chat started
|
||||
/// with another template does not have to stay there.
|
||||
/// </remarks>
|
||||
private bool IsInWorkspaceOfChatTemplate(ChatTemplate chatTemplate) => this.currentWorkspaceId != Guid.Empty
|
||||
&& this.SettingsManager.GetChatTemplateWorkspaceName(chatTemplate) is { } workspaceName
|
||||
&& string.Equals(WorkspaceBehaviour.NormalizeWorkspaceName(this.currentWorkspaceName), workspaceName, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// The data source options a chat starts with: those of its chat template, or the chat defaults.
|
||||
/// </summary>
|
||||
@@ -1084,36 +1006,24 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
private async Task ChatTemplateWasChanged(ChatTemplate chatTemplate)
|
||||
{
|
||||
var nextChatTemplate = this.SettingsManager.GetChatTemplateById(chatTemplate.Id);
|
||||
if (this.ChatThread is not null)
|
||||
{
|
||||
//
|
||||
// Starting the new chat is what hands the new template to it, with its text, attachments,
|
||||
// and selections. Nothing changes here before, since the user may still decline to leave
|
||||
// the open chat; it then stays as it was, with its template. The new chat stays in the
|
||||
// workspace of the open one, unless that workspace is merely the one of the current
|
||||
// template: then the new template decides where its chat goes.
|
||||
//
|
||||
await this.StartNewChat(useSameWorkspace: !this.IsInWorkspaceOfChatTemplate(this.currentChatTemplate), chatTemplate: nextChatTemplate);
|
||||
return;
|
||||
}
|
||||
|
||||
var previousChatTemplate = this.currentChatTemplate;
|
||||
this.currentChatTemplate = nextChatTemplate;
|
||||
this.currentChatTemplate = this.SettingsManager.GetChatTemplateById(chatTemplate.Id);
|
||||
if(!string.IsNullOrWhiteSpace(this.currentChatTemplate.PredefinedUserPrompt))
|
||||
this.ComposerState.SetSystemInput(this.currentChatTemplate.PredefinedUserPrompt);
|
||||
|
||||
// Apply template's file attachments (replaces existing):
|
||||
this.ComposerState.ReplaceFileAttachments(this.currentChatTemplate.FileAttachments);
|
||||
|
||||
if (this.ChatThread is not null)
|
||||
{
|
||||
// Starting the new chat is what hands the selection of the new template to it:
|
||||
await this.StartNewChat(true);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Without a thread there is nothing to start anew, so the selection of the new template is
|
||||
// applied right here. It travels into the thread which the first message creates, and that
|
||||
// message also decides the workspace, see ResolveWorkspaceOfFirstThreadAsync.
|
||||
// applied right here. It travels into the thread which the first message creates.
|
||||
//
|
||||
if (this.ApplyChatTemplateProfileAndProvider(previousChatTemplate))
|
||||
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||
|
||||
await this.ApplyChatTemplateToolSelectionAsync();
|
||||
this.ApplyAutomaticDataSourceOptions();
|
||||
}
|
||||
@@ -1137,19 +1047,6 @@ public partial class ChatComponent : MSGComponentBase
|
||||
{
|
||||
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
|
||||
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
|
||||
|
||||
//
|
||||
// A chat which has not started yet keeps up with its chat template. The provider follows
|
||||
// only when the template now preselects another one, so that a provider the user picked
|
||||
// by hand survives a change of some unrelated setting:
|
||||
//
|
||||
if (previousChatTemplate.Id != this.currentChatTemplate.Id || previousChatTemplate.ProviderId != this.currentChatTemplate.ProviderId)
|
||||
{
|
||||
if (this.ApplyChatTemplateProfileAndProvider(previousChatTemplate))
|
||||
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||
}
|
||||
else if (this.SettingsManager.GetChatTemplateStartProfile(this.currentChatTemplate) is { } templateProfile)
|
||||
this.currentProfile = templateProfile;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1359,16 +1256,10 @@ public partial class ChatComponent : MSGComponentBase
|
||||
if (!this.ComposerState.HasVisibleUserDraft)
|
||||
return;
|
||||
|
||||
//
|
||||
// The message moves out of the composer before anything below waits, so that leaving a
|
||||
// second time meanwhile finds nothing left to keep. Only the workspace follows afterward,
|
||||
// since finding or creating the one of the chat template may take a moment:
|
||||
//
|
||||
this.RefreshCurrentProfileAndChatTemplate();
|
||||
chat = this.NewChatThread(this.ExtractThreadName(this.ComposerState.UserInput), Guid.Empty);
|
||||
chat = this.NewChatThread(this.ExtractThreadName(this.ComposerState.UserInput));
|
||||
this.MirrorComposerIntoDraft(chat);
|
||||
this.ComposerState.Clear();
|
||||
chat.WorkspaceId = await this.ResolveWorkspaceOfFirstThreadAsync();
|
||||
}
|
||||
else if (unstoredChatBeingLeft is not null && (!unstoredChatBeingLeft.Draft.IsEmpty || this.MediaTranscriptionService.IsBusy(MediaImportOwner.ForChat(unstoredChatBeingLeft.ChatId))))
|
||||
{
|
||||
@@ -1439,28 +1330,22 @@ public partial class ChatComponent : MSGComponentBase
|
||||
? string.Format(T("Transcription: {0}"), Path.GetFileName(firstMediaPath))
|
||||
: promptName;
|
||||
|
||||
var chat = this.NewChatThread(threadName, await this.ResolveWorkspaceOfFirstThreadAsync());
|
||||
this.ChatThread = this.NewChatThread(threadName);
|
||||
|
||||
// A message the user started belongs to the new chat, so it is stored with it:
|
||||
if (this.ComposerState.HasUserDraft)
|
||||
this.MirrorComposerIntoDraft(chat);
|
||||
|
||||
//
|
||||
// The parent hears about the new thread before storing it waits, as with the first message.
|
||||
// A render of the parent in between would otherwise hand back no thread at all, and the
|
||||
// transcript would arrive in a chat which is nowhere:
|
||||
//
|
||||
this.ChatThread = chat;
|
||||
await this.PublishNewChatThreadAsync();
|
||||
this.MirrorComposerIntoDraft(this.ChatThread);
|
||||
|
||||
//
|
||||
// Like saving, this goes through the workspace component whenever there is one, so its tree
|
||||
// lists the new chat right away, together with the progress of the transcription, instead
|
||||
// of only after the first message was sent:
|
||||
//
|
||||
await this.StoreAndListChatAsync(chat);
|
||||
await this.StoreAndListChatAsync(this.ChatThread);
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
await this.SyncForegroundChatAsync();
|
||||
return chat;
|
||||
return this.ChatThread;
|
||||
}
|
||||
|
||||
private async Task SendMessage(bool reuseLastUserPrompt = false)
|
||||
@@ -1492,8 +1377,9 @@ public partial class ChatComponent : MSGComponentBase
|
||||
// Create a new chat thread if necessary:
|
||||
if (this.ChatThread is null)
|
||||
{
|
||||
this.ChatThread = this.NewChatThread(this.ExtractThreadName(this.ComposerState.UserInput), await this.ResolveWorkspaceOfFirstThreadAsync());
|
||||
await this.PublishNewChatThreadAsync();
|
||||
this.ChatThread = this.NewChatThread(this.ExtractThreadName(this.ComposerState.UserInput));
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1690,17 +1576,12 @@ public partial class ChatComponent : MSGComponentBase
|
||||
/// Every way to start a new chat leads here: the buttons of the toolbar, a change of the chat
|
||||
/// template, and the workspace tree. So every new chat gets its chat template with the data
|
||||
/// sources, tools, and example conversation, its profile, and its provider in the same way, and
|
||||
/// the user is asked about unsaved changes in the same way.<br/><br/>
|
||||
///
|
||||
/// A new chat template takes over only once the user agreed to leave the open chat and that
|
||||
/// chat is stored or deleted. Declining leaves the open chat as it was, with its template, and
|
||||
/// the template it runs with stays the one whose preselections the next chat must not carry over.
|
||||
/// the user is asked about unsaved changes in the same way.
|
||||
/// </remarks>
|
||||
/// <param name="useSameWorkspace">True starts the new chat with a thread right away, in the workspace of the open chat. False starts it outside any workspace, and its thread comes with the first message. A chat which would belong to no workspace either way, while its chat template names one, also gets its thread only with the first message, in the workspace of the template.</param>
|
||||
/// <param name="useSameWorkspace">True starts the new chat with a thread right away, in the workspace of the open chat. False starts it outside any workspace, and its thread comes with the first message.</param>
|
||||
/// <param name="deletePreviousChat">Whether to delete the open chat first.</param>
|
||||
/// <param name="targetWorkspaceId">The workspace for the new chat, when the workspace tree asks for one. When given, the new chat gets a thread in that workspace, whatever useSameWorkspace says.</param>
|
||||
/// <param name="chatTemplate">The chat template of the new chat, when the user picked another one. Without one, the new chat keeps the template of the open chat.</param>
|
||||
private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false, Guid? targetWorkspaceId = null, ChatTemplate? chatTemplate = null)
|
||||
private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false, Guid? targetWorkspaceId = null)
|
||||
{
|
||||
//
|
||||
// Where the new chat goes is decided right away, when it is asked for. Deleting the open chat
|
||||
@@ -1708,9 +1589,6 @@ public partial class ChatComponent : MSGComponentBase
|
||||
//
|
||||
var workspaceOfNewChat = targetWorkspaceId ?? this.currentWorkspaceId;
|
||||
|
||||
// The template the profile and the provider of the open chat were chosen with:
|
||||
var previousChatTemplate = this.currentChatTemplate;
|
||||
|
||||
//
|
||||
// Want the user to manage the chat storage manually? In that case, we have to ask the user
|
||||
// about possible data loss:
|
||||
@@ -1772,11 +1650,6 @@ public partial class ChatComponent : MSGComponentBase
|
||||
//
|
||||
this.hasUnsavedChanges = false;
|
||||
this.ComposerState.Clear();
|
||||
|
||||
// The open chat is left now, so the new template takes over:
|
||||
if (chatTemplate is not null)
|
||||
this.currentChatTemplate = chatTemplate;
|
||||
|
||||
this.RefreshCurrentProfileAndChatTemplate();
|
||||
|
||||
//
|
||||
@@ -1800,26 +1673,11 @@ public partial class ChatComponent : MSGComponentBase
|
||||
break;
|
||||
}
|
||||
|
||||
// What the chat template preselects comes last, so that it wins over the behavior above:
|
||||
if (this.ApplyChatTemplateProfileAndProvider(previousChatTemplate))
|
||||
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||
|
||||
//
|
||||
// Reset the chat thread or create a new one. A target workspace always means a new chat in
|
||||
// that workspace, no matter what useSameWorkspace says:
|
||||
//
|
||||
var startsOutsideWorkspaces = !useSameWorkspace && targetWorkspaceId is null;
|
||||
|
||||
//
|
||||
// The workspace of the chat template fills in only where the new chat would belong to no
|
||||
// workspace otherwise. A workspace asked for on purpose wins: the one of the workspace list,
|
||||
// and the open one for the button that starts a new chat in it. Such a chat gets no thread
|
||||
// here: its first message creates one in that workspace, so that the workspace is created
|
||||
// only once there is something to keep, see ResolveWorkspaceOfFirstThreadAsync.
|
||||
//
|
||||
if (targetWorkspaceId is null && workspaceOfNewChat == Guid.Empty && this.SettingsManager.GetChatTemplateWorkspaceName(this.currentChatTemplate) is not null)
|
||||
startsOutsideWorkspaces = true;
|
||||
|
||||
if (startsOutsideWorkspaces)
|
||||
{
|
||||
//
|
||||
@@ -2059,10 +1917,6 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
this.Provider = this.SettingsManager.GetChatProviderForLoadedChat(chatProvider);
|
||||
|
||||
// A chat without a thread has not started yet, so it starts with what its chat template preselects:
|
||||
if (this.ChatThread is null)
|
||||
this.ApplyChatTemplateProfileAndProvider(this.currentChatTemplate);
|
||||
|
||||
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||
|
||||
// Try to select the profile:
|
||||
@@ -2200,7 +2054,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
// that, and it may even bring an example conversation along -- reporting nothing for all
|
||||
// of it would tell a person their window is empty while their first message is not.
|
||||
//
|
||||
var thread = this.ChatThread ?? this.NewChatThread(string.Empty, this.currentWorkspaceId);
|
||||
var thread = this.ChatThread ?? this.NewChatThread(string.Empty);
|
||||
var toolDefinitions = this.GetRunnableToolDefinitions(offersSemanticSearch);
|
||||
provider = this.Provider;
|
||||
parts = ConversationParts.Of(thread, this.BuildSystemPromptFor(thread, toolDefinitions), this.UserInput, this.ComposerState.FileAttachments, provider.SupportsImageInput(), toolDefinitions);
|
||||
@@ -2251,9 +2105,9 @@ public partial class ChatComponent : MSGComponentBase
|
||||
/// its static definition: the one a request offers lists the data sources as well, which only
|
||||
/// the request asks for.
|
||||
///
|
||||
/// Read Web Page, Search Confluence, Search Mails, and Semantic Search likewise count with their
|
||||
/// registered instructions, which assume the default settings and a chat which holds no internal
|
||||
/// content, and warn that internal content ends web search. A request words them after the actual settings, the chat,
|
||||
/// Read Web Page, Search Confluence, and Search Mails likewise count with their registered
|
||||
/// instructions, which assume the default settings and a chat which holds no internal content,
|
||||
/// and warn that internal content ends web search. A request words them after the actual settings, the chat,
|
||||
/// and the tools it offers, so the count comes out a few tokens off. One case it gets wrong as a
|
||||
/// whole: a chat restricted to the configured services only, with neither a wiki nor allowed
|
||||
/// private hosts configured, leaves Read Web Page nothing to offer, and only the request finds
|
||||
@@ -2299,22 +2153,18 @@ public partial class ChatComponent : MSGComponentBase
|
||||
/// The thread a new chat starts with, as the selections made so far decide it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The only place which builds a new thread: every way to start a chat comes here, and so does
|
||||
/// the token count, which has to measure the same thing they build. A count against a thread
|
||||
/// assembled differently from the one which is then sent would be wrong in exactly the moment a
|
||||
/// person looks at it: before they send their first message.
|
||||
///
|
||||
/// The workspace is no default of its own but always named by the caller, since that is the one
|
||||
/// part the ways to start a chat decide differently: StartNewChat decides it right away, and a
|
||||
/// chat which gets its thread with its first content asks ResolveWorkspaceOfFirstThreadAsync.
|
||||
/// In one place because three code paths used to write it out, and because the token count has
|
||||
/// to measure the same thing they build. A count against a thread assembled differently from
|
||||
/// the one which is then sent would be wrong in exactly the moment a person looks at it: before
|
||||
/// they send their first message.
|
||||
///
|
||||
/// The data source options are those chosen before the chat had a thread. Starting a new chat in
|
||||
/// a workspace replaces them right afterward with those its chat template asks for.
|
||||
/// </remarks>
|
||||
/// <param name="name">The name of the thread.</param>
|
||||
/// <param name="workspaceId">The workspace of the thread, or Guid.Empty for none.</param>
|
||||
/// <param name="workspaceId">The workspace of the thread. Without one, the thread goes to the workspace of the open chat.</param>
|
||||
/// <returns>The new thread.</returns>
|
||||
private ChatThread NewChatThread(string name, Guid workspaceId) => new()
|
||||
private ChatThread NewChatThread(string name, Guid? workspaceId = null) => new()
|
||||
{
|
||||
DataSourceOptions = this.earlyDataSourceOptions,
|
||||
IncludeDateTime = true,
|
||||
@@ -2323,7 +2173,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
SelectedChatTemplate = this.currentChatTemplate.Id,
|
||||
SelectedToolIds = [..this.selectedToolIds],
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
WorkspaceId = workspaceId,
|
||||
WorkspaceId = workspaceId ?? this.currentWorkspaceId,
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = name,
|
||||
Blocks = this.currentChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : this.currentChatTemplate.ExampleConversation.Select(block => block.DeepClone()).ToList(),
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
@inherits ConfigurationText
|
||||
|
||||
<FileExclusionEditor Text="@this.EditingText" TextChanged="@this.InternalUpdate" Label="@this.OptionDescription" Disabled="@this.IsDisabled" ShowHelp="@this.ShowHelp" ShowReset="@(this.ResetValue is not null)" ResetValue="@(this.ResetValue?.Invoke() ?? FileExclusions.DEFAULT_PATTERNS)" ResetButtonText="@this.ResetButtonText" ResetRequested="@this.ApplyTextAsync"/>
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Reuses ConfigurationText's persistence, validation, reset, and organization locks with the
|
||||
/// shared exclusion editor. The editor supplies its own outlined fields and heading.
|
||||
/// </summary>
|
||||
public partial class ConfigurationFileExclusions : ConfigurationText
|
||||
{
|
||||
[Parameter]
|
||||
public bool ShowHelp { get; set; } = true;
|
||||
|
||||
protected override Variant Variant => Variant.Text;
|
||||
|
||||
protected override string Label => string.Empty;
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.Text()"
|
||||
Text="@(this.ExpandToContent ? this.EditingText : this.Text())"
|
||||
TextChanged="@this.InternalUpdate"
|
||||
Disabled="@this.IsDisabled"
|
||||
Adornment="@this.IconAdornment"
|
||||
@@ -14,6 +14,7 @@
|
||||
Lines="@this.NumLines"
|
||||
AutoGrow="@this.AutoGrow"
|
||||
MaxLines="@this.GetMaxLines"
|
||||
Class="@this.InputClass"
|
||||
Immediate="@true"
|
||||
Validation="@this.Validation"
|
||||
Underline="false"
|
||||
@@ -25,20 +26,26 @@ else
|
||||
<div>
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.Text()"
|
||||
Text="@(this.ExpandToContent ? this.EditingText : this.Text())"
|
||||
TextChanged="@this.InternalUpdate"
|
||||
Disabled="@this.IsDisabled"
|
||||
Adornment="@this.IconAdornment"
|
||||
AdornmentIcon="@this.Icon"
|
||||
Adornment="@(this.ResetButtonIconOnly ? Adornment.End : this.IconAdornment)"
|
||||
AdornmentIcon="@(this.ResetButtonIconOnly ? Icons.Material.Filled.Restore : this.Icon)"
|
||||
OnAdornmentClick="@this.ResetAdornmentClick"
|
||||
AdornmentAriaLabel="@(this.ResetButtonIconOnly ? this.ResetButtonText : null)"
|
||||
AdornmentColor="@this.IconColor"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Lines="@this.NumLines"
|
||||
AutoGrow="@this.AutoGrow"
|
||||
MaxLines="@this.GetMaxLines"
|
||||
Class="@this.InputClass"
|
||||
Immediate="@true"
|
||||
Validation="@this.Validation"
|
||||
Underline="false"
|
||||
/>
|
||||
<RestoreDefaultButton Text="@this.ResetButtonText" Disabled="@this.IsDisabled" OnClick="@this.ResetTextAsync"/>
|
||||
@if (!this.ResetButtonIconOnly)
|
||||
{
|
||||
<RestoreDefaultButton Text="@this.ResetButtonText" Disabled="@this.IsDisabled" OnClick="@this.ResetTextAsync"/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
@@ -24,6 +25,18 @@ public partial class ConfigurationText : ConfigurationBaseCore
|
||||
[Parameter]
|
||||
public string Icon { get; set; } = Icons.Material.Filled.Info;
|
||||
|
||||
/// <summary>
|
||||
/// Removes the default bottom margin when the field is part of a compact group.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool Compact { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Wraps and grows to show the full text, including while the field is not focused.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ExpandToContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The color of the icon to use.
|
||||
/// </summary>
|
||||
@@ -54,6 +67,12 @@ public partial class ConfigurationText : ConfigurationBaseCore
|
||||
[Parameter]
|
||||
public string ResetButtonText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Uses an accessible reset icon inside the field.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ResetButtonIconOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Validates the configured text before it is stored.
|
||||
/// </summary>
|
||||
@@ -61,11 +80,15 @@ public partial class ConfigurationText : ConfigurationBaseCore
|
||||
public Func<string, string?>? Validation { get; set; }
|
||||
|
||||
private string internalText = string.Empty;
|
||||
private string? lastSourceText;
|
||||
|
||||
private readonly Timer timer = new(TimeSpan.FromMilliseconds(500))
|
||||
{
|
||||
AutoReset = false
|
||||
};
|
||||
|
||||
protected string EditingText => this.internalText;
|
||||
|
||||
#region Overrides of ConfigurationBase
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -73,6 +96,8 @@ public partial class ConfigurationText : ConfigurationBaseCore
|
||||
|
||||
protected override Variant Variant => Variant.Outlined;
|
||||
|
||||
protected override string MarginClass => this.Compact ? "mb-0" : base.MarginClass;
|
||||
|
||||
protected override string Label => this.OptionDescription;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -83,15 +108,24 @@ public partial class ConfigurationText : ConfigurationBaseCore
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
this.internalText = this.Text();
|
||||
var sourceText = this.Text();
|
||||
// Re-rendering the parent must not replace a draft which is waiting for its debounce.
|
||||
// A genuinely changed setting, e.g. from a configuration plugin, still replaces it.
|
||||
if (this.IsDisabled || !string.Equals(sourceText, this.lastSourceText, StringComparison.Ordinal))
|
||||
{
|
||||
this.internalText = sourceText;
|
||||
this.lastSourceText = sourceText;
|
||||
}
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool AutoGrow => this.NumLines > 1;
|
||||
private bool AutoGrow => this.ExpandToContent || this.NumLines > 1;
|
||||
|
||||
private int GetMaxLines => this.AutoGrow ? this.MaxLines : 1;
|
||||
private int GetMaxLines => this.ExpandToContent ? 0 : this.AutoGrow ? this.MaxLines : 1;
|
||||
|
||||
private string InputClass => this.ExpandToContent ? "pattern-text-field" : string.Empty;
|
||||
|
||||
//
|
||||
// MudBlazor centers the icon vertically, so in a field of several lines it floats in the middle
|
||||
@@ -99,29 +133,50 @@ public partial class ConfigurationText : ConfigurationBaseCore
|
||||
//
|
||||
private Adornment IconAdornment => this.NumLines == 1 ? Adornment.Start : Adornment.None;
|
||||
|
||||
private void InternalUpdate(string text)
|
||||
protected void InternalUpdate(string text)
|
||||
{
|
||||
if (this.IsDisabled)
|
||||
return;
|
||||
|
||||
this.timer.Stop();
|
||||
this.internalText = text;
|
||||
this.timer.Start();
|
||||
}
|
||||
|
||||
private async Task ResetTextAsync()
|
||||
private EventCallback<MouseEventArgs> ResetAdornmentClick => this.ResetButtonIconOnly
|
||||
? EventCallback.Factory.Create<MouseEventArgs>(this, this.ResetTextAsync)
|
||||
: default;
|
||||
|
||||
protected async Task ResetTextAsync()
|
||||
{
|
||||
if (this.ResetValue is null || this.IsDisabled)
|
||||
return;
|
||||
|
||||
await this.ApplyTextAsync(this.ResetValue());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies an explicit value immediately, including a reset of part of a compound setting.
|
||||
/// </summary>
|
||||
protected async Task ApplyTextAsync(string text)
|
||||
{
|
||||
if (this.IsDisabled)
|
||||
return;
|
||||
|
||||
this.timer.Stop();
|
||||
this.internalText = this.ResetValue();
|
||||
this.internalText = text;
|
||||
await this.OptionChanged(this.internalText);
|
||||
}
|
||||
|
||||
private async Task OptionChanged(string updatedText)
|
||||
protected async Task OptionChanged(string updatedText)
|
||||
{
|
||||
if (this.Validation?.Invoke(updatedText) is not null)
|
||||
if (this.IsDisabled || this.Validation?.Invoke(updatedText) is not null)
|
||||
return;
|
||||
|
||||
this.TextUpdate(updatedText);
|
||||
// A save may finish after another keystroke. Remember the value it applied before
|
||||
// awaiting storage, so its configuration-change notification cannot erase that keystroke.
|
||||
this.lastSourceText = this.Text();
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.InformAboutChange();
|
||||
}
|
||||
|
||||
@@ -270,8 +270,70 @@ public partial class DataSourceManagement : MSGComponentBase
|
||||
|
||||
private async Task EditDataSource(IDataSource dataSource)
|
||||
{
|
||||
if (await DataSourceEditing.EditAsync(this.DialogService, this.SettingsManager, this.DataSourceEmbeddingService, dataSource.Id))
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
if (dataSource.IsEnterpriseConfiguration)
|
||||
return;
|
||||
|
||||
IDataSource? editedDataSource = null;
|
||||
var lockDataSourceOrigin = dataSource is IInternalDataSource
|
||||
&& await this.DataSourceEmbeddingService.ShouldLockDataSourceOriginAsync(dataSource.Id);
|
||||
switch (dataSource)
|
||||
{
|
||||
case DataSourceLocalFile localFile:
|
||||
var localFileDialogParameters = new DialogParameters<DataSourceLocalFileDialog>
|
||||
{
|
||||
{ x => x.IsEditing, true },
|
||||
{ x => x.DataSource, localFile },
|
||||
{ x => x.LockSource, lockDataSourceOrigin },
|
||||
};
|
||||
|
||||
var localFileDialogReference = await this.DialogService.ShowAsync<DataSourceLocalFileDialog>(T("Edit Local File Data Source"), localFileDialogParameters, DialogOptions.FULLSCREEN);
|
||||
var localFileDialogResult = await localFileDialogReference.Result;
|
||||
if (localFileDialogResult is null || localFileDialogResult.Canceled)
|
||||
return;
|
||||
|
||||
editedDataSource = (DataSourceLocalFile)localFileDialogResult.Data!;
|
||||
break;
|
||||
|
||||
case DataSourceLocalDirectory localDirectory:
|
||||
var localDirectoryDialogParameters = new DialogParameters<DataSourceLocalDirectoryDialog>
|
||||
{
|
||||
{ x => x.IsEditing, true },
|
||||
{ x => x.DataSource, localDirectory },
|
||||
{ x => x.LockSource, lockDataSourceOrigin },
|
||||
};
|
||||
|
||||
var localDirectoryDialogReference = await this.DialogService.ShowAsync<DataSourceLocalDirectoryDialog>(T("Edit Local Directory Data Source"), localDirectoryDialogParameters, DialogOptions.FULLSCREEN);
|
||||
var localDirectoryDialogResult = await localDirectoryDialogReference.Result;
|
||||
if (localDirectoryDialogResult is null || localDirectoryDialogResult.Canceled)
|
||||
return;
|
||||
|
||||
editedDataSource = (DataSourceLocalDirectory)localDirectoryDialogResult.Data!;
|
||||
break;
|
||||
|
||||
case DataSourceERI_V1 eriDataSource:
|
||||
var eriDialogParameters = new DialogParameters<DataSourceERI_V1Dialog>
|
||||
{
|
||||
{ x => x.IsEditing, true },
|
||||
{ x => x.DataSource, eriDataSource },
|
||||
};
|
||||
|
||||
var eriDialogReference = await this.DialogService.ShowAsync<DataSourceERI_V1Dialog>(T("Edit ERI v1 Data Source"), eriDialogParameters, DialogOptions.FULLSCREEN);
|
||||
var eriDialogResult = await eriDialogReference.Result;
|
||||
if (eriDialogResult is null || eriDialogResult.Canceled)
|
||||
return;
|
||||
|
||||
editedDataSource = (DataSourceERI_V1)eriDialogResult.Data!;
|
||||
break;
|
||||
}
|
||||
|
||||
if(editedDataSource is null)
|
||||
return;
|
||||
|
||||
this.SettingsManager.ConfigurationData.DataSources[this.SettingsManager.ConfigurationData.DataSources.IndexOf(dataSource)] = editedDataSource;
|
||||
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.DataSourceEmbeddingService.QueueDataSourceAsync(editedDataSource);
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task DeleteDataSource(IDataSource dataSource)
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
@using AIStudio.Settings.DataModel
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<WorkspaceNameField WorkspaceName="@this.WorkspaceName" WorkspaceNameChanged="@this.SetWorkspaceName" SelectLabel="@T("Existing workspace (Optional)")" Label="@T("Workspace name (Optional)")" HelperText="@T("Choose an existing workspace or enter a name that should be created when the launcher is opened.")" AdornmentIcon="@(this.OpensTemporaryChat ? Icons.Material.Filled.Timer : Icons.Material.Filled.CreateNewFolder)" AdornmentIconSize="Size.Small" TextFieldClass="mb-1"/>
|
||||
@if (this.availableWorkspaces.Count > 0)
|
||||
{
|
||||
<MudSelect T="string" Value="@this.WorkspaceName" ValueChanged="@this.SelectExistingWorkspace" Strict="@false" Label="@T("Existing workspace (Optional)")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Folder">
|
||||
@foreach (var workspace in this.availableWorkspaces)
|
||||
{
|
||||
<MudSelectItem T="string" Value="@workspace.Name">@workspace.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
|
||||
<MudTextField T="string" Text="@this.WorkspaceName" TextChanged="@this.SetWorkspaceName" AdornmentIcon="@(this.OpensTemporaryChat ? Icons.Material.Filled.Timer : Icons.Material.Filled.CreateNewFolder)" Adornment="Adornment.Start" IconSize="Size.Small" Label="@T("Workspace name (Optional)")" HelperText="@T("Choose an existing workspace or enter a name that should be created when the launcher is opened.")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-1" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
|
||||
@* The tile behaves differently with and without a workspace, so the form says which of the two is
|
||||
chosen right now instead of only explaining that both are possible. *@
|
||||
@@ -10,12 +19,6 @@
|
||||
? T("Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings.")
|
||||
: T("The tile opens its chat in this workspace and creates the workspace when it does not exist yet."))
|
||||
</MudText>
|
||||
@if (this.OpensTemporaryChat && this.SettingsManager.GetChatTemplateWorkspaceName(this.SelectedChatTemplate) is { } templateWorkspaceName)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
|
||||
@string.Format(T("The chosen chat template keeps its chats in the workspace '{0}', so the tile opens its chat there instead. A workspace entered here wins over the one of the template."), templateWorkspaceName)
|
||||
</MudText>
|
||||
}
|
||||
<MudSelect T="string" Value="@this.ProviderId" ValueChanged="@this.SetProviderId" Label="@T("Chat provider")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.SmartToy">
|
||||
<MudSelectItem T="string" Value="@string.Empty">@T("Use chat default")</MudSelectItem>
|
||||
@foreach (var provider in this.SettingsManager.GetConfidentProviders(Components.CHAT))
|
||||
@@ -25,12 +28,6 @@
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@if (this.SelectedChatTemplate.ProviderId is not null)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
|
||||
@T("The chosen chat template preselects a provider of its own, and it wins over a selection made here as long as that provider can be used for chats.")
|
||||
</MudText>
|
||||
}
|
||||
<MudSelect T="string" Value="@this.ProfileId" ValueChanged="@this.SetProfileId" Label="@T("Chat profile")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person">
|
||||
<MudSelectItem T="string" Value="@string.Empty">@T("Use chat default")</MudSelectItem>
|
||||
<MudSelectItem T="string" Value="@Guid.Empty.ToString()">@T("Use no profile")</MudSelectItem>
|
||||
@@ -39,12 +36,6 @@
|
||||
<MudSelectItem T="string" Value="@profile.Id">@profile.GetSafeName()</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@if (this.SelectedChatTemplate is { AllowProfileUsage: true, ProfileId: not null })
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
|
||||
@T("The chosen chat template preselects a profile of its own, and it wins over a selection made here as long as that profile exists.")
|
||||
</MudText>
|
||||
}
|
||||
<MudSelect T="string" Value="@this.ChatTemplateId" ValueChanged="@this.SetChatTemplateId" Label="@T("Chat template")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Article">
|
||||
<MudSelectItem T="string" Value="@string.Empty">@T("Use chat default")</MudSelectItem>
|
||||
<MudSelectItem T="string" Value="@Guid.Empty.ToString()">@T("Use no chat template")</MudSelectItem>
|
||||
@@ -53,25 +44,20 @@
|
||||
<MudSelectItem T="string" Value="@chatTemplate.Id">@chatTemplate.GetSafeName()</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@* Without the RAG preview, the chat uses no data sources, so there is nothing to choose here. A
|
||||
selection made before stays with the launcher and counts once RAG is switched on again: *@
|
||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<MudSelect T="string" Label="@T("Data sources (Optional)")" MultiSelection="@true" SelectedValues="@this.DataSourceIds" SelectedValuesChanged="@this.SetDataSourceIds" MultiSelectionTextFunc="@this.GetSelectedDataSourceText" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@AppIcons.DATABASE">
|
||||
@foreach (var dataSource in this.SettingsManager.ConfigurationData.DataSources)
|
||||
{
|
||||
<MudSelectItem T="string" Value="@dataSource.Id">@dataSource.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@* A chat template wins over what is chosen here, so the form says so before somebody picks
|
||||
something which would never take effect: *@
|
||||
@if (this.SelectedChatTemplate.DataSourceOptions is not null)
|
||||
<MudSelect T="string" Label="@T("Data sources (Optional)")" MultiSelection="@true" SelectedValues="@this.DataSourceIds" SelectedValuesChanged="@this.SetDataSourceIds" MultiSelectionTextFunc="@this.GetSelectedDataSourceText" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@AppIcons.DATABASE">
|
||||
@foreach (var dataSource in this.SettingsManager.ConfigurationData.DataSources)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
|
||||
@T("The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own.")
|
||||
</MudText>
|
||||
<MudSelectItem T="string" Value="@dataSource.Id">@dataSource.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@* A chat template wins over what is chosen here, so the form says so before somebody picks
|
||||
something which would never take effect: *@
|
||||
@if (this.SelectedChatTemplate.DataSourceOptions is not null)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
|
||||
@T("The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own.")
|
||||
</MudText>
|
||||
}
|
||||
|
||||
<ToolSelectionField Component="Components.CHAT" SelectedToolIds="@this.ToolIds" SelectedToolIdsChanged="@this.SetToolIds" Label="@T("Tools (Optional)")" Help="@T("These tools are preselected when the chat opens. Users can change the selection in the chat, and every tool has to meet the confidence requirements of the provider in use.")"/>
|
||||
|
||||
@@ -96,6 +96,37 @@ public partial class DirectChatLauncherForm : MSGComponentBase
|
||||
? this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT)
|
||||
: this.SettingsManager.GetChatTemplateById(this.ChatTemplateId);
|
||||
|
||||
private IReadOnlyList<WorkspaceTreeWorkspace> availableWorkspaces = [];
|
||||
|
||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Configure the spellchecking for the workspace name input:
|
||||
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
var workspaceSnapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
|
||||
this.availableWorkspaces = workspaceSnapshot.Workspaces;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
//
|
||||
// Picking an existing workspace fills the name field. Clearing the select must not wipe a name
|
||||
// the user typed, though, so an empty selection is ignored:
|
||||
//
|
||||
private async Task SelectExistingWorkspace(string workspaceName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(workspaceName))
|
||||
return;
|
||||
|
||||
await this.SetWorkspaceName(workspaceName);
|
||||
}
|
||||
|
||||
private async Task SetWorkspaceName(string workspaceName)
|
||||
{
|
||||
this.WorkspaceName = workspaceName;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<div class="file-exclusion-editor mb-3">
|
||||
@if (this.ShowHeader || this.ShowHelp)
|
||||
{
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Spacing="1" Class="mb-1">
|
||||
@if (this.ShowHeader)
|
||||
{
|
||||
<MudText Typo="Typo.subtitle2" Class="flex-grow-1">@this.EditorLabel</MudText>
|
||||
}
|
||||
@if (this.ShowHelp)
|
||||
{
|
||||
<MudTooltip Text="@T("Exclusion help and examples")" RootStyle="display: inline-flex;">
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.HelpOutline" Color="@(this.showHelp ? Color.Info : Color.Default)" OnClick="@this.ToggleHelp" aria-label="@T("Exclusion help and examples")" aria-expanded="@(this.showHelp ? "true" : "false")"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
|
||||
<MudStack Spacing="1">
|
||||
@if (this.ReadOnly)
|
||||
{
|
||||
<TextInfoLines Icon="@Icons.Material.Filled.FolderOff" Label="@T("Excluded folders")" Value="@this.state.FolderText" Lines="1" MaxLines="0" ClipboardTooltipSubject="@T("the excluded folders")"/>
|
||||
<TextInfoLines Icon="@Icons.Material.Filled.FilterAltOff" Label="@T("Excluded files")" Value="@this.state.FileText" Lines="1" MaxLines="0" ClipboardTooltipSubject="@T("the excluded files")"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTextField T="string" InputId="@this.folderInputId" Text="@this.state.FolderText" TextChanged="@this.ChangeFoldersAsync" Immediate="@true" Disabled="@this.Disabled" Label="@T("Folders")" Lines="1" AutoGrow="@true" MaxLines="0" Class="pattern-text-field" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@FileExclusions.ValidateFolders" UserAttributes="@INPUT_ATTRIBUTES" Placeholder="node_modules/;bin/;cmake-build-*/" Adornment="@(this.ShowReset ? Adornment.End : Adornment.None)" AdornmentIcon="@Icons.Material.Filled.Restore" OnAdornmentClick="@(() => this.RestoreDefaultsAsync(true))" AdornmentAriaLabel="@this.FolderRestoreLabel"/>
|
||||
<MudTextField T="string" InputId="@this.fileInputId" Text="@this.state.FileText" TextChanged="@this.ChangeFilesAsync" Immediate="@true" Disabled="@this.Disabled" Label="@T("Files")" Lines="1" AutoGrow="@true" MaxLines="0" Class="pattern-text-field" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@FileExclusions.ValidateFiles" UserAttributes="@INPUT_ATTRIBUTES" Placeholder="*.min.js;*.webm;draft-*" Adornment="@(this.ShowReset ? Adornment.End : Adornment.None)" AdornmentIcon="@Icons.Material.Filled.Restore" OnAdornmentClick="@(() => this.RestoreDefaultsAsync(false))" AdornmentAriaLabel="@this.FileRestoreLabel"/>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
@if (!this.ReadOnly)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">@T("Separate patterns with semicolons. Folder patterns must end with /. Exclusions take priority.")</MudText>
|
||||
}
|
||||
<MudCollapse Expanded="@(this.ShowHelp && this.showHelp)">
|
||||
<MudSimpleTable Dense="@true" Elevation="0" Class="mt-2" aria-label="@T("Exclusion examples")">
|
||||
<tbody>
|
||||
<tr><td><code>*.pdf</code></td><td>@T("PDF files, regardless of letter case")</td></tr>
|
||||
<tr><td><code>draft-*</code></td><td>@T("File names starting with draft-")</td></tr>
|
||||
<tr><td><code>*build/</code></td><td>@T("Folders ending in build, such as build or app-build")</td></tr>
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
</MudCollapse>
|
||||
</div>
|
||||
@@ -0,0 +1,118 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A compact, two-group editor for the exclusion string shared by folder-based features.
|
||||
/// </summary>
|
||||
public partial class FileExclusionEditor : MSGComponentBase
|
||||
{
|
||||
private static readonly Dictionary<string, object?> INPUT_ATTRIBUTES = new()
|
||||
{
|
||||
["spellcheck"] = false,
|
||||
["autocorrect"] = "off",
|
||||
["autocapitalize"] = "off",
|
||||
};
|
||||
|
||||
[Parameter]
|
||||
public string? Text { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> TextChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Displays both groups with copy buttons instead of editing and reset controls.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ReadOnly { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool ShowReset { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public bool ShowHelp { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public bool ShowHeader { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public string ResetValue { get; set; } = FileExclusions.DEFAULT_PATTERNS;
|
||||
|
||||
[Parameter]
|
||||
public string ResetButtonText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Immediately persists the combined value after restoring just one of its groups.
|
||||
/// Ordinary edits continue through TextChanged.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<string> ResetRequested { get; set; }
|
||||
|
||||
private readonly FileExclusionEditorState state = new();
|
||||
private readonly string folderInputId = $"exclusion-folders-{Guid.NewGuid():N}";
|
||||
private readonly string fileInputId = $"exclusion-files-{Guid.NewGuid():N}";
|
||||
private bool showHelp;
|
||||
|
||||
private string EditorLabel => string.IsNullOrWhiteSpace(this.Label) ? T("Excluded files and folders") : this.Label;
|
||||
|
||||
private string RestoreLabel => string.IsNullOrWhiteSpace(this.ResetButtonText) ? T("Restore default") : this.ResetButtonText;
|
||||
|
||||
private string FolderRestoreLabel => $"{this.RestoreLabel}: {T("Folders")}";
|
||||
|
||||
private string FileRestoreLabel => $"{this.RestoreLabel}: {T("Files")}";
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
this.state.Receive(this.Text, discardDraft: this.Disabled || this.ReadOnly);
|
||||
base.OnParametersSet();
|
||||
}
|
||||
|
||||
protected Task ChangeFoldersAsync(string text) => this.ChangePatternsAsync(text, folders: true);
|
||||
|
||||
protected Task ChangeFilesAsync(string text) => this.ChangePatternsAsync(text, folders: false);
|
||||
|
||||
private async Task ChangePatternsAsync(string text, bool folders)
|
||||
{
|
||||
if (this.Disabled || this.ReadOnly)
|
||||
return;
|
||||
|
||||
var previousText = this.state.Text;
|
||||
var wasValid = this.state.IsValid;
|
||||
if (folders)
|
||||
this.state.EditFolders(text);
|
||||
else
|
||||
this.state.EditFiles(text);
|
||||
|
||||
// Moving a rule to the correct field can keep the combined text identical. It must still
|
||||
// publish the now-valid draft, because the previous, invalid arrangement was not sent.
|
||||
if (this.state.IsValid && (!wasValid || !string.Equals(previousText, this.state.Text, StringComparison.Ordinal)))
|
||||
await this.TextChanged.InvokeAsync(this.state.Text);
|
||||
}
|
||||
|
||||
protected async Task RestoreDefaultsAsync(bool folders)
|
||||
{
|
||||
if (this.Disabled || this.ReadOnly || !this.ShowReset)
|
||||
return;
|
||||
|
||||
if (folders)
|
||||
this.state.RestoreFolders(this.ResetValue);
|
||||
else
|
||||
this.state.RestoreFiles(this.ResetValue);
|
||||
|
||||
if (!this.state.IsValid)
|
||||
return;
|
||||
|
||||
if (this.ResetRequested.HasDelegate)
|
||||
await this.ResetRequested.InvokeAsync(this.state.Text);
|
||||
else
|
||||
await this.TextChanged.InvokeAsync(this.state.Text);
|
||||
}
|
||||
|
||||
private void ToggleHelp() => this.showHelp = !this.showHelp;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Presents the existing exclusion string as two editable groups without losing the other group
|
||||
/// or letting delayed parameter echoes overwrite newer edits.
|
||||
/// </summary>
|
||||
internal sealed class FileExclusionEditorState
|
||||
{
|
||||
private const string PATTERN_SEPARATOR = "; ";
|
||||
|
||||
private string? receivedText;
|
||||
private readonly HashSet<string> pendingValues = new(StringComparer.Ordinal);
|
||||
|
||||
public string Text { get; private set; } = string.Empty;
|
||||
|
||||
public string FolderText { get; private set; } = string.Empty;
|
||||
|
||||
public string FileText { get; private set; } = string.Empty;
|
||||
|
||||
public bool IsValid => FileExclusions.IsValidFolders(this.FolderText) && FileExclusions.IsValidFiles(this.FileText);
|
||||
|
||||
public bool Receive(string? text, bool discardDraft = false)
|
||||
{
|
||||
text ??= string.Empty;
|
||||
if (discardDraft)
|
||||
{
|
||||
var changed = !string.Equals(text, this.Text, StringComparison.Ordinal);
|
||||
this.receivedText = text;
|
||||
this.Reset(text);
|
||||
return changed;
|
||||
}
|
||||
|
||||
if (string.Equals(text, this.receivedText, StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
this.receivedText = text;
|
||||
if (string.Equals(text, this.Text, StringComparison.Ordinal))
|
||||
{
|
||||
this.pendingValues.Clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.pendingValues.Remove(text))
|
||||
return false;
|
||||
|
||||
this.Reset(text);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset(string text)
|
||||
{
|
||||
this.pendingValues.Clear();
|
||||
this.Text = text;
|
||||
this.Regroup();
|
||||
}
|
||||
|
||||
public void EditFolders(string? text)
|
||||
{
|
||||
this.FolderText = text ?? string.Empty;
|
||||
this.UpdateText();
|
||||
}
|
||||
|
||||
public void EditFiles(string? text)
|
||||
{
|
||||
this.FileText = text ?? string.Empty;
|
||||
this.UpdateText();
|
||||
}
|
||||
|
||||
public void RestoreFolders(string defaults) => this.EditFolders(string.Join(PATTERN_SEPARATOR, FileExclusions.Parse(defaults).Where(IsFolder)));
|
||||
|
||||
public void RestoreFiles(string defaults) => this.EditFiles(string.Join(PATTERN_SEPARATOR, FileExclusions.Parse(defaults).Where(pattern => !IsFolder(pattern))));
|
||||
|
||||
private void Regroup()
|
||||
{
|
||||
var patterns = FileExclusions.Parse(this.Text);
|
||||
this.FolderText = string.Join(PATTERN_SEPARATOR, patterns.Where(IsFolder));
|
||||
this.FileText = string.Join(PATTERN_SEPARATOR, patterns.Where(pattern => !IsFolder(pattern)));
|
||||
}
|
||||
|
||||
private static bool IsFolder(string pattern) => pattern.EndsWith(FileExclusions.FOLDER_SUFFIX);
|
||||
|
||||
private void UpdateText()
|
||||
{
|
||||
this.Text = string.Join(PATTERN_SEPARATOR, FileExclusions.Parse(this.FolderText)
|
||||
.Concat(FileExclusions.Parse(this.FileText))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
this.pendingValues.Add(this.Text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudStack Spacing="1" Class="mb-3">
|
||||
<div style="@PATTERN_ROW_STYLE">
|
||||
<div style="min-width: 0;">
|
||||
@this.AllowedContent
|
||||
</div>
|
||||
<MudTooltip Text="@T("Pattern help and examples")" RootStyle="display: inline-flex;">
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.HelpOutline" Color="@(this.showHelp ? Color.Info : Color.Default)" OnClick="@this.ToggleHelp" aria-label="@T("Pattern help and examples")" aria-expanded="@(this.showHelp ? "true" : "false")"/>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
@if (this.HasExclusions)
|
||||
{
|
||||
<div style="@PATTERN_ROW_STYLE">
|
||||
<div style="grid-column: 1; min-width: 0;" class="mt-3">
|
||||
@this.ExcludedContent
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<MudCollapse Expanded="@this.showHelp">
|
||||
<div style="@PATTERN_ROW_STYLE">
|
||||
<MudPaper Outlined="@true" Class="pa-3" Elevation="0" Style="grid-column: 1; min-width: 0;">
|
||||
<MudText Typo="Typo.subtitle2">@T("Pattern examples")</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">@T("Separate patterns with semicolons. Exclusions take priority over included files.")</MudText>
|
||||
<MudSimpleTable Dense="@true" Elevation="0" aria-label="@T("Pattern examples")">
|
||||
<tbody>
|
||||
<tr><td><code>*.pdf;*.docx</code></td><td>@T("PDF and Word files")</td></tr>
|
||||
<tr><td><code>:documents:</code></td><td>@T("Include all documents and readable text files")</td></tr>
|
||||
<tr><td><code>*</code></td><td>@T("Include all files, including audio and video")</td></tr>
|
||||
<tr><td><code>:text:</code></td><td>@T("Include readable text files, including files without an extension")</td></tr>
|
||||
<tr><td><code>draft-*</code></td><td>@T("File names starting with draft-")</td></tr>
|
||||
@if (this.HasExclusions)
|
||||
{
|
||||
<tr><td><code>*build/</code></td><td>@T("Exclude folders ending in build, such as build or app-build")</td></tr>
|
||||
}
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-2">@T("Patterns match file names regardless of letter case. Folder paths are not supported.")</MudText>
|
||||
</MudPaper>
|
||||
</div>
|
||||
</MudCollapse>
|
||||
</MudStack>
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A file-pattern field with shared, collapsible help and optional, always-visible exclusions.
|
||||
/// Callers supply their existing fields, preserving validation and managed-configuration locks.
|
||||
/// </summary>
|
||||
public partial class FilePatternSelection : MSGComponentBase
|
||||
{
|
||||
private const string PATTERN_ROW_STYLE = "display: grid; grid-template-columns: minmax(0, 1fr) 48px; column-gap: 8px; align-items: center;";
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public RenderFragment AllowedContent { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// When supplied, shows the existing exclusion field below the inclusion field.
|
||||
/// Both groups stay visible and use the same help control.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment? ExcludedContent { get; set; }
|
||||
|
||||
internal bool HasExclusions => this.ExcludedContent is not null;
|
||||
|
||||
private bool showHelp;
|
||||
|
||||
private void ToggleHelp() => this.showHelp = !this.showHelp;
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudStack Row="false" Class="mb-3 pa-3 border-dashed border rounded-lg">
|
||||
<MudJustifiedText Typo="Typo.body1">
|
||||
@this.DetermineIntroText()
|
||||
</MudJustifiedText>
|
||||
<MudButton Color="@this.DetermineButtonColor()" Variant="Variant.Filled" OnClick="() => this.ShowPandocDialogAsync()">
|
||||
@this.DetermineButtonText()
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
@if (ShouldShowInstallationPrompt(this.pandocInstallation))
|
||||
{
|
||||
<MudStack Row="false" Class="mb-3 pa-3 border-dashed border rounded-lg">
|
||||
<MudJustifiedText Typo="Typo.body2">
|
||||
@this.DetermineIntroText()
|
||||
</MudJustifiedText>
|
||||
<MudButton Color="@this.DetermineButtonColor()" Variant="Variant.Filled" OnClick="() => this.ShowPandocDialogAsync()">
|
||||
@this.DetermineButtonText()
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
}
|
||||
@@ -18,7 +18,10 @@ public partial class ManagePandocDependency : MSGComponentBase
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
private PandocInstallation pandocInstallation;
|
||||
// Null means the silent availability check has not completed yet.
|
||||
private PandocInstallation? pandocInstallation;
|
||||
|
||||
internal static bool ShouldShowInstallationPrompt(PandocInstallation? installation) => installation is { IsAvailable: false };
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
@@ -32,10 +35,10 @@ public partial class ManagePandocDependency : MSGComponentBase
|
||||
|
||||
private Color DetermineButtonColor()
|
||||
{
|
||||
if (this.pandocInstallation == default)
|
||||
if (this.pandocInstallation is null)
|
||||
return Color.Default;
|
||||
|
||||
switch (this.pandocInstallation)
|
||||
switch (this.pandocInstallation.Value)
|
||||
{
|
||||
case { CheckWasSuccessful: true, IsAvailable: true }:
|
||||
return Color.Default;
|
||||
@@ -50,10 +53,10 @@ public partial class ManagePandocDependency : MSGComponentBase
|
||||
|
||||
private string DetermineButtonText()
|
||||
{
|
||||
if(this.pandocInstallation == default)
|
||||
if(this.pandocInstallation is null)
|
||||
return T("Please wait while we check the availability of Pandoc.");
|
||||
|
||||
switch (this.pandocInstallation)
|
||||
switch (this.pandocInstallation.Value)
|
||||
{
|
||||
case { CheckWasSuccessful: true, IsAvailable: true }:
|
||||
return T("Check your Pandoc installation");
|
||||
@@ -68,10 +71,10 @@ public partial class ManagePandocDependency : MSGComponentBase
|
||||
|
||||
private string DetermineIntroText()
|
||||
{
|
||||
if (this.pandocInstallation == default)
|
||||
if (this.pandocInstallation is null)
|
||||
return $"{this.IntroText} {T("Please wait while we check the availability of Pandoc.")}";
|
||||
|
||||
switch (this.pandocInstallation)
|
||||
switch (this.pandocInstallation.Value)
|
||||
{
|
||||
case { CheckWasSuccessful: true, IsAvailable: true }:
|
||||
return $"{this.IntroText} {T("Your Pandoc installation meets the requirements.")}";
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
@using AIStudio.Settings
|
||||
@using AIStudio.Settings.DataModel
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudSelect T="OutboundDataRestriction" Value="@this.Value" ValueChanged="@this.ValueChanged" Text="@this.Value.GetName()" Label="@this.Label" Class="mb-1" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Shield">
|
||||
@foreach (var restriction in ConfigurationSelectDataFactory.GetOutboundDataRestrictionData(this.Minimum))
|
||||
{
|
||||
<MudSelectItem T="OutboundDataRestriction" Value="@restriction.Value">
|
||||
@restriction.Name
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-1">
|
||||
@this.Value.GetDescription()
|
||||
</MudJustifiedText>
|
||||
@if (this.LimitationText is { } limitationText)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-1">
|
||||
@limitationText
|
||||
</MudJustifiedText>
|
||||
}
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-6">
|
||||
@this.Explanation
|
||||
</MudJustifiedText>
|
||||
@@ -1,72 +0,0 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Lets the user choose where a chat may still send data once it has read from a mailbox or a data
|
||||
/// source, and explains the choice.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only the levels allowed are offered: an organization may rule out the less strict ones, and so
|
||||
/// does a data source which only self-hosted providers may use, see
|
||||
/// DataSourceOutboundDataExtensions.GetMinimumOutboundDataRestriction. The dialog keeps the value it
|
||||
/// passes within those, since it stores it.
|
||||
/// </remarks>
|
||||
public partial class OutboundDataRestrictionSelection : MSGComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The level chosen.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public OutboundDataRestriction Value { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<OutboundDataRestriction> ValueChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The label of the setting. The blocked message of a tool quotes it, so the user finds the
|
||||
/// setting by the words they read there, see OutboundDataRequirementExtensions.GetToolBlockedMessage.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Why the setting matters for this kind of source.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Explanation { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The least strict level the organization allows for this kind of source.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public OutboundDataRestriction OrganizationMinimum { get; set; } = OutboundDataRestriction.UNRESTRICTED;
|
||||
|
||||
/// <summary>
|
||||
/// The security policy of the data source, or NOT_SPECIFIED for a source which has none.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public DataSourceSecurity SecurityPolicy { get; set; } = DataSourceSecurity.NOT_SPECIFIED;
|
||||
|
||||
private OutboundDataRestriction Minimum => DataSourceOutboundDataExtensions.GetMinimumOutboundDataRestriction(this.SecurityPolicy, this.OrganizationMinimum);
|
||||
|
||||
/// <summary>
|
||||
/// Why less strict levels are not offered, or null when every level is.
|
||||
/// </summary>
|
||||
private string? LimitationText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.SecurityPolicy is DataSourceSecurity.SELF_HOSTED)
|
||||
return T("Only self-hosted providers may use this data source. A chat which found something in it may therefore only use the services configured in AI Studio, such as the allowed private hosts of the tool 'Read Web Page'.");
|
||||
|
||||
if (this.OrganizationMinimum is not OutboundDataRestriction.UNRESTRICTED)
|
||||
return T("Your organization does not allow less strict settings than the ones offered here.");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,25 +76,21 @@ public partial class ProviderSelection : MSGComponentBase
|
||||
/// Says why there is nothing to choose from, or nothing at all when that is not the user's doing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An empty list has three causes the user can act on, and they lead to different places in the
|
||||
/// settings: there is no provider yet, the organization prohibited all of them, or none of the
|
||||
/// configured ones reaches the confidence this component asks for. Naming the wrong one sends
|
||||
/// the user looking in the wrong place -- a first start has nobody to blame for a confidence
|
||||
/// level it never set. A missing or invalid component is another case and none of those: it is
|
||||
/// a defect, it was logged as one, and any explanation offered to the user here would be a guess.
|
||||
/// An empty list has two causes the user can act on, and they lead to different places in the
|
||||
/// settings: there is no provider yet, or none of the configured ones reaches the confidence
|
||||
/// this component asks for. Naming the wrong one sends the user looking in the wrong place --
|
||||
/// a first start has nobody to blame for a confidence level it never set. A missing or invalid
|
||||
/// component is a third case and neither of those: it is a defect, it was logged as one, and
|
||||
/// any explanation offered to the user here would be a guess.
|
||||
/// </remarks>
|
||||
private string? GetEmptySelectionHint()
|
||||
{
|
||||
if (this.Component is null or Tools.Components.NONE)
|
||||
return null;
|
||||
|
||||
var configuredProviders = this.SettingsManager.GetAllProviders().Where(x => x.UsedLLMProvider is not LLMProviders.NONE).ToList();
|
||||
if (configuredProviders.Count == 0)
|
||||
if (!this.SettingsManager.GetAllProviders().Any(x => x.UsedLLMProvider is not LLMProviders.NONE))
|
||||
return this.T("No LLM providers are configured yet. Add a provider in the app settings.");
|
||||
|
||||
if (configuredProviders.All(x => this.SettingsManager.IsProviderProhibited(x.UsedLLMProvider)))
|
||||
return this.T("The use of all your LLM providers has been prohibited by your organization. Add a different provider in the app settings.");
|
||||
|
||||
return this.T("No LLM providers meet the confidence requirements. Configure an eligible provider in the app settings.");
|
||||
}
|
||||
|
||||
@@ -170,17 +166,6 @@ public partial class ProviderSelection : MSGComponentBase
|
||||
await this.ProviderSettingsChanged.InvokeAsync(updatedProvider);
|
||||
}
|
||||
|
||||
//
|
||||
// A configuration may also take our provider out of reach, e.g., when the organization
|
||||
// prohibits it or raises the minimum confidence. The list already leaves it out, so the
|
||||
// selection must not keep showing it either:
|
||||
//
|
||||
if (this.Component is not (null or Tools.Components.NONE) && this.ProviderSettings != AIStudio.Settings.Provider.NONE && !this.GetAvailableProviders().Contains(this.ProviderSettings))
|
||||
{
|
||||
this.ProviderSettings = AIStudio.Settings.Provider.NONE;
|
||||
await this.ProviderSettingsChanged.InvokeAsync(AIStudio.Settings.Provider.NONE);
|
||||
}
|
||||
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Rust;
|
||||
@@ -104,7 +105,7 @@ public partial class SettingsPanelApp : SettingsPanelBase
|
||||
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(Tools.Components.APP_SETTINGS);
|
||||
foreach (var provider in this.SettingsManager.GetAllTranscriptionProviders())
|
||||
{
|
||||
if (this.SettingsManager.IsProviderUsable(provider.UsedLLMProvider, minimumLevel))
|
||||
if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
|
||||
yield return new(provider.Name, provider.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,15 +39,9 @@
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudMarkdown Value="@context.GetConfidence(this.SettingsManager).Description" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||
@if (this.SettingsManager.IsProviderProhibited(context))
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Dense="@true" Class="mt-2">
|
||||
@T("The use of this provider has been prohibited by your organization. Therefore, it is untrusted, and you cannot change its confidence level.")
|
||||
</MudAlert>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd Style="vertical-align: top;">
|
||||
<MudMenu StartIcon="@Icons.Material.Filled.Security" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.GetCurrentConfidenceLevelName(context)" Variant="Variant.Filled" Style="@this.SetCurrentConfidenceLevelColorStyle(context)" Disabled="@this.IsConfidenceLevelLocked(context)">
|
||||
<MudMenu StartIcon="@Icons.Material.Filled.Security" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.GetCurrentConfidenceLevelName(context)" Variant="Variant.Filled" Style="@this.SetCurrentConfidenceLevelColorStyle(context)" Disabled="@this.IsCustomConfidenceSchemeLocked()">
|
||||
@foreach (var confidenceLevel in Enum.GetValues<ConfidenceLevel>().OrderBy(n => n))
|
||||
{
|
||||
if(confidenceLevel is ConfidenceLevel.NONE or ConfidenceLevel.UNKNOWN)
|
||||
|
||||
@@ -7,7 +7,7 @@ public partial class SettingsPanelConfidence : SettingsPanelBase
|
||||
{
|
||||
private string GetCurrentConfidenceLevelName(LLMProviders llmProvider)
|
||||
{
|
||||
if (this.TryGetShownConfidenceLevel(llmProvider, out var level))
|
||||
if (this.SettingsManager.ConfigurationData.Confidence.CustomConfidenceScheme.TryGetValue(llmProvider, out var level))
|
||||
return level.GetName();
|
||||
|
||||
return T("Not yet configured");
|
||||
@@ -15,41 +15,20 @@ public partial class SettingsPanelConfidence : SettingsPanelBase
|
||||
|
||||
private string SetCurrentConfidenceLevelColorStyle(LLMProviders llmProvider)
|
||||
{
|
||||
if (this.TryGetShownConfidenceLevel(llmProvider, out var level))
|
||||
if (this.SettingsManager.ConfigurationData.Confidence.CustomConfidenceScheme.TryGetValue(llmProvider, out var level))
|
||||
return $"background-color: {level.GetColor(this.SettingsManager)};";
|
||||
|
||||
return $"background-color: {ConfidenceLevel.UNKNOWN.GetColor(this.SettingsManager)};";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the confidence level the table shows for a provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A prohibited provider is untrusted, whatever level was chosen for it, see
|
||||
/// SettingsManager.GetConfiguredConfidenceLevel. The chosen level stays stored, so the table
|
||||
/// shows it again once the prohibition ends.
|
||||
/// </remarks>
|
||||
private bool TryGetShownConfidenceLevel(LLMProviders llmProvider, out ConfidenceLevel level)
|
||||
{
|
||||
if (this.SettingsManager.IsProviderProhibited(llmProvider))
|
||||
{
|
||||
level = ConfidenceLevel.UNTRUSTED;
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.SettingsManager.ConfigurationData.Confidence.CustomConfidenceScheme.TryGetValue(llmProvider, out level);
|
||||
}
|
||||
|
||||
private bool IsCustomConfidenceSchemeLocked()
|
||||
{
|
||||
return ManagedConfiguration.TryGet(x => x.Confidence, x => x.CustomConfidenceScheme, out var meta) && meta.IsLocked;
|
||||
}
|
||||
|
||||
private bool IsConfidenceLevelLocked(LLMProviders llmProvider) => this.IsCustomConfidenceSchemeLocked() || this.SettingsManager.IsProviderProhibited(llmProvider);
|
||||
|
||||
private async Task ChangeCustomConfidenceLevel(LLMProviders llmProvider, ConfidenceLevel level)
|
||||
{
|
||||
if (this.IsConfidenceLevelLocked(llmProvider))
|
||||
if (this.IsCustomConfidenceSchemeLocked())
|
||||
return;
|
||||
|
||||
this.SettingsManager.ConfigurationData.Confidence.CustomConfidenceScheme[llmProvider] = level;
|
||||
|
||||
@@ -44,12 +44,6 @@
|
||||
|
||||
<MudTd>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
|
||||
@if (this.SettingsManager.IsProviderProhibited(context.UsedLLMProvider))
|
||||
{
|
||||
<MudTooltip Text="@T("The use of this embedding provider has been prohibited by your organization.")">
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Block" Disabled="true"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (context.IsTrustedForDataSourceSecurityChecks(this.SettingsManager))
|
||||
{
|
||||
<MudTooltip Text="@(context.IsSelfHosted ? T("This self-hosted embedding provider is trusted for data source security checks. Local data can be sent to it without security warnings.") : T("This embedding provider is trusted by your organization for data source security checks. Local data can be sent to it without security warnings."))">
|
||||
@@ -68,7 +62,7 @@
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Key" OnClick="@(() => this.EditEmbeddingProvider(context))"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Test")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Api" OnClick="@(() => this.TestEmbeddingProvider(context))" Disabled="@this.SettingsManager.IsProviderProhibited(context.UsedLLMProvider)"/>
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Api" OnClick="@(() => this.TestEmbeddingProvider(context))"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
@@ -84,7 +78,7 @@
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteEmbeddingProvider(context))"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Test")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Api" OnClick="@(() => this.TestEmbeddingProvider(context))" Disabled="@this.SettingsManager.IsProviderProhibited(context.UsedLLMProvider)"/>
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Api" OnClick="@(() => this.TestEmbeddingProvider(context))"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
@@ -36,12 +36,6 @@
|
||||
<MudTd>@this.GetLLMProviderModelName(context)</MudTd>
|
||||
<MudTd>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
|
||||
@if (this.SettingsManager.IsProviderProhibited(context.UsedLLMProvider))
|
||||
{
|
||||
<MudTooltip Text="@T("The use of this provider has been prohibited by your organization.")">
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Block" Disabled="true"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (context.IsTrustedForDataSourceSecurityChecks(this.SettingsManager))
|
||||
{
|
||||
<MudTooltip Text="@(context.IsSelfHosted ? T("This self-hosted provider is trusted for data source security checks.") : T("This provider is trusted by your organization for data source security checks."))">
|
||||
|
||||
@@ -41,12 +41,6 @@
|
||||
|
||||
<MudTd>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
|
||||
@if (this.SettingsManager.IsProviderProhibited(context.UsedLLMProvider))
|
||||
{
|
||||
<MudTooltip Text="@T("The use of this transcription provider has been prohibited by your organization.")">
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Block" Disabled="true"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (context.IsTrustedForDataSourceSecurityChecks(this.SettingsManager))
|
||||
{
|
||||
<MudTooltip Text="@(context.IsSelfHosted ? T("This self-hosted transcription provider is trusted for data source security checks.") : T("This transcription provider is trusted by your organization for data source security checks."))">
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
Text="@this.Value"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Lines="3"
|
||||
Lines="@this.Lines"
|
||||
MaxLines="@this.MaxLines"
|
||||
AutoGrow="@true"
|
||||
Adornment="@(string.IsNullOrEmpty(this.Icon) ? Adornment.None : Adornment.Start)"
|
||||
AdornmentIcon="@this.Icon"
|
||||
Style="@this.GetColor()"
|
||||
UserAttributes="@USER_INPUT_ATTRIBUTES" />
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ public partial class TextInfoLines : MSGComponentBase
|
||||
[Parameter]
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
@@ -18,6 +21,9 @@ public partial class TextInfoLines : MSGComponentBase
|
||||
[Parameter]
|
||||
public int MaxLines { get; set; } = 30;
|
||||
|
||||
[Parameter]
|
||||
public int Lines { get; set; } = 3;
|
||||
|
||||
[Parameter]
|
||||
public bool ShowingCopyButton { get; set; } = true;
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
@if (this.availableWorkspaces.Count > 0 && !this.ReadOnly)
|
||||
{
|
||||
<MudSelect T="string" Value="@this.WorkspaceName" ValueChanged="@this.SelectExistingWorkspace" Strict="@false" Label="@this.SelectLabel" Variant="Variant.Outlined" Margin="Margin.Dense" Class="@this.SelectClass" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Folder">
|
||||
@foreach (var workspace in this.availableWorkspaces)
|
||||
{
|
||||
<MudSelectItem T="string" Value="@workspace.Name">@workspace.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
|
||||
<MudTextField T="string" Text="@this.WorkspaceName" TextChanged="@this.SetWorkspaceName" Label="@this.Label" HelperText="@this.HelperText" Validation="@this.Validation" Immediate="@this.Immediate" ReadOnly="@this.ReadOnly" Variant="Variant.Outlined" Margin="Margin.Dense" Class="@this.TextFieldClass" Adornment="Adornment.Start" AdornmentIcon="@this.AdornmentIcon" IconSize="@this.AdornmentIconSize" UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
@@ -1,107 +0,0 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Names a workspace by its name: an existing one to pick from, or a new name to type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Assistant chat launchers and chat templates both name their workspace this way, because a
|
||||
/// workspace has an ID of its own on every computer, and both create it when it does not exist
|
||||
/// yet. The field offers the existing workspaces only as a shortcut; the name it hands out is
|
||||
/// whatever the text field says. All texts come from the form around it, which knows what the
|
||||
/// name is used for.
|
||||
/// </remarks>
|
||||
public partial class WorkspaceNameField : MSGComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the workspace. An empty name is up to the form around the field, e.g., a
|
||||
/// disappearing chat for an assistant chat launcher.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string WorkspaceName { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> WorkspaceNameChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The label of the selection of existing workspaces.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string SelectLabel { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The label of the text field for the name.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string HelperText { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string AdornmentIcon { get; set; } = Icons.Material.Filled.CreateNewFolder;
|
||||
|
||||
[Parameter]
|
||||
public Size AdornmentIconSize { get; set; } = Size.Medium;
|
||||
|
||||
/// <summary>
|
||||
/// The validation of the name, or null for none.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<string, string?>? Validation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether every keystroke counts as a change, e.g., so that a validation follows the typing.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool Immediate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Shows the name without letting the user change it. The selection of existing workspaces
|
||||
/// is left out then, since picking from it would change the name.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ReadOnly { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string SelectClass { get; set; } = "mb-3 rounded-lg";
|
||||
|
||||
[Parameter]
|
||||
public string TextFieldClass { get; set; } = "mb-3";
|
||||
|
||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||
|
||||
private IReadOnlyList<WorkspaceTreeWorkspace> availableWorkspaces = [];
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
var workspaceSnapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
|
||||
this.availableWorkspaces = workspaceSnapshot.Workspaces;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
//
|
||||
// Picking an existing workspace fills the name field. Clearing the select must not wipe a name
|
||||
// the user typed, though, so an empty selection is ignored:
|
||||
//
|
||||
private async Task SelectExistingWorkspace(string workspaceName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(workspaceName))
|
||||
return;
|
||||
|
||||
await this.SetWorkspaceName(workspaceName);
|
||||
}
|
||||
|
||||
private async Task SetWorkspaceName(string workspaceName)
|
||||
{
|
||||
this.WorkspaceName = workspaceName;
|
||||
await this.WorkspaceNameChanged.InvokeAsync(workspaceName);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
@using AIStudio.Chat
|
||||
@using AIStudio.Settings
|
||||
@using AIStudio.Settings.DataModel
|
||||
@inherits MSGComponentBase
|
||||
|
||||
@@ -137,73 +136,6 @@
|
||||
@T("Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here.")
|
||||
</MudJustifiedText>
|
||||
<MudTextSwitch @bind-Value="@this.AllowProfileUsage" Color="Color.Primary" Label="@T("Allow the use of profiles together with this chat template?")" LabelOn="@T("Yes, allow profiles when using this template")" LabelOff="@T("No, prohibit profile use for this template")" Disabled="@this.IsReadOnly" />
|
||||
@if (this.AllowProfileUsage)
|
||||
{
|
||||
<MudJustifiedText Class="mb-3 mt-3" Typo="Typo.body1">
|
||||
@T("A chat template may also decide which profile a chat starts with, including starting without one. Without such a decision, those chats start with the profile they would start with anyway. The profile stays changeable in the chat.")
|
||||
</MudJustifiedText>
|
||||
<MudTextSwitch @bind-Value="@this.preselectProfile" Color="Color.Primary" Label="@T("Does this chat template preselect a profile?")" LabelOn="@T("Yes, this template decides which profile a chat starts with")" LabelOff="@T("No, chats keep their usual profile")" Disabled="@this.IsReadOnly"/>
|
||||
@if (this.preselectProfile)
|
||||
{
|
||||
<MudSelect T="string" @bind-Value="@this.selectedProfileId" Label="@T("Preselected profile")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 mt-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person" Disabled="@this.IsReadOnly">
|
||||
<MudSelectItem T="string" Value="@Profile.NO_PROFILE.Id">@T("Use no profile")</MudSelectItem>
|
||||
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles)
|
||||
{
|
||||
<MudSelectItem T="string" Value="@profile.Id">@profile.GetSafeName()</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@if (this.IsPreselectedProfileMissing)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-3">
|
||||
@T("The profile this chat template preselects does not exist on this computer. Chats started with it keep their usual profile.")
|
||||
</MudAlert>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Provider")
|
||||
</MudText>
|
||||
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
|
||||
@T("A chat template may decide which provider, and with it which model, a chat starts with. Without such a decision, those chats start with the provider they would start with anyway. The provider stays changeable in the chat, and it must meet the confidence level required for chats.")
|
||||
</MudJustifiedText>
|
||||
<MudTextSwitch @bind-Value="@this.preselectProvider" Color="Color.Primary" Label="@T("Does this chat template preselect a provider?")" LabelOn="@T("Yes, this template decides which provider a chat starts with")" LabelOff="@T("No, chats keep their usual provider")" Disabled="@this.IsReadOnly"/>
|
||||
@if (this.preselectProvider)
|
||||
{
|
||||
<MudSelect T="string" @bind-Value="@this.selectedProviderId" Label="@T("Preselected provider")" Validation="@this.ValidatePreselectedProvider" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 mt-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.SmartToy" Disabled="@this.IsReadOnly">
|
||||
@foreach (var provider in this.SettingsManager.GetConfidentProviders(Components.CHAT))
|
||||
{
|
||||
<MudSelectItem T="string" Value="@provider.Id">
|
||||
<ProviderLabel ProviderSettings="@provider" Text="@provider.InstanceName"/>
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@if (this.IsPreselectedProviderUnusable)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-3">
|
||||
@T("The provider this chat template preselects does not exist on this computer or does not meet the confidence level required for chats. Chats started with it keep their usual provider.")
|
||||
</MudAlert>
|
||||
}
|
||||
}
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Workspace")
|
||||
</MudText>
|
||||
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
|
||||
@T("A chat template may keep its chats in a workspace of its own, which is created when it does not exist yet. This applies whenever a chat would otherwise be a disappearing chat, for example, when you start a new chat outside the workspaces or pick this template in a new chat. A chat you start in a workspace on purpose, for example, from the workspace list, stays in that workspace.")
|
||||
</MudJustifiedText>
|
||||
@* While workspaces are off, nothing goes into a workspace, so choosing one here would promise what no chat does: *@
|
||||
@if (this.AreWorkspacesDisabled)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-3">
|
||||
@T("Workspaces are switched off in your workspace settings. While they are off, chats with this template belong to no workspace, and they do not move into a workspace when you switch workspaces on again. You can change this choice once workspaces are on.")
|
||||
</MudAlert>
|
||||
}
|
||||
<MudTextSwitch @bind-Value="@this.preselectWorkspace" Color="Color.Primary" Label="@T("Does this chat template keep its chats in a workspace?")" LabelOn="@T("Yes, chats with this template go into a workspace")" LabelOff="@T("No, chats go where they would go anyway")" Disabled="@this.IsWorkspaceChoiceLocked"/>
|
||||
@if (this.preselectWorkspace)
|
||||
{
|
||||
<WorkspaceNameField @bind-WorkspaceName="@this.selectedWorkspaceName" SelectLabel="@T("Existing workspace (Optional)")" Label="@T("Workspace name")" HelperText="@T("Choose an existing workspace or enter a name that should be created when a chat with this template starts.")" Validation="@this.ValidateWorkspaceName" Immediate="@true" ReadOnly="@this.IsWorkspaceChoiceLocked" SelectClass="mb-3 mt-3 rounded-lg" TextFieldClass="mb-3 mt-3"/>
|
||||
}
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Tools")
|
||||
|
||||
@@ -79,25 +79,6 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
[Parameter]
|
||||
public DataSourceOptions? DataSourceOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The profile this template preselects, the ID of the no-profile entry to start without one,
|
||||
/// or null when it says nothing about profiles.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? ProfileId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The provider this template preselects, or null when it says nothing about providers.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? ProviderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the workspace this template keeps its chats in, or null when it says nothing about workspaces.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? WorkspaceName { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool CreateFromExistingChatThread { get; set; }
|
||||
|
||||
@@ -124,12 +105,6 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
private HashSet<string> selectedToolIds = new(StringComparer.Ordinal);
|
||||
private bool preselectDataSources;
|
||||
private DataSourceOptions templateDataSourceOptions = new();
|
||||
private bool preselectProfile;
|
||||
private string selectedProfileId = Profile.NO_PROFILE.Id;
|
||||
private bool preselectProvider;
|
||||
private string selectedProviderId = string.Empty;
|
||||
private bool preselectWorkspace;
|
||||
private string selectedWorkspaceName = string.Empty;
|
||||
private string[] dataIssues = [];
|
||||
private string dataEditingPreviousName = string.Empty;
|
||||
private bool isInlineEditOnGoing;
|
||||
@@ -163,14 +138,6 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
// them, so the switch inside the selection starts on instead of at its usual default:
|
||||
this.templateDataSourceOptions = this.DataSourceOptions?.CreateCopy() ?? new DataSourceOptions { DisableDataSources = false };
|
||||
|
||||
// The profile and the provider follow the same pattern, with a switch each:
|
||||
this.preselectProfile = this.ProfileId is not null;
|
||||
this.selectedProfileId = this.ProfileId ?? Profile.NO_PROFILE.Id;
|
||||
this.preselectProvider = this.ProviderId is not null;
|
||||
this.selectedProviderId = this.ProviderId ?? string.Empty;
|
||||
this.preselectWorkspace = this.WorkspaceName is not null;
|
||||
this.selectedWorkspaceName = this.WorkspaceName ?? string.Empty;
|
||||
|
||||
// When editing, we need to load the data:
|
||||
if(this.IsEditing)
|
||||
this.dataEditingPreviousName = this.DataName.ToLowerInvariant();
|
||||
@@ -216,11 +183,6 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
ToolIds = this.preselectTools ? new HashSet<string>(this.selectedToolIds, StringComparer.Ordinal) : null,
|
||||
DataSourceOptions = this.preselectDataSources ? this.templateDataSourceOptions.CreateCopy() : null,
|
||||
|
||||
// A template which forbids profiles has no use for a profile, so none is kept for it:
|
||||
ProfileId = this.AllowProfileUsage && this.preselectProfile ? this.selectedProfileId : null,
|
||||
ProviderId = this.preselectProvider && !string.IsNullOrWhiteSpace(this.selectedProviderId) ? this.selectedProviderId : null,
|
||||
WorkspaceName = this.preselectWorkspace && !string.IsNullOrWhiteSpace(this.selectedWorkspaceName) ? this.selectedWorkspaceName.Trim() : null,
|
||||
|
||||
EnterpriseConfigurationPluginId = Guid.Empty,
|
||||
IsEnterpriseConfiguration = false,
|
||||
};
|
||||
@@ -243,57 +205,6 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
|
||||
private void SetSelectedToolIds(HashSet<string> toolIds) => this.selectedToolIds = toolIds;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the preselected profile is gone, so that chats started with this template keep their usual profile.
|
||||
/// </summary>
|
||||
private bool IsPreselectedProfileMissing => this.preselectProfile
|
||||
&& this.selectedProfileId != Profile.NO_PROFILE.Id
|
||||
&& this.SettingsManager.GetProfileById(this.selectedProfileId) == Profile.NO_PROFILE;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the preselected provider is gone or may not be used for chats, so that chats started
|
||||
/// with this template keep their usual provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The list to pick from holds only the providers which may be used for chats. A template from
|
||||
/// another machine or from a configuration plugin may name another one, though, and the list
|
||||
/// alone would then show nothing about it.
|
||||
/// </remarks>
|
||||
private bool IsPreselectedProviderUnusable => this.preselectProvider
|
||||
&& !string.IsNullOrWhiteSpace(this.selectedProviderId)
|
||||
&& this.SettingsManager.GetChatTemplateProvider(this.selectedProviderId) == AIStudio.Settings.Provider.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// Whether workspaces are switched off, so that a workspace of this template has no effect for now.
|
||||
/// </summary>
|
||||
private bool AreWorkspacesDisabled => this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.DISABLE_WORKSPACES;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the workspace of this template can be changed right now.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not while workspaces are switched off: a choice made then would take effect for no chat,
|
||||
/// and nothing in the chat would say so. A workspace the template already names stays as it
|
||||
/// is, so it applies again once workspaces are back.
|
||||
/// </remarks>
|
||||
private bool IsWorkspaceChoiceLocked => this.IsReadOnly || this.AreWorkspacesDisabled;
|
||||
|
||||
private string? ValidateWorkspaceName(string workspaceName)
|
||||
{
|
||||
if (this.preselectWorkspace && string.IsNullOrWhiteSpace(workspaceName))
|
||||
return T("Please enter the workspace for the chats of this template, or switch the preselection off.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? ValidatePreselectedProvider(string providerId)
|
||||
{
|
||||
if (this.preselectProvider && string.IsNullOrWhiteSpace(providerId))
|
||||
return T("Please choose the provider this chat template preselects, or switch the preselection off.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void RemoveMessage(ContentBlock item)
|
||||
{
|
||||
if (this.IsReadOnly)
|
||||
|
||||
@@ -125,8 +125,6 @@
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<OutboundDataRestrictionSelection @bind-Value="@this.SelectedOutboundDataRestriction" Label="@T("Where a chat may send data after reading this data source")" Explanation="@T("This setting decides where a chat may still send data once the AI found something in this data source, so that its content does not end up in a web search, for example. Choose no restriction only for content anybody may read anyway.")" OrganizationMinimum="@this.OrganizationMinimumOutboundDataRestriction" SecurityPolicy="@this.dataSecurityPolicy"/>
|
||||
|
||||
<MudNumericField T="ushort" Min="10" @bind-Value="@this.dataMaxMatches" Label="@T("How many matches do you want at most per query?")" Variant="Variant.Outlined" Step="10" />
|
||||
|
||||
</MudForm>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using AIStudio.Assistants.ERI;
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.ERIClient;
|
||||
using AIStudio.Tools.ERIClient.DataModel;
|
||||
@@ -62,7 +61,6 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
|
||||
private string dataEditingPreviousInstanceName = string.Empty;
|
||||
private List<AuthMethod> availableAuthMethods = [];
|
||||
private DataSourceSecurity dataSecurityPolicy;
|
||||
private OutboundDataRestriction dataOutboundDataRestriction = DataSourceOutboundDataExtensions.DEFAULT_OUTBOUND_DATA_RESTRICTION;
|
||||
private SecurityRequirements dataSourceSecurityRequirements;
|
||||
private ushort dataMaxMatches = 10;
|
||||
private bool connectionTested;
|
||||
@@ -123,7 +121,6 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
|
||||
this.dataAuthMethod = this.DataSource.AuthMethod;
|
||||
this.dataUsername = this.DataSource.Username;
|
||||
this.dataSecurityPolicy = this.DataSource.SecurityPolicy;
|
||||
this.dataOutboundDataRestriction = this.DataSource.OutboundDataRestriction;
|
||||
this.dataMaxMatches = this.DataSource.MaxMatches;
|
||||
|
||||
// We cannot load the retrieval processes now, since we have
|
||||
@@ -202,29 +199,11 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
|
||||
UsernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED,
|
||||
Type = DataSourceType.ERI_V1,
|
||||
SecurityPolicy = this.dataSecurityPolicy,
|
||||
OutboundDataRestriction = this.SelectedOutboundDataRestriction,
|
||||
SelectedRetrievalId = this.dataSelectedRetrievalProcess.Id,
|
||||
MaxMatches = this.dataMaxMatches,
|
||||
};
|
||||
}
|
||||
|
||||
private OutboundDataRestriction OrganizationMinimumOutboundDataRestriction => this.SettingsManager.ConfigurationData.DataSourceSecurity.MinimumOutboundDataRestriction;
|
||||
|
||||
/// <summary>
|
||||
/// The level the data source gets: the one chosen, as far as the organization and the security
|
||||
/// policy allow it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Choosing the policy for self-hosted providers rules out every level but the strictest, see
|
||||
/// DataSourceOutboundDataExtensions.GetMinimumOutboundDataRestriction. The choice itself stays,
|
||||
/// so the dialog shows it again when the user chooses another policy.
|
||||
/// </remarks>
|
||||
private OutboundDataRestriction SelectedOutboundDataRestriction
|
||||
{
|
||||
get => this.dataOutboundDataRestriction.StricterOf(DataSourceOutboundDataExtensions.GetMinimumOutboundDataRestriction(this.dataSecurityPolicy, this.OrganizationMinimumOutboundDataRestriction));
|
||||
set => this.dataOutboundDataRestriction = value;
|
||||
}
|
||||
|
||||
private void ClearImportRetrievalIssue() => this.importRetrievalIssue = string.Empty;
|
||||
|
||||
private bool IsConnectionEncrypted() => this.dataHostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
<TextInfoLines Label="@T("Server description")" MaxLines="14" Value="@this.serverDescription" ClipboardTooltipSubject="@T("the server description")"/>
|
||||
<TextInfoLines Label="@T("Security requirements of the data provider")" MaxLines="3" Value="@this.securityRequirements.Explain()" ClipboardTooltipSubject="@T("the security requirements of the data provider")"/>
|
||||
<TextInfoLines Label="@T("Your security policy")" MaxLines="3" Value="@this.DataSource.SecurityPolicy.ToInfoText()" Color="@this.DataSource.SecurityPolicy.GetColor()" ClipboardTooltipSubject="@T("your security policy")"/>
|
||||
<TextInfoLine Icon="@Icons.Material.Filled.Shield" Label="@T("Where a chat may send data after reading this data source")" Value="@this.OutboundDataRestrictionName" ClipboardTooltipSubject="@T("where a chat may send data after reading this data source")"/>
|
||||
<TextInfoLine Label="@T("Maximum matches per query")" Value="@this.DataSource.MaxMatches.ToString()" ClipboardTooltipSubject="@T("the maximum number of matches per query")"/>
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mt-6">
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Text;
|
||||
|
||||
using AIStudio.Assistants.ERI;
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.ERIClient;
|
||||
using AIStudio.Tools.ERIClient.DataModel;
|
||||
@@ -53,11 +52,6 @@ public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, ISecretId
|
||||
|
||||
private string Port => this.DataSource.Port == 0 ? string.Empty : $"{this.DataSource.Port}";
|
||||
|
||||
/// <summary>
|
||||
/// The level which applies, including what the organization or the security policy demands.
|
||||
/// </summary>
|
||||
private string OutboundDataRestrictionName => this.DataSource.GetEffectiveOutboundDataRestriction(this.SettingsManager.ConfigurationData.DataSourceSecurity.MinimumOutboundDataRestriction).GetName();
|
||||
|
||||
private async Task<(bool Success, DataSourceERI_V1 EffectiveDataSource)> CreateEffectiveDataSource()
|
||||
{
|
||||
this.effectiveUsername = this.DataSource.Username;
|
||||
|
||||
@@ -10,12 +10,15 @@
|
||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues" FieldChanged="@this.RevalidateAfterFieldChange">
|
||||
<DataSourceMissingEmbeddingWarning IsInDataSourceDialog="@true" EmbeddingProviderAdded="@this.SelectAddedEmbeddingProvider"/>
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3">
|
||||
@T("Data source")
|
||||
</MudText>
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataName"
|
||||
Label="@T("Data Source Name")"
|
||||
Class="mb-6"
|
||||
Class="mb-3"
|
||||
MaxLength="@DataSourceValidation.MAX_NAME_LENGTH"
|
||||
Counter="@DataSourceValidation.MAX_NAME_LENGTH"
|
||||
Immediate="@true"
|
||||
@@ -31,7 +34,7 @@
|
||||
T="string"
|
||||
@bind-Text="@this.dataDescription"
|
||||
Label="@T("Description")"
|
||||
Class="mb-6"
|
||||
Class="mb-0"
|
||||
MaxLength="200"
|
||||
Counter="200"
|
||||
Immediate="@true"
|
||||
@@ -44,10 +47,37 @@
|
||||
Lines="3"
|
||||
/>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Folder")
|
||||
</MudText>
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
|
||||
@T("Select a root directory for this data source.")
|
||||
</MudJustifiedText>
|
||||
<SelectDirectory @bind-Directory="@this.dataPath" Label="@T("Selected base directory for this data source")" DirectoryDialogTitle="@T("Select the base directory")" Validation="@this.dataSourceValidation.ValidatePath" EnableDragDrop="true" CatchAllDocuments="true" />
|
||||
@if (!this.CanChangeSource)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Class="mb-3">
|
||||
@T("The documents of this data source are already prepared, so its folder cannot be changed. Another folder holds other documents, which makes it another data source: please add one for it. The embedding provider below can be changed.")
|
||||
</MudAlert>
|
||||
}
|
||||
@if (this.CanChangeSource)
|
||||
{
|
||||
<SelectDirectory @bind-Directory="@this.dataPath" Label="@T("Selected base directory for this data source")" DirectoryDialogTitle="@T("Select the base directory")" Validation="@this.dataSourceValidation.ValidatePath" EnableDragDrop="true" CatchAllDocuments="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.dataPath"
|
||||
Label="@T("Selected base directory for this data source")"
|
||||
Class="mb-3"
|
||||
ReadOnly="@true"
|
||||
Validation="@this.dataSourceValidation.ValidatePath"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Folder"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Variant="Variant.Outlined"
|
||||
/>
|
||||
}
|
||||
|
||||
<MudTextSwitch
|
||||
Label="@T("Include subfolders?")"
|
||||
@@ -57,28 +87,42 @@
|
||||
LabelOff="@T("No, only index files in the selected folder")"
|
||||
/>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Files to include")
|
||||
</MudText>
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
|
||||
@T("Select the file patterns this data source should index.")
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataFilePatterns"
|
||||
Label="@T("File patterns")"
|
||||
Class="mb-3"
|
||||
Immediate="@true"
|
||||
Validation="@this.dataSourceValidation.ValidateFilePatterns"
|
||||
HelperText="@T("Which files should be indexed? Separate multiple patterns with a semicolon, e.g., *.txt;*.pdf;*.docx;.gitignore. Use :documents: for every document and readable text file, :text: for readable text files only, or * for every file. Audio and video files, e.g., *.mp3 or *.mp4, are transcribed by your transcription provider.")"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.FilterAlt"
|
||||
AdornmentColor="Color.Info"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Variant="Variant.Outlined"
|
||||
/>
|
||||
<FilePatternSelection>
|
||||
<AllowedContent>
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataFilePatterns"
|
||||
Label="@T("Files to include")"
|
||||
Immediate="@true"
|
||||
Validation="@this.dataSourceValidation.ValidateFilePatterns"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentIcon="@Icons.Material.Filled.Restore"
|
||||
OnAdornmentClick="@this.RestoreDefaultFilePatterns"
|
||||
AdornmentAriaLabel="@T("Restore default patterns")"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Lines="1"
|
||||
AutoGrow="@true"
|
||||
MaxLines="0"
|
||||
Class="pattern-text-field"
|
||||
/>
|
||||
</AllowedContent>
|
||||
</FilePatternSelection>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("In order for the AI to be able to determine the appropriate data at any time, you must choose an embedding provider.")
|
||||
</MudJustifiedText>
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Embedding")
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mb-3">
|
||||
@T("Choose an embedding provider to make this data source searchable.")
|
||||
</MudText>
|
||||
<MudSelect @ref="@this.embeddingSelect" @bind-Value="@this.dataEmbeddingId" Label="@T("Embedding")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateEmbeddingProviderAccess">
|
||||
@foreach (var provider in this.SettingsManager.GetAllEmbeddingProviders())
|
||||
{
|
||||
@@ -94,14 +138,17 @@
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId))
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Color="Color.Tertiary" Class="mb-3">
|
||||
<MudJustifiedText Typo="Typo.body2" Color="Color.Tertiary" Class="mb-3">
|
||||
@T("The embedding you selected runs locally or in your organization. Your data is not sent to the cloud.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
|
||||
<ManagePandocDependency IntroText="@T("For some data types, such as Office files, MindWork AI Studio requires the open-source application Pandoc.")"/>
|
||||
<ManagePandocDependency IntroText="@T("MindWork AI Studio uses Pandoc to read HTML files.")"/>
|
||||
|
||||
<MudSelect @ref="@this.confidenceLevelSelect" @bind-Value="@this.dataConfidenceLevel" Text="@this.dataConfidenceLevel.GetName()" Label="@T("Required provider confidence level")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateDataSourceConfidenceLevel">
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Provider trust")
|
||||
</MudText>
|
||||
<MudSelect @ref="@this.confidenceLevelSelect" @bind-Value="@this.dataConfidenceLevel" Text="@this.dataConfidenceLevel.GetName()" Label="@T("Required provider confidence level")" HelperText="@T("Only providers with this confidence level or higher may receive and process data from this data source.")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateDataSourceConfidenceLevel">
|
||||
@foreach (var level in this.ConfidenceLevels)
|
||||
{
|
||||
<MudSelectItem Value="@level.Value">
|
||||
@@ -110,8 +157,6 @@
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<OutboundDataRestrictionSelection @bind-Value="@this.SelectedOutboundDataRestriction" Label="@T("Where a chat may send data after reading this data source")" Explanation="@T("This setting decides where a chat may still send data once the AI found something in this data source, so that its content does not end up in a web search, for example. Choose no restriction only for content anybody may read anyway.")" OrganizationMinimum="@this.OrganizationMinimumOutboundDataRestriction"/>
|
||||
|
||||
@if (this.MediaTranscriptionHint is { } mediaTranscriptionHint)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="@true" Class="mb-3">
|
||||
@@ -125,31 +170,21 @@
|
||||
</MudButton>
|
||||
<MudDivider/>
|
||||
<MudCollapse Expanded="@this.showExpertSettings" Class="@this.GetExpertStyles">
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">@T("Hidden files and folders")</MudText>
|
||||
<MudTextSwitch
|
||||
Label="@T("Include hidden files and folders?")"
|
||||
Value="@this.dataIncludeHiddenFilesAndFolders"
|
||||
ValueChanged="@(value => this.dataIncludeHiddenFilesAndFolders = value)"
|
||||
LabelOn="@T("Yes, index hidden files and folders as well")"
|
||||
LabelOn="@T("Yes, include hidden files and folders")"
|
||||
LabelOff="@T("No, ignore hidden files and folders")"
|
||||
/>
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
|
||||
@T("Hidden are files and folders whose names start with a dot, such as .git or .env, and those hidden by the operating system. A file pattern which starts with a dot, such as .gitignore, selects such files anyway.")
|
||||
@T("Files and folders are hidden if their names start with a dot or the operating system marks them as hidden. If you choose 'No', hidden items are skipped. To explicitly include a file such as .gitignore, enter its name under 'Files to include'. Using generic patterns such as *, :text: or :documents: still skips hidden files. Exclusion rules always take priority; you can change them in the fields below.")
|
||||
</MudJustifiedText>
|
||||
@* Without an icon: in a field of several lines, it floats in the middle of the text. *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataExcludePatterns"
|
||||
Label="@T("Excluded files and folders")"
|
||||
Lines="@FileExclusions.DEFAULT_LINE_COUNT"
|
||||
AutoGrow="@true"
|
||||
MaxLines="@FileExclusions.MAX_LINE_COUNT"
|
||||
Immediate="@true"
|
||||
Validation="@this.dataSourceValidation.ValidateExcludePatterns"
|
||||
HelperText="@T("These files and folders are left out, even when a file pattern selects them. Separate entries with a semicolon or a line break. End an entry with a slash to exclude every folder of that name, e.g., node_modules/. Other entries name files, e.g., *.min.js.")"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Variant="Variant.Outlined"
|
||||
/>
|
||||
<RestoreDefaultButton Text="@T("Restore default")" OnClick="@this.RestoreDefaultExcludePatterns" Class="mb-3"/>
|
||||
<div class="mt-4">
|
||||
<FileExclusionEditor @bind-Text="@this.dataExcludePatterns" Label="@T("Excluded files and folders")" ResetButtonText="@T("Restore default")"/>
|
||||
</div>
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2 mt-4">@T("File size limit")</MudText>
|
||||
<MudNumericField
|
||||
T="int"
|
||||
@bind-Value="@this.dataMaxFileSizeMegabytes"
|
||||
@@ -161,6 +196,10 @@
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentText="MB"/>
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2 mt-4">@T("Text chunking")</MudText>
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
|
||||
@T("Optional expert settings for how this data source is split before embedding.")
|
||||
</MudJustifiedText>
|
||||
@if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId))
|
||||
{
|
||||
<MudTextField
|
||||
@@ -174,9 +213,6 @@
|
||||
AdornmentIcon="@Icons.Material.Filled.TextFields"
|
||||
AdornmentColor="Color.Info"/>
|
||||
}
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("Optional expert settings for how this data source is split before embedding.")
|
||||
</MudJustifiedText>
|
||||
<MudNumericField
|
||||
@ref="@this.maxChunkTokenLengthField"
|
||||
T="int"
|
||||
@@ -204,6 +240,7 @@
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.CompareArrows"
|
||||
AdornmentColor="Color.Info"/>
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2 mt-4">@T("Search results")</MudText>
|
||||
<MudNumericField
|
||||
T="ushort"
|
||||
Min="10"
|
||||
|
||||
@@ -23,6 +23,16 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
|
||||
[Parameter]
|
||||
public DataSourceLocalDirectory DataSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the folder this data source reads must stay as it is.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Set once the index holds something for this data source. The embedding is not locked along
|
||||
/// with it: it can be changed, and DataSourceReindexWarning asks what that costs.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public bool LockSource { get; set; }
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
@@ -63,7 +73,6 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
|
||||
private bool showExpertSettings;
|
||||
private bool revalidateAfterRender;
|
||||
private ConfidenceLevel dataConfidenceLevel = ConfidenceLevel.UNKNOWN;
|
||||
private OutboundDataRestriction dataOutboundDataRestriction = DataSourceOutboundDataExtensions.DEFAULT_OUTBOUND_DATA_RESTRICTION;
|
||||
|
||||
// We get the form reference from Blazor code to validate it manually:
|
||||
private MudForm form = null!;
|
||||
@@ -118,7 +127,6 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
|
||||
this.dataMaxChunkTokenLength = this.DataSource.MaxChunkTokenLength;
|
||||
this.dataChunkOverlapTokenLength = this.DataSource.ChunkOverlapTokenLength;
|
||||
this.dataConfidenceLevel = this.DataSource.ConfidenceLevel;
|
||||
this.dataOutboundDataRestriction = this.DataSource.OutboundDataRestriction;
|
||||
this.dataMaxMatches = this.DataSource.MaxMatches;
|
||||
}
|
||||
|
||||
@@ -207,24 +215,10 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
|
||||
? T("Audio and video files which this data source selects are skipped, because your transcription provider does not meet the confidence level this data source requires.")
|
||||
: null;
|
||||
|
||||
private bool CanChangeSource => !this.IsEditing || !this.LockSource;
|
||||
|
||||
private IEnumerable<ConfigurationSelectData<ConfidenceLevel>> ConfidenceLevels => ConfigurationSelectDataFactory.GetDataSourceConfidenceLevelsData();
|
||||
|
||||
private OutboundDataRestriction OrganizationMinimumOutboundDataRestriction => this.SettingsManager.ConfigurationData.DataSourceSecurity.MinimumOutboundDataRestriction;
|
||||
|
||||
/// <summary>
|
||||
/// The level the data source gets: the one chosen, as far as the organization allows it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The organization may rule out a level while the dialog is open, or after the data source was
|
||||
/// stored with it. The choice itself stays, so the dialog shows it again once the organization
|
||||
/// allows it again.
|
||||
/// </remarks>
|
||||
private OutboundDataRestriction SelectedOutboundDataRestriction
|
||||
{
|
||||
get => this.dataOutboundDataRestriction.StricterOf(this.OrganizationMinimumOutboundDataRestriction);
|
||||
set => this.dataOutboundDataRestriction = value;
|
||||
}
|
||||
|
||||
private string SelectedEmbeddingTokenizerText => this.SelectedEmbedding is null
|
||||
? T("No embedding selected")
|
||||
: string.IsNullOrWhiteSpace(this.SelectedEmbedding.TokenizerPath)
|
||||
@@ -250,7 +244,9 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
|
||||
Type = DataSourceType.LOCAL_DIRECTORY,
|
||||
EmbeddingId = this.dataEmbeddingId,
|
||||
|
||||
Path = this.dataPath,
|
||||
// Kept out of reach of the form while the source is locked, so a stale field cannot point an
|
||||
// indexed data source somewhere else:
|
||||
Path = this.CanChangeSource ? this.dataPath : this.DataSource.Path,
|
||||
FilePatterns = this.NormalizeFilePatterns(),
|
||||
|
||||
// Kept as typed, since the line breaks group the entries for the eye:
|
||||
@@ -261,7 +257,6 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
|
||||
MaxChunkTokenLength = this.dataMaxChunkTokenLength,
|
||||
ChunkOverlapTokenLength = this.dataChunkOverlapTokenLength,
|
||||
ConfidenceLevel = this.dataConfidenceLevel,
|
||||
OutboundDataRestriction = this.SelectedOutboundDataRestriction,
|
||||
MaxMatches = this.dataMaxMatches,
|
||||
};
|
||||
|
||||
@@ -273,14 +268,6 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
|
||||
if (!this.dataIsValid)
|
||||
return;
|
||||
|
||||
await this.StoreValidatedDataSourceAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Continues Save after form validation, asking before the draft is returned to its caller.
|
||||
/// </summary>
|
||||
internal async Task StoreValidatedDataSourceAsync()
|
||||
{
|
||||
var addedDataSource = this.CreateDataSource();
|
||||
|
||||
//
|
||||
@@ -301,7 +288,7 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
|
||||
|
||||
private string NormalizeFilePatterns() => FileNamePatterns.Normalize(this.dataFilePatterns);
|
||||
|
||||
private void RestoreDefaultExcludePatterns() => this.dataExcludePatterns = FileExclusions.DEFAULT_PATTERNS;
|
||||
private void RestoreDefaultFilePatterns() => this.dataFilePatterns = DataSourceLocalDirectory.DEFAULT_FILE_PATTERNS;
|
||||
|
||||
/// <summary>
|
||||
/// Gives the fields which are checked against each other a fresh verdict: the embedding provider
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
<TextInfoLine Icon="@Icons.Material.Filled.FolderOpen" Label="@T("Path")" Value="@this.DataSource.Path" ClipboardTooltipSubject="@T("this path")"/>
|
||||
<TextInfoLine Icon="@Icons.Material.Filled.AccountTree" Label="@T("Include subfolders")" Value="@(this.DataSource.IncludeSubdirectories ? T("Yes") : T("No"))" ClipboardTooltipSubject="@T("whether subfolders are included")"/>
|
||||
<TextInfoLine Icon="@Icons.Material.Filled.VisibilityOff" Label="@T("Include hidden files and folders")" Value="@(this.DataSource.IncludeHiddenFilesAndFolders ? T("Yes") : T("No"))" ClipboardTooltipSubject="@T("whether hidden files and folders are included")"/>
|
||||
<TextInfoLine Icon="@Icons.Material.Filled.FilterAlt" Label="@T("File patterns")" Value="@this.DataSource.EffectiveFilePatterns" ClipboardTooltipSubject="@T("the file patterns")"/>
|
||||
<TextInfoLines Label="@T("Excluded files and folders")" Value="@this.DataSource.EffectiveExcludePatterns" ClipboardTooltipSubject="@T("the excluded files and folders")"/>
|
||||
<TextInfoLine Icon="@Icons.Material.Filled.Straighten" Label="@T("Ignore files larger than")" Value="@FolderFileSelection.MegabytesToBytes(this.DataSource.EffectiveMaxFileSizeMegabytes).FileSize()" ClipboardTooltipSubject="@T("the size limit for files")"/>
|
||||
<TextInfoLines Icon="@Icons.Material.Filled.FilterAlt" Label="@T("Files to include")" Value="@this.DataSource.EffectiveFilePatterns" Lines="1" MaxLines="0" ClipboardTooltipSubject="@T("the file patterns")"/>
|
||||
<FileExclusionEditor Text="@this.DataSource.EffectiveExcludePatterns" ReadOnly="@true" ShowHeader="@false" ShowHelp="@false"/>
|
||||
<TextInfoLine Icon="@Icons.Material.Filled.Straighten" Label="@T("Ignore files larger than")" Value="@($"{this.DataSource.EffectiveMaxFileSizeMegabytes} MB")" ClipboardTooltipSubject="@T("the size limit for files")"/>
|
||||
@if (!this.IsDirectoryAvailable)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Color="Color.Error" Class="mb-3">
|
||||
@@ -43,7 +43,6 @@
|
||||
}
|
||||
|
||||
<TextInfoLine Label="@T("Required provider confidence level")" Value="@this.DataSource.ConfidenceLevel.GetName()" ClipboardTooltipSubject="@T("the required provider confidence level")"/>
|
||||
<TextInfoLine Icon="@Icons.Material.Filled.Shield" Label="@T("Where a chat may send data after reading this data source")" Value="@this.OutboundDataRestrictionName" ClipboardTooltipSubject="@T("where a chat may send data after reading this data source")"/>
|
||||
<TextInfoLine Label="@T("Maximum matches per query")" Value="@this.DataSource.MaxMatches.ToString()" ClipboardTooltipSubject="@T("the maximum number of matches per query")"/>
|
||||
|
||||
<TextInfoLine Icon="@Icons.Material.Filled.SquareFoot" Label="@T("Number of matching files")" Value="@this.NumberFilesInDirectory" ClipboardTooltipSubject="@T("the number of matching files in the directory")"/>
|
||||
|
||||
@@ -60,11 +60,6 @@ public partial class DataSourceLocalDirectoryInfoDialog : MSGComponentBase
|
||||
|
||||
private bool IsDirectoryAvailable => this.directoryInfo.Exists;
|
||||
|
||||
/// <summary>
|
||||
/// The level which applies, including what the organization demands.
|
||||
/// </summary>
|
||||
private string OutboundDataRestrictionName => this.DataSource.GetEffectiveOutboundDataRestriction(this.SettingsManager.ConfigurationData.DataSourceSecurity.MinimumOutboundDataRestriction).GetName();
|
||||
|
||||
/// <summary>
|
||||
/// Takes the next file which the directory scan found.
|
||||
/// </summary>
|
||||
|
||||
@@ -10,12 +10,15 @@
|
||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues" FieldChanged="@this.RevalidateAfterFieldChange">
|
||||
<DataSourceMissingEmbeddingWarning IsInDataSourceDialog="@true" EmbeddingProviderAdded="@this.SelectAddedEmbeddingProvider"/>
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3">
|
||||
@T("Data source")
|
||||
</MudText>
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataName"
|
||||
Label="@T("Data Source Name")"
|
||||
Class="mb-6"
|
||||
Class="mb-3"
|
||||
MaxLength="@DataSourceValidation.MAX_NAME_LENGTH"
|
||||
Counter="@DataSourceValidation.MAX_NAME_LENGTH"
|
||||
Immediate="@true"
|
||||
@@ -31,7 +34,7 @@
|
||||
T="string"
|
||||
@bind-Text="@this.dataDescription"
|
||||
Label="@T("Description")"
|
||||
Class="mb-6"
|
||||
Class="mb-0"
|
||||
MaxLength="200"
|
||||
Counter="200"
|
||||
Immediate="@true"
|
||||
@@ -44,14 +47,44 @@
|
||||
Lines="3"
|
||||
/>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("File")
|
||||
</MudText>
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
|
||||
@T("Select a file for this data source. The content of this file will be processed for the data source. An audio or video file is transcribed by your transcription provider first.")
|
||||
</MudJustifiedText>
|
||||
<SelectFile @bind-File="@this.dataFilePath" Label="@T("Selected file path for this data source")" FileDialogTitle="@T("Select the file")" Validation="@this.dataSourceValidation.ValidateFilePath" EnableDragDrop="true" CatchAllDocuments="true" />
|
||||
@if (!this.CanChangeSource)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Class="mb-3">
|
||||
@T("The documents of this data source are already prepared, so its file cannot be changed. Another file holds other content, which makes it another data source: please add one for it. The embedding provider below can be changed.")
|
||||
</MudAlert>
|
||||
}
|
||||
@if (this.CanChangeSource)
|
||||
{
|
||||
<SelectFile @bind-File="@this.dataFilePath" Label="@T("Selected file path for this data source")" FileDialogTitle="@T("Select the file")" Validation="@this.dataSourceValidation.ValidateFilePath" EnableDragDrop="true" CatchAllDocuments="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.dataFilePath"
|
||||
Label="@T("Selected file path for this data source")"
|
||||
Class="mb-3"
|
||||
ReadOnly="@true"
|
||||
Validation="@this.dataSourceValidation.ValidateFilePath"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.AttachFile"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Variant="Variant.Outlined"
|
||||
/>
|
||||
}
|
||||
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("In order for the AI to be able to determine the appropriate data at any time, you must choose an embedding provider.")
|
||||
</MudJustifiedText>
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Embedding")
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mb-3">
|
||||
@T("Choose an embedding provider to make this data source searchable.")
|
||||
</MudText>
|
||||
<MudSelect @ref="@this.embeddingSelect" @bind-Value="@this.dataEmbeddingId" Label="@T("Embedding")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateEmbeddingProviderAccess">
|
||||
@foreach (var provider in this.SettingsManager.GetAllEmbeddingProviders())
|
||||
{
|
||||
@@ -67,14 +100,17 @@
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId))
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Color="Color.Tertiary" Class="mb-3">
|
||||
<MudJustifiedText Typo="Typo.body2" Color="Color.Tertiary" Class="mb-3">
|
||||
@T("The embedding you selected runs locally or in your organization. Your data is not sent to the cloud.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
|
||||
<ManagePandocDependency IntroText="@T("For some data types, such as Office files, MindWork AI Studio requires the open-source application Pandoc.")"/>
|
||||
<ManagePandocDependency IntroText="@T("MindWork AI Studio uses Pandoc to read HTML files.")"/>
|
||||
|
||||
<MudSelect @ref="@this.confidenceLevelSelect" @bind-Value="@this.dataConfidenceLevel" Text="@this.dataConfidenceLevel.GetName()" Label="@T("Required provider confidence level")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateDataSourceConfidenceLevel">
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Provider trust")
|
||||
</MudText>
|
||||
<MudSelect @ref="@this.confidenceLevelSelect" @bind-Value="@this.dataConfidenceLevel" Text="@this.dataConfidenceLevel.GetName()" Label="@T("Required provider confidence level")" HelperText="@T("Only providers with this confidence level or higher may receive and process data from this data source.")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateDataSourceConfidenceLevel">
|
||||
@foreach (var level in this.ConfidenceLevels)
|
||||
{
|
||||
<MudSelectItem Value="@level.Value">
|
||||
@@ -83,8 +119,6 @@
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<OutboundDataRestrictionSelection @bind-Value="@this.SelectedOutboundDataRestriction" Label="@T("Where a chat may send data after reading this data source")" Explanation="@T("This setting decides where a chat may still send data once the AI found something in this data source, so that its content does not end up in a web search, for example. Choose no restriction only for content anybody may read anyway.")" OrganizationMinimum="@this.OrganizationMinimumOutboundDataRestriction"/>
|
||||
|
||||
@if (this.MediaTranscriptionHint is { } mediaTranscriptionHint)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="@true" Class="mb-3">
|
||||
@@ -98,6 +132,10 @@
|
||||
</MudButton>
|
||||
<MudDivider/>
|
||||
<MudCollapse Expanded="@this.showExpertSettings" Class="@this.GetExpertStyles">
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">@T("Text chunking")</MudText>
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
|
||||
@T("Optional expert settings for how this data source is split before embedding.")
|
||||
</MudJustifiedText>
|
||||
@if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId))
|
||||
{
|
||||
<MudTextField
|
||||
@@ -111,9 +149,6 @@
|
||||
AdornmentIcon="@Icons.Material.Filled.TextFields"
|
||||
AdornmentColor="Color.Info"/>
|
||||
}
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("Optional expert settings for how this data source is split before embedding.")
|
||||
</MudJustifiedText>
|
||||
<MudNumericField
|
||||
@ref="@this.maxChunkTokenLengthField"
|
||||
T="int"
|
||||
@@ -141,6 +176,7 @@
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.CompareArrows"
|
||||
AdornmentColor="Color.Info"/>
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2 mt-4">@T("Search results")</MudText>
|
||||
<MudNumericField
|
||||
T="ushort"
|
||||
Min="10"
|
||||
|
||||
@@ -24,6 +24,16 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
|
||||
[Parameter]
|
||||
public DataSourceLocalFile DataSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the file this data source reads must stay as it is.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Set once the index holds something for this data source. The embedding is not locked along
|
||||
/// with it: it can be changed, and DataSourceReindexWarning asks what that costs.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public bool LockSource { get; set; }
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
@@ -59,7 +69,6 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
|
||||
private bool showExpertSettings;
|
||||
private bool revalidateAfterRender;
|
||||
private ConfidenceLevel dataConfidenceLevel = ConfidenceLevel.UNKNOWN;
|
||||
private OutboundDataRestriction dataOutboundDataRestriction = DataSourceOutboundDataExtensions.DEFAULT_OUTBOUND_DATA_RESTRICTION;
|
||||
|
||||
// We get the form reference from Blazor code to validate it manually:
|
||||
private MudForm form = null!;
|
||||
@@ -109,7 +118,6 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
|
||||
this.dataMaxChunkTokenLength = this.DataSource.MaxChunkTokenLength;
|
||||
this.dataChunkOverlapTokenLength = this.DataSource.ChunkOverlapTokenLength;
|
||||
this.dataConfidenceLevel = this.DataSource.ConfidenceLevel;
|
||||
this.dataOutboundDataRestriction = this.DataSource.OutboundDataRestriction;
|
||||
this.dataMaxMatches = this.DataSource.MaxMatches;
|
||||
}
|
||||
|
||||
@@ -198,24 +206,10 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
|
||||
? T("This audio or video file is skipped, because your transcription provider does not meet the confidence level this data source requires.")
|
||||
: null;
|
||||
|
||||
private bool CanChangeSource => !this.IsEditing || !this.LockSource;
|
||||
|
||||
private IEnumerable<ConfigurationSelectData<ConfidenceLevel>> ConfidenceLevels => ConfigurationSelectDataFactory.GetDataSourceConfidenceLevelsData();
|
||||
|
||||
private OutboundDataRestriction OrganizationMinimumOutboundDataRestriction => this.SettingsManager.ConfigurationData.DataSourceSecurity.MinimumOutboundDataRestriction;
|
||||
|
||||
/// <summary>
|
||||
/// The level the data source gets: the one chosen, as far as the organization allows it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The organization may rule out a level while the dialog is open, or after the data source was
|
||||
/// stored with it. The choice itself stays, so the dialog shows it again once the organization
|
||||
/// allows it again.
|
||||
/// </remarks>
|
||||
private OutboundDataRestriction SelectedOutboundDataRestriction
|
||||
{
|
||||
get => this.dataOutboundDataRestriction.StricterOf(this.OrganizationMinimumOutboundDataRestriction);
|
||||
set => this.dataOutboundDataRestriction = value;
|
||||
}
|
||||
|
||||
private string SelectedEmbeddingTokenizerText => this.SelectedEmbedding is null
|
||||
? T("No embedding selected")
|
||||
: string.IsNullOrWhiteSpace(this.SelectedEmbedding.TokenizerPath)
|
||||
@@ -241,11 +235,12 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
|
||||
Type = DataSourceType.LOCAL_FILE,
|
||||
EmbeddingId = this.dataEmbeddingId,
|
||||
|
||||
FilePath = this.dataFilePath,
|
||||
// Kept out of reach of the form while the source is locked, so a stale field cannot point an
|
||||
// indexed data source somewhere else:
|
||||
FilePath = this.CanChangeSource ? this.dataFilePath : this.DataSource.FilePath,
|
||||
MaxChunkTokenLength = this.dataMaxChunkTokenLength,
|
||||
ChunkOverlapTokenLength = this.dataChunkOverlapTokenLength,
|
||||
ConfidenceLevel = this.dataConfidenceLevel,
|
||||
OutboundDataRestriction = this.SelectedOutboundDataRestriction,
|
||||
MaxMatches = this.dataMaxMatches,
|
||||
};
|
||||
|
||||
@@ -257,14 +252,6 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
|
||||
if (!this.dataIsValid)
|
||||
return;
|
||||
|
||||
await this.StoreValidatedDataSourceAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Continues Save after form validation, asking before the draft is returned to its caller.
|
||||
/// </summary>
|
||||
internal async Task StoreValidatedDataSourceAsync()
|
||||
{
|
||||
var addedDataSource = this.CreateDataSource();
|
||||
|
||||
//
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
}
|
||||
|
||||
<TextInfoLine Label="@T("Required provider confidence level")" Value="@this.DataSource.ConfidenceLevel.GetName()" ClipboardTooltipSubject="@T("the required provider confidence level")"/>
|
||||
<TextInfoLine Icon="@Icons.Material.Filled.Shield" Label="@T("Where a chat may send data after reading this data source")" Value="@this.OutboundDataRestrictionName" ClipboardTooltipSubject="@T("where a chat may send data after reading this data source")"/>
|
||||
<TextInfoLine Label="@T("Maximum matches per query")" Value="@this.DataSource.MaxMatches.ToString()" ClipboardTooltipSubject="@T("the maximum number of matches per query")"/>
|
||||
<TextInfoLine Icon="@Icons.Material.Filled.SquareFoot" Label="@T("File size")" Value="@this.FileSize" ClipboardTooltipSubject="@T("the file size")"/>
|
||||
</DialogContent>
|
||||
|
||||
@@ -34,10 +34,5 @@ public partial class DataSourceLocalFileInfoDialog : MSGComponentBase
|
||||
|
||||
private string FileSize => this.fileInfo.FileSize();
|
||||
|
||||
/// <summary>
|
||||
/// The level which applies, including what the organization demands.
|
||||
/// </summary>
|
||||
private string OutboundDataRestrictionName => this.DataSource.GetEffectiveOutboundDataRestriction(this.SettingsManager.ConfigurationData.DataSourceSecurity.MinimumOutboundDataRestriction).GetName();
|
||||
|
||||
private void Close() => this.MudDialog.Close();
|
||||
}
|
||||
@@ -6,12 +6,6 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<PreviewPrototype/>
|
||||
<MudText Typo="Typo.h6">
|
||||
@this.MudDialog.Title
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues" FieldChanged="@this.RevalidateAfterFieldChange">
|
||||
@if (this.IsAddingNotAllowed)
|
||||
@@ -371,7 +365,26 @@
|
||||
@T("Only providers with at least this confidence level see the content of your mails: the chat provider, the embedding provider, and every provider which checks a mail for you.")
|
||||
</MudJustifiedText>
|
||||
|
||||
<OutboundDataRestrictionSelection @bind-Value="@this.dataOutboundDataRestriction" Label="@T("Where a chat may send data after reading mails")" Explanation="@T("Mails come from strangers and may contain instructions meant for the AI. This setting decides where a chat may still send data once it has read from this mailbox. Mind that opening a link can be enough to set something off, e.g., to confirm a subscription, an order, or a payment.")" OrganizationMinimum="@this.MinimumOutboundDataRestriction"/>
|
||||
<MudSelect T="OutboundDataRestriction" @bind-Value="@this.dataOutboundDataRestriction" Text="@this.dataOutboundDataRestriction.GetName()" Label="@T("Where a chat may send data after reading mails")" Class="mb-1" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Shield">
|
||||
@foreach (var restriction in ConfigurationSelectDataFactory.GetOutboundDataRestrictionData(this.MinimumOutboundDataRestriction))
|
||||
{
|
||||
<MudSelectItem T="OutboundDataRestriction" Value="@restriction.Value">
|
||||
@restriction.Name
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-1">
|
||||
@this.dataOutboundDataRestriction.GetDescription()
|
||||
</MudJustifiedText>
|
||||
@if (this.IsOutboundDataRestrictionLimited)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-1">
|
||||
@T("Your organization does not allow less strict settings than the ones offered here.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-6">
|
||||
@T("Mails come from strangers and may contain instructions meant for the AI. This setting decides where a chat may still send data once it has read from this mailbox. Mind that opening a link can be enough to set something off, e.g., to confirm a subscription, an order, or a payment.")
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudStack Class="mb-3">
|
||||
<MudButton OnClick="@this.ToggleExpertSettings" Variant="Variant.Text" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Tune">
|
||||
|
||||
@@ -343,6 +343,8 @@ public partial class DataSourceMailboxDialog : MSGComponentBase
|
||||
|
||||
private OutboundDataRestriction MinimumOutboundDataRestriction => this.SettingsManager.ConfigurationData.MailboxSettings.MinimumOutboundDataRestriction;
|
||||
|
||||
private bool IsOutboundDataRestrictionLimited => this.MinimumOutboundDataRestriction is not OutboundDataRestriction.UNRESTRICTED;
|
||||
|
||||
private string TestResultText
|
||||
{
|
||||
get
|
||||
|
||||
@@ -25,203 +25,194 @@
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
<MudButton Disabled="@(!this.DataLLMProvider.ShowRegisterButton() || this.IsProviderProhibited)" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.DataLLMProvider.GetCreationURL()" Target="_blank">
|
||||
<MudButton Disabled="@(!this.DataLLMProvider.ShowRegisterButton())" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.DataLLMProvider.GetCreationURL()" Target="_blank">
|
||||
@T("Create account")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
@if (this.IsProviderProhibited)
|
||||
@if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost))
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-3">
|
||||
@T("The use of this provider has been prohibited by your organization. Please select a different provider.")
|
||||
</MudAlert>
|
||||
<SecretInputField Secret="@this.dataAPIKey" SecretChanged="@this.OnAPIKeyChanged" Label="@this.APIKeyText" Validation="@this.providerValidation.ValidatingAPIKey"/>
|
||||
}
|
||||
else
|
||||
|
||||
@if (this.DataLLMProvider.IsHostnameNeeded())
|
||||
{
|
||||
@if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost))
|
||||
{
|
||||
<SecretInputField Secret="@this.dataAPIKey" SecretChanged="@this.OnAPIKeyChanged" Label="@this.APIKeyText" Validation="@this.providerValidation.ValidatingAPIKey"/>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHostnameNeeded())
|
||||
{
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.DataHostname"
|
||||
Label="@T("Hostname")"
|
||||
Class="mb-3"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingHostname"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHostNeeded())
|
||||
{
|
||||
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHost">
|
||||
@foreach (Host host in Enum.GetValues(typeof(Host)))
|
||||
{
|
||||
if (host.IsEmbeddingSupported())
|
||||
{
|
||||
<MudSelectItem Value="@host">
|
||||
@host.Name()
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHFInstanceProviderNeeded())
|
||||
{
|
||||
<MudSelect T="HFInferenceProvider" Value="@this.HFInferenceProviderId" ValueChanged="@this.OnHFInferenceProviderChanged" Label="@T("Hugging Face Inference Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHFInstanceProviderForEmbeddings">
|
||||
@foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider)))
|
||||
{
|
||||
@if (inferenceProvider.SupportsEmbeddings())
|
||||
{
|
||||
<MudSelectItem Value="@inferenceProvider">
|
||||
@inferenceProvider.ToName()
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
<MudJustifiedText Class="mb-3">
|
||||
@T("Hugging Face offers embeddings through a few of its inference providers only, which is why this list is shorter than the one for chatting.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
|
||||
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" StretchItems="StretchItems.End">
|
||||
<MudButton Disabled="@(this.IsEnterpriseConfiguration || !this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
|
||||
@T("Load")
|
||||
</MudButton>
|
||||
@if (this.availableModels.Count is 0)
|
||||
{
|
||||
<MudText Typo="Typo.body1">
|
||||
@T("No models loaded or available.")
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSelect Disabled="@(this.IsEnterpriseConfiguration || this.IsNoneProvider)" @bind-Value="@this.DataModel" Label="@T("Model")"
|
||||
OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural"
|
||||
AdornmentColor="Color.Info" Adornment="Adornment.Start"
|
||||
Validation="@this.providerValidation.ValidatingModel">
|
||||
@foreach (var model in this.availableModels)
|
||||
{
|
||||
<MudSelectItem Value="@model">
|
||||
@model
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
</MudStack>
|
||||
@if (this.dataConfiguredModelIsNotOffered)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mt-3">
|
||||
@T("This server does not offer the selected model right now. It stays selected, so the documents you already prepared keep working. Choosing another model means every document of the data sources behind this provider is prepared again.")
|
||||
</MudAlert>
|
||||
}
|
||||
@if (this.ServerNamedNoEmbeddingModel)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mt-3">
|
||||
@T("Your server answered, but none of the models it serves is one we know to create embeddings. Either there is none installed, or it runs under a name we do not recognize. In the latter case, your organization can describe the model in a model plugin, and it will show up here.")
|
||||
</MudAlert>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(this.dataLoadingModelsIssue))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mt-3">
|
||||
@this.dataLoadingModelsIssue
|
||||
</MudAlert>
|
||||
}
|
||||
</MudField>
|
||||
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.DataName"
|
||||
Label="@T("Instance Name")"
|
||||
@bind-Text="@this.DataHostname"
|
||||
Label="@T("Hostname")"
|
||||
Class="mb-3"
|
||||
MaxLength="40"
|
||||
Counter="40"
|
||||
Immediate="@true"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
|
||||
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingInstanceName"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
@if (this.DataLLMProvider != LLMProviders.NONE)
|
||||
Validation="@this.providerValidation.ValidatingHostname"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHostNeeded())
|
||||
{
|
||||
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHost">
|
||||
@foreach (Host host in Enum.GetValues(typeof(Host)))
|
||||
{
|
||||
if (host.IsEmbeddingSupported())
|
||||
{
|
||||
<MudSelectItem Value="@host">
|
||||
@host.Name()
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHFInstanceProviderNeeded())
|
||||
{
|
||||
<MudSelect T="HFInferenceProvider" Value="@this.HFInferenceProviderId" ValueChanged="@this.OnHFInferenceProviderChanged" Label="@T("Hugging Face Inference Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHFInstanceProviderForEmbeddings">
|
||||
@foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider)))
|
||||
{
|
||||
@if (inferenceProvider.SupportsEmbeddings())
|
||||
{
|
||||
<MudSelectItem Value="@inferenceProvider">
|
||||
@inferenceProvider.ToName()
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
<MudJustifiedText Class="mb-3">
|
||||
@T("Hugging Face offers embeddings through a few of its inference providers only, which is why this list is shorter than the one for chatting.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
|
||||
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" StretchItems="StretchItems.End">
|
||||
<MudButton Disabled="@(this.IsEnterpriseConfiguration || !this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
|
||||
@T("Load")
|
||||
</MudButton>
|
||||
@if (this.availableModels.Count is 0)
|
||||
{
|
||||
<MudText Typo="Typo.body1">
|
||||
@T("No models loaded or available.")
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSelect Disabled="@(this.IsEnterpriseConfiguration || this.IsNoneProvider)" @bind-Value="@this.DataModel" Label="@T("Model")"
|
||||
OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural"
|
||||
AdornmentColor="Color.Info" Adornment="Adornment.Start"
|
||||
Validation="@this.providerValidation.ValidatingModel">
|
||||
@foreach (var model in this.availableModels)
|
||||
{
|
||||
<MudSelectItem Value="@model">
|
||||
@model
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
</MudStack>
|
||||
@if (this.dataConfiguredModelIsNotOffered)
|
||||
{
|
||||
<MudStack>
|
||||
<MudButton OnClick="@this.ToggleExpertSettings">
|
||||
@(this.showExpertSettings ? T("Hide Expert Settings") : T("Show Expert Settings"))
|
||||
</MudButton>
|
||||
<MudDivider />
|
||||
<MudCollapse Expanded="@this.showExpertSettings" Class="@this.GetExpertStyles">
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("Please be aware: This section is for experts only. For cloud providers, the selected tokenizer and chunk settings may not match the real embedding model limits exactly.")
|
||||
</MudJustifiedText>
|
||||
<MudNumericField
|
||||
T="int"
|
||||
@bind-Value="@this.DataTokenLimit"
|
||||
Label="@T("Token limit")"
|
||||
Class="mb-3"
|
||||
Min="1"
|
||||
Immediate="@true"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Numbers"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.ValidateTokenLimit"
|
||||
HelperText="@T("Maximum number of tokens sent to the embedding model per chunk. The default is 8,192.")"/>
|
||||
<MudNumericField
|
||||
T="int"
|
||||
@bind-Value="@this.DataEmbeddingBatchSize"
|
||||
Label="@T("Embedding batch size")"
|
||||
Class="mb-3"
|
||||
Min="1"
|
||||
Immediate="@true"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.FormatListNumbered"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.ValidateEmbeddingBatchSize"
|
||||
HelperText="@T("How many chunks are sent to the embedding provider at once. The default is 1.")"/>
|
||||
<TokenizerHint LLMProvider="@this.DataLLMProvider" Model="@this.DataModel"/>
|
||||
<PathDropZone IdPrefix="tokenizer" Disabled="@(() => this.IsEnterpriseConfiguration)" OnPathsDropped="@this.OnTokenizerPathsDropped">
|
||||
<MudStack Row="@true" Spacing="3" Class="mb-3" StretchItems="StretchItems.None" AlignItems="AlignItems.Center">
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.dataFilePath"
|
||||
TextChanged="@this.OnDataFilePathChanged"
|
||||
Label="@T("Selected file path for the custom tokenizer")"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingCustomTokenizer"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.AttachFile"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Variant="Variant.Outlined"
|
||||
Clearable="@true"
|
||||
Error="@(!string.IsNullOrWhiteSpace(this.dataCustomTokenizerValidationIssue))"
|
||||
ErrorText="@this.dataCustomTokenizerValidationIssue"
|
||||
OnClearButtonClick="@this.ClearPathTokenizer"
|
||||
/>
|
||||
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="@(this.IsEnterpriseConfiguration || this.isTokenizerFileDialogOpen)" OnClick="@this.OpenTokenizerFileDialog">
|
||||
@T("Choose File")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("You can also drag & drop the tokenizer file here.")
|
||||
</MudText>
|
||||
</PathDropZone>
|
||||
</MudCollapse>
|
||||
</MudStack>
|
||||
<MudAlert Severity="Severity.Info" Class="mt-3">
|
||||
@T("This server does not offer the selected model right now. It stays selected, so the documents you already prepared keep working. Choosing another model means every document of the data sources behind this provider is prepared again.")
|
||||
</MudAlert>
|
||||
}
|
||||
@if (this.ServerNamedNoEmbeddingModel)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mt-3">
|
||||
@T("Your server answered, but none of the models it serves is one we know to create embeddings. Either there is none installed, or it runs under a name we do not recognize. In the latter case, your organization can describe the model in a model plugin, and it will show up here.")
|
||||
</MudAlert>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(this.dataLoadingModelsIssue))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mt-3">
|
||||
@this.dataLoadingModelsIssue
|
||||
</MudAlert>
|
||||
}
|
||||
</MudField>
|
||||
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.DataName"
|
||||
Label="@T("Instance Name")"
|
||||
Class="mb-3"
|
||||
MaxLength="40"
|
||||
Counter="40"
|
||||
Immediate="@true"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingInstanceName"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
@if (this.DataLLMProvider != LLMProviders.NONE)
|
||||
{
|
||||
<MudStack>
|
||||
<MudButton OnClick="@this.ToggleExpertSettings">
|
||||
@(this.showExpertSettings ? T("Hide Expert Settings") : T("Show Expert Settings"))
|
||||
</MudButton>
|
||||
<MudDivider />
|
||||
<MudCollapse Expanded="@this.showExpertSettings" Class="@this.GetExpertStyles">
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("Please be aware: This section is for experts only. For cloud providers, the selected tokenizer and chunk settings may not match the real embedding model limits exactly.")
|
||||
</MudJustifiedText>
|
||||
<MudNumericField
|
||||
T="int"
|
||||
@bind-Value="@this.DataTokenLimit"
|
||||
Label="@T("Token limit")"
|
||||
Class="mb-3"
|
||||
Min="1"
|
||||
Immediate="@true"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Numbers"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.ValidateTokenLimit"
|
||||
HelperText="@T("Maximum number of tokens sent to the embedding model per chunk. The default is 8,192.")"/>
|
||||
<MudNumericField
|
||||
T="int"
|
||||
@bind-Value="@this.DataEmbeddingBatchSize"
|
||||
Label="@T("Embedding batch size")"
|
||||
Class="mb-3"
|
||||
Min="1"
|
||||
Immediate="@true"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.FormatListNumbered"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.ValidateEmbeddingBatchSize"
|
||||
HelperText="@T("How many chunks are sent to the embedding provider at once. The default is 1.")"/>
|
||||
<TokenizerHint LLMProvider="@this.DataLLMProvider" Model="@this.DataModel"/>
|
||||
<PathDropZone IdPrefix="tokenizer" Disabled="@(() => this.IsEnterpriseConfiguration)" OnPathsDropped="@this.OnTokenizerPathsDropped">
|
||||
<MudStack Row="@true" Spacing="3" Class="mb-3" StretchItems="StretchItems.None" AlignItems="AlignItems.Center">
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.dataFilePath"
|
||||
TextChanged="@this.OnDataFilePathChanged"
|
||||
Label="@T("Selected file path for the custom tokenizer")"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingCustomTokenizer"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.AttachFile"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Variant="Variant.Outlined"
|
||||
Clearable="@true"
|
||||
Error="@(!string.IsNullOrWhiteSpace(this.dataCustomTokenizerValidationIssue))"
|
||||
ErrorText="@this.dataCustomTokenizerValidationIssue"
|
||||
OnClearButtonClick="@this.ClearPathTokenizer"
|
||||
/>
|
||||
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="@(this.IsEnterpriseConfiguration || this.isTokenizerFileDialogOpen)" OnClick="@this.OpenTokenizerFileDialog">
|
||||
@T("Choose File")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("You can also drag & drop the tokenizer file here.")
|
||||
</MudText>
|
||||
</PathDropZone>
|
||||
</MudCollapse>
|
||||
</MudStack>
|
||||
}
|
||||
</MudForm>
|
||||
@if (this.dataStoreWasAttempted && !this.IsProviderProhibited)
|
||||
@if (this.dataStoreWasAttempted)
|
||||
{
|
||||
<Issues IssuesData="@this.dataIssues"/>
|
||||
}
|
||||
@@ -230,7 +221,7 @@
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary" Disabled="@this.IsProviderProhibited">
|
||||
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@if (this.IsEditing)
|
||||
{
|
||||
@T("Update")
|
||||
|
||||
@@ -136,15 +136,6 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
|
||||
/// </summary>
|
||||
private List<string> UsedInstanceNames { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether the organization prohibited the selected provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The provider stays selectable, so that nobody wonders where it went. The dialog then shows
|
||||
/// the reason instead of the form, and nothing can be stored.
|
||||
/// </remarks>
|
||||
private bool IsProviderProhibited => this.SettingsManager.IsProviderProhibited(this.DataLLMProvider);
|
||||
|
||||
private bool dataIsValid;
|
||||
private string[] dataIssues = [];
|
||||
private string dataAPIKey = string.Empty;
|
||||
@@ -293,9 +284,6 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
private async Task Store()
|
||||
{
|
||||
if (this.IsProviderProhibited)
|
||||
return;
|
||||
|
||||
this.dataStoreWasAttempted = true;
|
||||
await this.dataTokenizerValidationTask;
|
||||
await this.form.Validate();
|
||||
@@ -556,10 +544,6 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
private async Task ReloadModels()
|
||||
{
|
||||
// Loading the models would contact the provider:
|
||||
if (this.IsProviderProhibited)
|
||||
return;
|
||||
|
||||
this.dataLoadingModelsIssue = string.Empty;
|
||||
this.dataServerWasAskedForItsModels = true;
|
||||
var currentEmbeddingProviderSettings = this.CreateEmbeddingProviderSettings();
|
||||
|
||||
@@ -27,338 +27,326 @@
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudButton Disabled="@(!this.DataLLMProvider.ShowRegisterButton() || this.IsProviderProhibited)" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.DataLLMProvider.GetCreationURL()" Target="_blank">
|
||||
<MudButton Disabled="@(!this.DataLLMProvider.ShowRegisterButton())" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.DataLLMProvider.GetCreationURL()" Target="_blank">
|
||||
@T("Create account")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
@if (this.IsProviderProhibited)
|
||||
@if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost))
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-3">
|
||||
@T("The use of this provider has been prohibited by your organization. Please select a different provider.")
|
||||
</MudAlert>
|
||||
<SecretInputField Secret="@this.dataAPIKey" SecretChanged="@this.OnAPIKeyChanged" Label="@this.APIKeyText" Validation="@this.providerValidation.ValidatingAPIKey"/>
|
||||
}
|
||||
else
|
||||
|
||||
@if (this.DataLLMProvider.IsHostnameNeeded())
|
||||
{
|
||||
@if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost))
|
||||
{
|
||||
<SecretInputField Secret="@this.dataAPIKey" SecretChanged="@this.OnAPIKeyChanged" Label="@this.APIKeyText" Validation="@this.providerValidation.ValidatingAPIKey"/>
|
||||
}
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.DataHostname"
|
||||
Label="@T("Hostname")"
|
||||
Class="mb-3"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingHostname"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHostnameNeeded())
|
||||
{
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.DataHostname"
|
||||
Label="@T("Hostname")"
|
||||
Class="mb-3"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingHostname"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHostNeeded())
|
||||
{
|
||||
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHost">
|
||||
@foreach (Host host in Enum.GetValues(typeof(Host)))
|
||||
@if (this.DataLLMProvider.IsHostNeeded())
|
||||
{
|
||||
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHost">
|
||||
@foreach (Host host in Enum.GetValues(typeof(Host)))
|
||||
{
|
||||
@if (host.IsChatSupported())
|
||||
{
|
||||
@if (host.IsChatSupported())
|
||||
{
|
||||
<MudSelectItem Value="@host">
|
||||
@host.Name()
|
||||
</MudSelectItem>
|
||||
}
|
||||
<MudSelectItem Value="@host">
|
||||
@host.Name()
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHFInstanceProviderNeeded())
|
||||
{
|
||||
<MudSelect T="HFInferenceProvider" Value="@this.HFInferenceProviderId" ValueChanged="@this.OnHFInferenceProviderChanged" Label="@T("Hugging Face Inference Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHFInstanceProvider">
|
||||
@foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider)))
|
||||
@if (this.DataLLMProvider.IsHFInstanceProviderNeeded())
|
||||
{
|
||||
<MudSelect T="HFInferenceProvider" Value="@this.HFInferenceProviderId" ValueChanged="@this.OnHFInferenceProviderChanged" Label="@T("Hugging Face Inference Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHFInstanceProvider">
|
||||
@foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider)))
|
||||
{
|
||||
@if (inferenceProvider.SupportsChat())
|
||||
{
|
||||
@if (inferenceProvider.SupportsChat())
|
||||
{
|
||||
<MudSelectItem Value="@inferenceProvider">
|
||||
@inferenceProvider.ToName()
|
||||
</MudSelectItem>
|
||||
}
|
||||
<MudSelectItem Value="@inferenceProvider">
|
||||
@inferenceProvider.ToName()
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudJustifiedText Class="mb-3">
|
||||
@T("Choose which inference provider should answer your requests. When you pick one of the automatic options instead, Hugging Face selects a provider for you and switches to another one when your choice is unavailable.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
<MudJustifiedText Class="mb-3">
|
||||
@T("Choose which inference provider should answer your requests. When you pick one of the automatic options instead, Hugging Face selects a provider for you and switches to another one when your choice is unavailable.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
|
||||
@if (!this.IsLLMModelSelectionHidden)
|
||||
{
|
||||
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" StretchItems="StretchItems.End">
|
||||
@if (this.DataLLMProvider.IsLLMModelProvidedManually())
|
||||
@if (!this.IsLLMModelSelectionHidden)
|
||||
{
|
||||
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" StretchItems="StretchItems.End">
|
||||
@if (this.DataLLMProvider.IsLLMModelProvidedManually())
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.ModelsOverviewURL" Target="_blank">
|
||||
@T("Show available models")
|
||||
</MudButton>
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataManuallyModel"
|
||||
Label="@T("Model")"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.FaceRetouchingNatural"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.ValidateManuallyModel"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
HelperText="@T("Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually.")"
|
||||
/>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (!string.IsNullOrWhiteSpace(this.ModelsOverviewURL))
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.ModelsOverviewURL" Target="_blank">
|
||||
@T("Show available models")
|
||||
</MudButton>
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataManuallyModel"
|
||||
Label="@T("Model")"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.FaceRetouchingNatural"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.ValidateManuallyModel"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
HelperText="@T("Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually.")"
|
||||
/>
|
||||
}
|
||||
<MudButton Disabled="@(this.IsEnterpriseConfiguration || !this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
|
||||
@T("Load models")
|
||||
</MudButton>
|
||||
@if(this.availableModels.Count is 0)
|
||||
{
|
||||
<MudText Typo="Typo.body1">
|
||||
@T("No models loaded or available.")
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (!string.IsNullOrWhiteSpace(this.ModelsOverviewURL))
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.ModelsOverviewURL" Target="_blank">
|
||||
@T("Show available models")
|
||||
</MudButton>
|
||||
}
|
||||
<MudButton Disabled="@(this.IsEnterpriseConfiguration || !this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
|
||||
@T("Load models")
|
||||
</MudButton>
|
||||
@if(this.availableModels.Count is 0)
|
||||
{
|
||||
<MudText Typo="Typo.body1">
|
||||
@T("No models loaded or available.")
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSelect T="Provider.Model"
|
||||
Value="@this.DataModel"
|
||||
ValueChanged="@(async model => await this.OnModelChanged(model))"
|
||||
OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural" AdornmentColor="Color.Info"
|
||||
Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingModel">
|
||||
@foreach (var model in this.availableModels)
|
||||
{
|
||||
<MudSelectItem Value="@model">
|
||||
@model
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
}
|
||||
</MudStack>
|
||||
@if (!string.IsNullOrWhiteSpace(this.dataLoadingModelsIssue))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mt-3">
|
||||
@this.dataLoadingModelsIssue
|
||||
</MudAlert>
|
||||
}
|
||||
</MudField>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
|
||||
<MudText Typo="Typo.body1">
|
||||
@T("This host uses the model configured at the provider level. No model selection is available.")
|
||||
</MudText>
|
||||
</MudField>
|
||||
}
|
||||
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.DataInstanceName"
|
||||
Label="@T("Instance Name")"
|
||||
Class="mb-3"
|
||||
MaxLength="40"
|
||||
Counter="40"
|
||||
Immediate="@true"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingInstanceName"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
/>
|
||||
|
||||
<MudStack>
|
||||
<MudButton OnClick="@this.ToggleExpertSettings">
|
||||
@(this.showExpertSettings ? T("Hide Expert Settings") : T("Show Expert Settings"))
|
||||
</MudButton>
|
||||
<MudDivider />
|
||||
<MudCollapse Expanded="@this.showExpertSettings" Class="@this.GetExpertStyles">
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-4">
|
||||
@T("Please be careful: wrong expert settings can break model usage, disable supported features, or make unsupported features appear available.")
|
||||
</MudAlert>
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">
|
||||
@T("Override Model Capabilities")
|
||||
</MudText>
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-4">
|
||||
@T("Stored default model capabilities may not reflect its full range. Override them here if needed.")
|
||||
</MudJustifiedText>
|
||||
<MudStack Class="mb-4" Spacing="2">
|
||||
@foreach (var capability in SWITCH_CAPABILITY_OVERRIDES)
|
||||
{
|
||||
<MudPaper Outlined="@true" Class="pa-3">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Wrap="Wrap.Wrap">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.body2">
|
||||
@this.GetCapabilityOverrideLabel(capability)
|
||||
</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
@this.GetCapabilityEffectiveLabel(capability)
|
||||
</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudSwitch T="bool"
|
||||
Value="@this.IsCapabilityEnabled(capability)"
|
||||
ValueChanged="@(value => this.OnCapabilitySwitchChanged(capability, value))"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Color="Color.Primary" />
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Default"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.RestartAlt"
|
||||
Disabled="@(this.IsEnterpriseConfiguration || !this.HasCapabilityOverride(capability))"
|
||||
OnClick="@(() => this.ResetCapabilityOverride(capability))">
|
||||
@T("Reset")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
<MudPaper Outlined="@true" Class="pa-3">
|
||||
<MudSelect T="ReasoningOverrideMode"
|
||||
Value="@this.GetReasoningOverrideMode()"
|
||||
ValueChanged="@this.SetReasoningOverrideMode"
|
||||
Label="@T("Reasoning (thinking) behavior")"
|
||||
HelperText="@this.GetReasoningOverrideModeDescription(this.GetReasoningOverrideMode())"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
OpenIcon="@Icons.Material.Filled.Psychology"
|
||||
AdornmentColor="Color.Info"
|
||||
Adornment="Adornment.Start"
|
||||
Disabled="@this.IsEnterpriseConfiguration">
|
||||
@foreach (var mode in REASONING_OVERRIDE_MODES)
|
||||
<MudSelect T="Provider.Model"
|
||||
Value="@this.DataModel"
|
||||
ValueChanged="@(async model => await this.OnModelChanged(model))"
|
||||
OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural" AdornmentColor="Color.Info"
|
||||
Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingModel">
|
||||
@foreach (var model in this.availableModels)
|
||||
{
|
||||
<MudSelectItem Value="@mode">
|
||||
@this.GetReasoningOverrideModeLabel(mode)
|
||||
<MudSelectItem Value="@model">
|
||||
@model
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">
|
||||
@T("Override Model Limits")
|
||||
</MudText>
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-4">
|
||||
@T("Where the stored numbers do not match your installation, state yours here. This matters most for self-hosted models: they run with whatever their operator configured, which the model card cannot know.")
|
||||
</MudJustifiedText>
|
||||
<MudStack Class="mb-4" Spacing="2">
|
||||
}
|
||||
}
|
||||
</MudStack>
|
||||
@if (!string.IsNullOrWhiteSpace(this.dataLoadingModelsIssue))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mt-3">
|
||||
@this.dataLoadingModelsIssue
|
||||
</MudAlert>
|
||||
}
|
||||
</MudField>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
|
||||
<MudText Typo="Typo.body1">
|
||||
@T("This host uses the model configured at the provider level. No model selection is available.")
|
||||
</MudText>
|
||||
</MudField>
|
||||
}
|
||||
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.DataInstanceName"
|
||||
Label="@T("Instance Name")"
|
||||
Class="mb-3"
|
||||
MaxLength="40"
|
||||
Counter="40"
|
||||
Immediate="@true"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingInstanceName"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
/>
|
||||
|
||||
<MudStack>
|
||||
<MudButton OnClick="@this.ToggleExpertSettings">
|
||||
@(this.showExpertSettings ? T("Hide Expert Settings") : T("Show Expert Settings"))
|
||||
</MudButton>
|
||||
<MudDivider />
|
||||
<MudCollapse Expanded="@this.showExpertSettings" Class="@this.GetExpertStyles">
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-4">
|
||||
@T("Please be careful: wrong expert settings can break model usage, disable supported features, or make unsupported features appear available.")
|
||||
</MudAlert>
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">
|
||||
@T("Override Model Capabilities")
|
||||
</MudText>
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-4">
|
||||
@T("Stored default model capabilities may not reflect its full range. Override them here if needed.")
|
||||
</MudJustifiedText>
|
||||
<MudStack Class="mb-4" Spacing="2">
|
||||
@foreach (var capability in SWITCH_CAPABILITY_OVERRIDES)
|
||||
{
|
||||
<MudPaper Outlined="@true" Class="pa-3">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Wrap="Wrap.Wrap">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.body2">
|
||||
@this.GetCapabilityOverrideLabel(capability)
|
||||
</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
@this.GetCapabilityEffectiveLabel(capability)
|
||||
</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudSwitch T="bool"
|
||||
Value="@this.IsCapabilityEnabled(capability)"
|
||||
ValueChanged="@(value => this.OnCapabilitySwitchChanged(capability, value))"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Color="Color.Primary" />
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Default"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.RestartAlt"
|
||||
Disabled="@(this.IsEnterpriseConfiguration || !this.HasCapabilityOverride(capability))"
|
||||
OnClick="@(() => this.ResetCapabilityOverride(capability))">
|
||||
@T("Reset")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
<MudPaper Outlined="@true" Class="pa-3">
|
||||
<MudSelect T="ReasoningOverrideMode"
|
||||
Value="@this.GetReasoningOverrideMode()"
|
||||
ValueChanged="@this.SetReasoningOverrideMode"
|
||||
Label="@T("Reasoning (thinking) behavior")"
|
||||
HelperText="@this.GetReasoningOverrideModeDescription(this.GetReasoningOverrideMode())"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
OpenIcon="@Icons.Material.Filled.Psychology"
|
||||
AdornmentColor="Color.Info"
|
||||
Adornment="Adornment.Start"
|
||||
Disabled="@this.IsEnterpriseConfiguration">
|
||||
@foreach (var mode in REASONING_OVERRIDE_MODES)
|
||||
{
|
||||
<MudSelectItem Value="@mode">
|
||||
@this.GetReasoningOverrideModeLabel(mode)
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">
|
||||
@T("Override Model Limits")
|
||||
</MudText>
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-4">
|
||||
@T("Where the stored numbers do not match your installation, state yours here. This matters most for self-hosted models: they run with whatever their operator configured, which the model card cannot know.")
|
||||
</MudJustifiedText>
|
||||
<MudStack Class="mb-4" Spacing="2">
|
||||
<MudPaper Outlined="@true" Class="pa-3">
|
||||
<MudNumericField T="int?"
|
||||
Value="@this.capabilityOverrides.ContextWindowTokens"
|
||||
ValueChanged="@this.SetContextWindowOverride"
|
||||
Label="@T("Context window in tokens")"
|
||||
HelperText="@this.ContextWindowHelperText"
|
||||
Placeholder="@this.AutomaticContextWindowPlaceholder"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Min="1"
|
||||
HideSpinButtons="@true"
|
||||
Clearable="@true"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Straighten"
|
||||
AdornmentColor="Color.Info" />
|
||||
</MudPaper>
|
||||
<MudPaper Outlined="@true" Class="pa-3">
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("Images")
|
||||
</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
@this.ImageLimitsEffectiveLabel
|
||||
</MudText>
|
||||
<MudStack Row="@true" Spacing="3" Wrap="Wrap.Wrap" Class="border-dashed border rounded-lg pa-3 mt-3">
|
||||
<MudNumericField T="int?"
|
||||
Value="@this.capabilityOverrides.ContextWindowTokens"
|
||||
ValueChanged="@this.SetContextWindowOverride"
|
||||
Label="@T("Context window in tokens")"
|
||||
HelperText="@this.ContextWindowHelperText"
|
||||
Placeholder="@this.AutomaticContextWindowPlaceholder"
|
||||
Value="@this.capabilityOverrides.MaxImagesPerMessage"
|
||||
ValueChanged="@this.SetMaxImagesPerMessageOverride"
|
||||
Label="@T("Per message")"
|
||||
Placeholder="@this.AutomaticImageLimitPlaceholder(this.GetAutomaticModelProfile().Images.MaxPerMessage)"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Min="1"
|
||||
HideSpinButtons="@true"
|
||||
Min="0"
|
||||
Clearable="@true"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Straighten"
|
||||
AdornmentColor="Color.Info" />
|
||||
</MudPaper>
|
||||
<MudPaper Outlined="@true" Class="pa-3">
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("Images")
|
||||
</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
@this.ImageLimitsEffectiveLabel
|
||||
</MudText>
|
||||
<MudStack Row="@true" Spacing="3" Wrap="Wrap.Wrap" Class="border-dashed border rounded-lg pa-3 mt-3">
|
||||
<MudNumericField T="int?"
|
||||
Value="@this.capabilityOverrides.MaxImagesPerMessage"
|
||||
ValueChanged="@this.SetMaxImagesPerMessageOverride"
|
||||
Label="@T("Per message")"
|
||||
Placeholder="@this.AutomaticImageLimitPlaceholder(this.GetAutomaticModelProfile().Images.MaxPerMessage)"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Min="0"
|
||||
Clearable="@true"
|
||||
Disabled="@this.IsEnterpriseConfiguration" />
|
||||
<MudNumericField T="int?"
|
||||
Value="@this.capabilityOverrides.MaxImagesPerRequest"
|
||||
ValueChanged="@this.SetMaxImagesPerRequestOverride"
|
||||
Label="@T("Per request")"
|
||||
Placeholder="@this.AutomaticImageLimitPlaceholder(this.GetAutomaticModelProfile().Images.MaxPerRequest)"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Min="0"
|
||||
Clearable="@true"
|
||||
Disabled="@this.IsEnterpriseConfiguration" />
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mt-2">
|
||||
@T("Vendors state one of the two, the other, or neither. Whichever is smaller decides how many images one message may carry; an empty field states nothing.")
|
||||
</MudText>
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-4">
|
||||
@string.Format(T("The current model uses the {0}."), this.GetCurrentModelApiLabel())
|
||||
Disabled="@this.IsEnterpriseConfiguration" />
|
||||
<MudNumericField T="int?"
|
||||
Value="@this.capabilityOverrides.MaxImagesPerRequest"
|
||||
ValueChanged="@this.SetMaxImagesPerRequestOverride"
|
||||
Label="@T("Per request")"
|
||||
Placeholder="@this.AutomaticImageLimitPlaceholder(this.GetAutomaticModelProfile().Images.MaxPerRequest)"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Min="0"
|
||||
Clearable="@true"
|
||||
Disabled="@this.IsEnterpriseConfiguration" />
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mt-2">
|
||||
@T("Vendors state one of the two, the other, or neither. Whichever is smaller decides how many images one message may carry; an empty field states nothing.")
|
||||
</MudText>
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-4">
|
||||
@string.Format(T("The current model uses the {0}."), this.GetCurrentModelApiLabel())
|
||||
</MudJustifiedText>
|
||||
<MudTextField T="string" Label=@T("Additional API parameters") Variant="Variant.Outlined" Lines="4" AutoGrow="true" MaxLines="10" HelperText=@T("""Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though.""") Placeholder="@GetPlaceholderExpertSettings" @bind-Value="@this.AdditionalJsonApiParameters" Immediate="true" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.ValidateAdditionalJsonApiParameters" OnBlur="@this.OnInputChangeExpertSettings"/>
|
||||
@if (this.ShowTokenizerSettings)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mt-4 mb-3">
|
||||
@T("For better token estimates, you can configure a custom tokenizer for this provider.")
|
||||
</MudJustifiedText>
|
||||
<MudTextField T="string" Label=@T("Additional API parameters") Variant="Variant.Outlined" Lines="4" AutoGrow="true" MaxLines="10" HelperText=@T("""Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though.""") Placeholder="@GetPlaceholderExpertSettings" @bind-Value="@this.AdditionalJsonApiParameters" Immediate="true" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.ValidateAdditionalJsonApiParameters" OnBlur="@this.OnInputChangeExpertSettings"/>
|
||||
@if (this.ShowTokenizerSettings)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mt-4 mb-3">
|
||||
@T("For better token estimates, you can configure a custom tokenizer for this provider.")
|
||||
</MudJustifiedText>
|
||||
<TokenizerHint LLMProvider="@this.DataLLMProvider" Model="@this.DataModel"/>
|
||||
<PathDropZone IdPrefix="tokenizer" Disabled="@(() => this.IsEnterpriseConfiguration)" OnPathsDropped="@this.OnTokenizerPathsDropped">
|
||||
<MudStack Row="@true" Spacing="3" Class="mb-3" StretchItems="StretchItems.None" AlignItems="AlignItems.Center">
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.dataFilePath"
|
||||
TextChanged="@this.OnDataFilePathChanged"
|
||||
Label="@T("Selected file path for the custom tokenizer")"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingCustomTokenizer"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.AttachFile"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Variant="Variant.Outlined"
|
||||
Clearable="@true"
|
||||
Error="@(!string.IsNullOrWhiteSpace(this.dataCustomTokenizerValidationIssue))"
|
||||
ErrorText="@this.dataCustomTokenizerValidationIssue"
|
||||
OnClearButtonClick="@this.ClearPathTokenizer"
|
||||
/>
|
||||
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="@(this.IsEnterpriseConfiguration || this.isTokenizerFileDialogOpen)" OnClick="@this.OpenTokenizerFileDialog">
|
||||
@T("Choose File")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("You can also drag & drop the tokenizer file here.")
|
||||
</MudText>
|
||||
</PathDropZone>
|
||||
}
|
||||
</MudCollapse>
|
||||
</MudStack>
|
||||
}
|
||||
<TokenizerHint LLMProvider="@this.DataLLMProvider" Model="@this.DataModel"/>
|
||||
<PathDropZone IdPrefix="tokenizer" Disabled="@(() => this.IsEnterpriseConfiguration)" OnPathsDropped="@this.OnTokenizerPathsDropped">
|
||||
<MudStack Row="@true" Spacing="3" Class="mb-3" StretchItems="StretchItems.None" AlignItems="AlignItems.Center">
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.dataFilePath"
|
||||
TextChanged="@this.OnDataFilePathChanged"
|
||||
Label="@T("Selected file path for the custom tokenizer")"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingCustomTokenizer"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.AttachFile"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Variant="Variant.Outlined"
|
||||
Clearable="@true"
|
||||
Error="@(!string.IsNullOrWhiteSpace(this.dataCustomTokenizerValidationIssue))"
|
||||
ErrorText="@this.dataCustomTokenizerValidationIssue"
|
||||
OnClearButtonClick="@this.ClearPathTokenizer"
|
||||
/>
|
||||
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="@(this.IsEnterpriseConfiguration || this.isTokenizerFileDialogOpen)" OnClick="@this.OpenTokenizerFileDialog">
|
||||
@T("Choose File")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("You can also drag & drop the tokenizer file here.")
|
||||
</MudText>
|
||||
</PathDropZone>
|
||||
}
|
||||
</MudCollapse>
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
@if (!this.IsProviderProhibited)
|
||||
{
|
||||
<Issues IssuesData="@this.dataIssues"/>
|
||||
}
|
||||
<Issues IssuesData="@this.dataIssues"/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary" Disabled="@this.IsProviderProhibited">
|
||||
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@if(this.IsEditing)
|
||||
{
|
||||
@T("Update")
|
||||
|
||||
@@ -154,15 +154,6 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
/// </summary>
|
||||
private List<string> UsedInstanceNames { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether the organization prohibited the selected provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The provider stays selectable, so that nobody wonders where it went. The dialog then shows
|
||||
/// the reason instead of the form, and nothing can be stored.
|
||||
/// </remarks>
|
||||
private bool IsProviderProhibited => this.SettingsManager.IsProviderProhibited(this.DataLLMProvider);
|
||||
|
||||
private bool dataIsValid;
|
||||
private string[] dataIssues = [];
|
||||
private string dataAPIKey = string.Empty;
|
||||
@@ -342,9 +333,6 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
private async Task Store()
|
||||
{
|
||||
if (this.IsProviderProhibited)
|
||||
return;
|
||||
|
||||
this.dataStoreWasAttempted = true;
|
||||
await this.dataTokenizerValidationTask;
|
||||
await this.form.Validate();
|
||||
@@ -605,10 +593,6 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
private async Task ReloadModels()
|
||||
{
|
||||
// Loading the models would contact the provider:
|
||||
if (this.IsProviderProhibited)
|
||||
return;
|
||||
|
||||
this.dataLoadingModelsIssue = string.Empty;
|
||||
var currentProviderSettings = this.CreateProviderSettings();
|
||||
var provider = currentProviderSettings.CreateProvider();
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
@using AIStudio.Settings
|
||||
@inherits SettingsDialogBase
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6" Class="d-flex align-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AutoMode" Class="mr-2"/>
|
||||
@T("Assistant: Assistant Builder defaults")
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||
<ConfigurationOption OptionDescription="@T("Preselect Assistant Builder options?")" LabelOn="@T("Assistant Builder options are preselected")" LabelOff="@T("No Assistant Builder options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectOptions)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectOptions = value)" OptionHelp="@T("When enabled, the Assistant Builder starts with the defaults configured below. When disabled, it uses the app-wide default provider and preselects no output language.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.AssistantBuilder, x => x.PreselectOptions, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Default output language")" Disabled="@this.DefaultsDisabled" SelectedValue="@(() => this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedOutputLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesOptionalData()" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedOutputLanguage = value)" OptionHelp="@T("The language in which the assistants you build answer.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.AssistantBuilder, x => x.PreselectedOutputLanguage, out var meta) && meta.IsLocked"/>
|
||||
@if (this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedOutputLanguage is CommonLanguages.OTHER)
|
||||
{
|
||||
<ConfigurationText OptionDescription="@T("Default custom output language")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedOtherOutputLanguage)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedOtherOutputLanguage = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.AssistantBuilder, x => x.PreselectedOtherOutputLanguage, out var meta) && meta.IsLocked"/>
|
||||
}
|
||||
<ConfigurationMinConfidenceSelection Disabled="@this.DefaultsDisabled" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.AssistantBuilder.MinimumProviderConfidence)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.AssistantBuilder.MinimumProviderConfidence = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.AssistantBuilder, x => x.MinimumProviderConfidence, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationProviderSelection Component="Components.META_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@this.DefaultsDisabled" SelectedValue="@(() => this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedProvider)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedProvider = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.AssistantBuilder, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/>
|
||||
</MudPaper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">@T("Close")</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace AIStudio.Dialogs.Settings;
|
||||
|
||||
public partial class SettingsDialogAssistantBuilder : SettingsDialogBase
|
||||
{
|
||||
private bool DefaultsDisabled() => !this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectOptions;
|
||||
}
|
||||
@@ -20,9 +20,18 @@
|
||||
<MudText Typo="Typo.h6" Class="mb-3">@T("Input")</MudText>
|
||||
<ConfigurationDirectory OptionDescription="@T("Default input folder")" Disabled="@this.DefaultsDisabled" DirectoryDialogTitle="@T("Select the default input folder")" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.InputDirectory)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.InputDirectory = value)" OptionHelp="@T("Leave empty when an input folder should be selected for every batch run.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.InputDirectory, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationOption OptionDescription="@T("Include subfolders by default?")" Disabled="@this.DefaultsDisabled" LabelOn="@T("Subfolders are included")" LabelOff="@T("Only the selected folder is processed")" State="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.IncludeSubdirectories)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.IncludeSubdirectories = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.IncludeSubdirectories, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationOption OptionDescription="@T("Include hidden files and folders by default?")" Disabled="@this.DefaultsDisabled" LabelOn="@T("Hidden files and folders are included")" LabelOff="@T("Hidden files and folders are ignored")" State="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.IncludeHiddenFilesAndFolders)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.IncludeHiddenFilesAndFolders = value)" OptionHelp="@T("Hidden are files and folders whose names start with a dot, such as .git or .env, and those hidden by the operating system. A file pattern which starts with a dot, such as .gitignore, selects such files anyway.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.IncludeHiddenFilesAndFolders, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationText OptionDescription="@T("Default file patterns")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.FilterAlt" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.FilePatterns)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.FilePatterns = value)" ResetValue="@(() => DataBatchProcessing.DEFAULT_FILE_PATTERNS)" ResetButtonText="@T("Restore default patterns")" OptionHelp="@T("Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. The standard patterns include all supported audio and video formats as well as images in JPG, PNG, WebP, GIF, BMP, and TIFF format.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.FilePatterns, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationText OptionDescription="@T("Default excluded files and folders")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.FilterAltOff" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.ExcludePatterns)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.ExcludePatterns = value)" NumLines="@FileExclusions.DEFAULT_LINE_COUNT" MaxLines="@FileExclusions.MAX_LINE_COUNT" ResetValue="@(() => FileExclusions.DEFAULT_PATTERNS)" ResetButtonText="@T("Restore default")" OptionHelp="@T("These files and folders are left out, even when a file pattern selects them. Separate entries with a semicolon or a line break. End an entry with a slash to exclude every folder of that name, e.g., node_modules/. Other entries name files, e.g., *.min.js.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.ExcludePatterns, out var meta) && meta.IsLocked"/>
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2 mt-4">@T("Hidden files and folders")</MudText>
|
||||
<ConfigurationOption OptionDescription="@T("Include hidden files and folders by default?")" Disabled="@this.DefaultsDisabled" LabelOn="@T("Yes, include hidden files and folders")" LabelOff="@T("No, ignore hidden files and folders")" State="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.IncludeHiddenFilesAndFolders)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.IncludeHiddenFilesAndFolders = value)" OptionHelp="@T("Files and folders are hidden if their names start with a dot or the operating system marks them as hidden. If you choose 'No', hidden items are skipped. To explicitly include a file such as .gitignore, enter its name under 'Files to include'. Using generic patterns such as *, :text: or :documents: still skips hidden files. Exclusion rules always take priority; you can change them in the fields below.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.IncludeHiddenFilesAndFolders, out var meta) && meta.IsLocked"/>
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2 mt-4">@T("Files to include")</MudText>
|
||||
<FilePatternSelection>
|
||||
<AllowedContent>
|
||||
<ConfigurationText Compact="@true" ExpandToContent="@true" OptionDescription="@T("Files to include")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.FilterAlt" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.FilePatterns)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.FilePatterns = value)" ResetValue="@(() => DataBatchProcessing.DEFAULT_FILE_PATTERNS)" ResetButtonText="@T("Restore default patterns")" ResetButtonIconOnly="@true" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.FilePatterns, out var meta) && meta.IsLocked"/>
|
||||
</AllowedContent>
|
||||
<ExcludedContent>
|
||||
<ConfigurationFileExclusions Compact="@true" ShowHelp="@false" OptionDescription="@T("Default excluded files and folders")" Disabled="@this.DefaultsDisabled" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.ExcludePatterns)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.ExcludePatterns = value)" Validation="@FileExclusions.Validate" ResetValue="@(() => FileExclusions.DEFAULT_PATTERNS)" ResetButtonText="@T("Restore default")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.ExcludePatterns, out var meta) && meta.IsLocked"/>
|
||||
</ExcludedContent>
|
||||
</FilePatternSelection>
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2 mt-4">@T("File size limit")</MudText>
|
||||
<ConfigurationNumber T="int" OptionDescription="@T("Ignore files larger than")" Min="@FolderFileSelection.SMALLEST_MAX_FILE_SIZE_MEGABYTES" Unit="MB" Disabled="@this.DefaultsDisabled" Value="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.MaxFileSizeMegabytes)" ValueUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.MaxFileSizeMegabytes = value)" OptionHelp="@T("Matching files above this size are left out of a batch run, e.g., images of virtual machines.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.MaxFileSizeMegabytes, out var meta) && meta.IsLocked"/>
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">@T("Instructions")</MudText>
|
||||
|
||||
@@ -114,9 +114,6 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
||||
{ x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage },
|
||||
{ x => x.ToolIds, chatTemplate.ToolIds },
|
||||
{ x => x.DataSourceOptions, chatTemplate.DataSourceOptions },
|
||||
{ x => x.ProfileId, chatTemplate.ProfileId },
|
||||
{ x => x.ProviderId, chatTemplate.ProviderId },
|
||||
{ x => x.WorkspaceName, chatTemplate.WorkspaceName },
|
||||
};
|
||||
|
||||
private async Task DeleteChatTemplate(ChatTemplate chatTemplate)
|
||||
|
||||
@@ -25,146 +25,134 @@
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
<MudButton Disabled="@(!this.DataLLMProvider.ShowRegisterButton() || this.IsProviderProhibited)" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.DataLLMProvider.GetCreationURL()" Target="_blank">
|
||||
<MudButton Disabled="@(!this.DataLLMProvider.ShowRegisterButton())" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.DataLLMProvider.GetCreationURL()" Target="_blank">
|
||||
@T("Create account")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
@if (this.IsProviderProhibited)
|
||||
@if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost))
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-3">
|
||||
@T("The use of this provider has been prohibited by your organization. Please select a different provider.")
|
||||
</MudAlert>
|
||||
<SecretInputField Secret="@this.dataAPIKey" SecretChanged="@this.OnAPIKeyChanged" Label="@this.APIKeyText" Validation="@this.providerValidation.ValidatingAPIKey"/>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHostnameNeeded())
|
||||
{
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.DataHostname"
|
||||
Label="@T("Hostname")"
|
||||
Class="mb-3"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingHostname"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHostNeeded())
|
||||
{
|
||||
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHost">
|
||||
@foreach (Host host in Enum.GetValues(typeof(Host)))
|
||||
{
|
||||
if (host.IsTranscriptionSupported())
|
||||
{
|
||||
<MudSelectItem Value="@host">
|
||||
@host.Name()
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHFInstanceProviderNeeded())
|
||||
{
|
||||
<MudSelect T="HFInferenceProvider" Value="@this.HFInferenceProviderId" ValueChanged="@this.OnHFInferenceProviderChanged" Label="@T("Hugging Face Inference Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHFInstanceProviderForTranscription">
|
||||
@foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider)))
|
||||
{
|
||||
@if (inferenceProvider.SupportsTranscription())
|
||||
{
|
||||
<MudSelectItem Value="@inferenceProvider">
|
||||
@inferenceProvider.ToName()
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
<MudJustifiedText Class="mb-3">
|
||||
@T("Hugging Face transcribes audio through a few of its inference providers only, which is why this list is shorter than the one for chatting.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
|
||||
@if (!this.DataLLMProvider.IsTranscriptionModelSelectionHidden(this.DataHost))
|
||||
{
|
||||
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" StretchItems="StretchItems.End">
|
||||
<MudButton Disabled="@(this.IsEnterpriseConfiguration || !this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
|
||||
@T("Load")
|
||||
</MudButton>
|
||||
@if(this.availableModels.Count is 0)
|
||||
{
|
||||
<MudText Typo="Typo.body1">
|
||||
@T("No models loaded or available.")
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSelect Disabled="@(this.IsEnterpriseConfiguration || this.IsNoneProvider)" @bind-Value="@this.DataModel" Label="@T("Model")"
|
||||
OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural"
|
||||
AdornmentColor="Color.Info" Adornment="Adornment.Start"
|
||||
Validation="@this.providerValidation.ValidatingModel">
|
||||
@foreach (var model in this.availableModels)
|
||||
{
|
||||
<MudSelectItem Value="@model">
|
||||
@model
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
</MudStack>
|
||||
@if (!string.IsNullOrWhiteSpace(this.dataLoadingModelsIssue))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mt-3">
|
||||
@this.dataLoadingModelsIssue
|
||||
</MudAlert>
|
||||
}
|
||||
</MudField>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost))
|
||||
{
|
||||
<SecretInputField Secret="@this.dataAPIKey" SecretChanged="@this.OnAPIKeyChanged" Label="@this.APIKeyText" Validation="@this.providerValidation.ValidatingAPIKey"/>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHostnameNeeded())
|
||||
{
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.DataHostname"
|
||||
Label="@T("Hostname")"
|
||||
Class="mb-3"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingHostname"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHostNeeded())
|
||||
{
|
||||
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHost">
|
||||
@foreach (Host host in Enum.GetValues(typeof(Host)))
|
||||
{
|
||||
if (host.IsTranscriptionSupported())
|
||||
{
|
||||
<MudSelectItem Value="@host">
|
||||
@host.Name()
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHFInstanceProviderNeeded())
|
||||
{
|
||||
<MudSelect T="HFInferenceProvider" Value="@this.HFInferenceProviderId" ValueChanged="@this.OnHFInferenceProviderChanged" Label="@T("Hugging Face Inference Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHFInstanceProviderForTranscription">
|
||||
@foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider)))
|
||||
{
|
||||
@if (inferenceProvider.SupportsTranscription())
|
||||
{
|
||||
<MudSelectItem Value="@inferenceProvider">
|
||||
@inferenceProvider.ToName()
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
<MudJustifiedText Class="mb-3">
|
||||
@T("Hugging Face transcribes audio through a few of its inference providers only, which is why this list is shorter than the one for chatting.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
|
||||
@if (!this.DataLLMProvider.IsTranscriptionModelSelectionHidden(this.DataHost))
|
||||
{
|
||||
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" StretchItems="StretchItems.End">
|
||||
<MudButton Disabled="@(this.IsEnterpriseConfiguration || !this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
|
||||
@T("Load")
|
||||
</MudButton>
|
||||
@if(this.availableModels.Count is 0)
|
||||
{
|
||||
<MudText Typo="Typo.body1">
|
||||
@T("No models loaded or available.")
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSelect Disabled="@(this.IsEnterpriseConfiguration || this.IsNoneProvider)" @bind-Value="@this.DataModel" Label="@T("Model")"
|
||||
OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural"
|
||||
AdornmentColor="Color.Info" Adornment="Adornment.Start"
|
||||
Validation="@this.providerValidation.ValidatingModel">
|
||||
@foreach (var model in this.availableModels)
|
||||
{
|
||||
<MudSelectItem Value="@model">
|
||||
@model
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
</MudStack>
|
||||
@if (!string.IsNullOrWhiteSpace(this.dataLoadingModelsIssue))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mt-3">
|
||||
@this.dataLoadingModelsIssue
|
||||
</MudAlert>
|
||||
}
|
||||
</MudField>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
|
||||
<MudText Typo="Typo.body1">
|
||||
@T("This host uses the model configured at the provider level. No model selection is available.")
|
||||
</MudText>
|
||||
</MudField>
|
||||
}
|
||||
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.DataName"
|
||||
Label="@T("Instance Name")"
|
||||
Class="mb-3"
|
||||
MaxLength="40"
|
||||
Counter="40"
|
||||
Immediate="@true"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingInstanceName"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
/>
|
||||
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
|
||||
<MudText Typo="Typo.body1">
|
||||
@T("This host uses the model configured at the provider level. No model selection is available.")
|
||||
</MudText>
|
||||
</MudField>
|
||||
}
|
||||
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.DataName"
|
||||
Label="@T("Instance Name")"
|
||||
Class="mb-3"
|
||||
MaxLength="40"
|
||||
Counter="40"
|
||||
Immediate="@true"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingInstanceName"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
/>
|
||||
|
||||
</MudForm>
|
||||
@if (!this.IsProviderProhibited)
|
||||
{
|
||||
<Issues IssuesData="@this.dataIssues"/>
|
||||
}
|
||||
<Issues IssuesData="@this.dataIssues"/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary" Disabled="@this.IsProviderProhibited">
|
||||
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@if(this.IsEditing)
|
||||
{
|
||||
@T("Update")
|
||||
|
||||
@@ -108,15 +108,6 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
|
||||
/// </summary>
|
||||
private List<string> UsedInstanceNames { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether the organization prohibited the selected provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The provider stays selectable, so that nobody wonders where it went. The dialog then shows
|
||||
/// the reason instead of the form, and nothing can be stored.
|
||||
/// </remarks>
|
||||
private bool IsProviderProhibited => this.SettingsManager.IsProviderProhibited(this.DataLLMProvider);
|
||||
|
||||
private bool dataIsValid;
|
||||
private string[] dataIssues = [];
|
||||
private string dataAPIKey = string.Empty;
|
||||
@@ -248,9 +239,6 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
private async Task Store()
|
||||
{
|
||||
if (this.IsProviderProhibited)
|
||||
return;
|
||||
|
||||
await this.form.Validate();
|
||||
this.dataAPIKeyStorageIssue = string.Empty;
|
||||
|
||||
@@ -340,10 +328,6 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
private async Task ReloadModels()
|
||||
{
|
||||
// Loading the models would contact the provider:
|
||||
if (this.IsProviderProhibited)
|
||||
return;
|
||||
|
||||
this.dataLoadingModelsIssue = string.Empty;
|
||||
var currentTranscriptionProviderSettings = this.CreateTranscriptionProviderSettings();
|
||||
var provider = currentTranscriptionProviderSettings.CreateProvider();
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<AssistantBlock TSettings="SettingsDialogRewrite" Component="Components.REWRITE_ASSISTANT" Name="@T("Rewrite & Improve")" Description="@T("Rewrite and improve a given text for a chosen style.")" Icon="@Icons.Material.Filled.Edit" Link="@Routes.ASSISTANT_REWRITE"/>
|
||||
<AssistantBlock TSettings="SettingsDialogPromptOptimizer" Component="Components.PROMPT_OPTIMIZER_ASSISTANT" Name="@T("Prompt Optimizer")" Description="@T("Optimize your prompt using a structured guideline.")" Icon="@Icons.Material.Filled.AutoFixHigh" Link="@Routes.ASSISTANT_PROMPT_OPTIMIZER"/>
|
||||
<AssistantBlock TSettings="SettingsDialogSynonyms" Component="Components.SYNONYMS_ASSISTANT" Name="@T("Synonyms")" Description="@T("Find synonyms for a given word or phrase.")" Icon="@Icons.Material.Filled.Spellcheck" Link="@Routes.ASSISTANT_SYNONYMS"/>
|
||||
<AssistantBlock TSettings="SettingsDialogAssistantBuilder" Component="Components.META_ASSISTANT" RequiredPreviewFeature="PreviewFeatures.PRE_META_ASSISTANT_V1" Name="@T("Assistant Builder")" Description="@T("Generate your own assistants.")" Icon="@Icons.Material.Filled.AutoMode" Link="@Routes.ASSISTANT_META_ASSISTANT"/>
|
||||
<AssistantBlock TSettings="NoSettingsPanel" Component="Components.META_ASSISTANT" RequiredPreviewFeature="PreviewFeatures.PRE_META_ASSISTANT_V1" Name="@T("Assistant Builder")" Description="@T("Generate your own assistants.")" Icon="@Icons.Material.Filled.AutoMode" Link="@Routes.ASSISTANT_META_ASSISTANT"/>
|
||||
</AssistantCategoryBlock>
|
||||
|
||||
@if (this.AssistantPlugins.Count > 0)
|
||||
|
||||
@@ -296,7 +296,7 @@
|
||||
@*
|
||||
What the user has to decide comes with the buttons to decide it. The
|
||||
sentence above says what is at stake; reading it and then having to find
|
||||
the settings of the mailbox or the data source is where people give up.
|
||||
the settings of the mailbox is where people give up.
|
||||
*@
|
||||
@if (status.Attention is DataSourceAttention.AUTH_FAILED)
|
||||
{
|
||||
@@ -320,14 +320,6 @@
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
}
|
||||
else if (status.Attention is DataSourceAttention.LOCATION_UNREACHABLE && this.CanEditDataSource(status))
|
||||
{
|
||||
<MudStack Row="@true" Wrap="Wrap.Wrap" Spacing="2">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Edit" OnClick="@(async () => await this.EditDataSource(status))">
|
||||
@T("Edit this data source")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
}
|
||||
</MudStack>
|
||||
</ChildContent>
|
||||
</MudExpansionPanel>
|
||||
|
||||
@@ -509,25 +509,6 @@ public partial class Embeddings : MSGComponentBase
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Not offered for a data source the organization manages: its settings cannot be edited.
|
||||
/// </remarks>
|
||||
private bool CanEditDataSource(DataSourceEmbeddingStatus status) => DataSourceEditing.CanEdit(this.SettingsManager, status.DataSourceId);
|
||||
|
||||
/// <summary>
|
||||
/// Opens the settings of a data source, e.g. to select the new location of a moved folder.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Saving them indexes the data source right away, and that run reconnects what is already
|
||||
/// stored to the new location. The page reloads through CONFIGURATION_CHANGED, which it
|
||||
/// receives like everybody else.
|
||||
/// </remarks>
|
||||
private async Task EditDataSource(DataSourceEmbeddingStatus status)
|
||||
{
|
||||
if (await DataSourceEditing.EditAsync(this.DialogService, this.SettingsManager, this.DataSourceEmbeddingService, status.DataSourceId))
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Agrees to the removal a mailbox held back, for the very number the user read.
|
||||
/// </summary>
|
||||
|
||||
@@ -153,8 +153,6 @@ ASSISTANT = {
|
||||
- A selected chat template supplies the chat system prompt, profile allowance, predefined user prompt, attachments, and cloned example conversation. A launcher `SystemPrompt`, if retained in an older plugin, is ignored, so there is never a second competing system prompt.
|
||||
- When the selected chat template does not allow profiles, the template wins: the launcher `ProfileId` is dropped and the chat starts without a profile. This matches the disabled profile selection such a template produces in the chat.
|
||||
- A chat template may preselect tools and data sources as well. When it does, it decides them alone: the launcher `ToolIds` and `DataSourceIds` are dropped, and AI Studio writes a warning into the log naming both sides. The rule is the same for tools and for data sources, so there is only one to remember.
|
||||
- A chat template may also preselect a provider and a profile, and the same rule applies: they win over the launcher `ProviderId` and `ProfileId`, with a warning in the log. A provider of the template which is gone or does not meet the confidence level required for chats, and a profile of the template which is gone, are left out instead of stopping the launch; the launcher then decides as if the template named none.
|
||||
- A chat template may name a workspace for its chats as well. Here the launcher wins when it names a workspace of its own, because that choice was made on purpose. A launcher that opens a disappearing chat opens it in the workspace of its template instead, which is created when it does not exist yet; if that fails, the chat stays a disappearing chat.
|
||||
- The reason the template wins as a whole rather than field by field is a difference in what the two can express. A launcher can only ever say "these sources, picked by hand", while a chat template carries the whole data source options and can also say "let an agent pick the sources for each message". A mix of both would be something neither of them asked for.
|
||||
- The data sources of a chat template are checked exactly like the ones of a launcher: a source which no longer exists, or which is not permitted for the effective provider, stops the launch before a workspace or chat is created. The message then names the chat template as the cause, not the launcher. A template which leaves the choice to an agent names no source and is therefore not checked here; that decision is made per message in the chat.
|
||||
- The predefined user prompt and the attachments of the selected chat template are placed into the chat input, unless the user already has an unsent draft there.
|
||||
|
||||
@@ -276,13 +276,6 @@ CONFIG["EMBEDDING_PROVIDERS"] = {}
|
||||
-- }
|
||||
|
||||
-- ERI v1 data sources for retrieval-augmented generation:
|
||||
--
|
||||
-- "OutboundDataRestriction" is optional and decides where a chat may still send data once the
|
||||
-- AI found something in the data source: ONLY_CONFIGURED_SERVICES, ONLY_LINKS_FROM_CHAT, or
|
||||
-- UNRESTRICTED, see DataSourceSecuritySettings.MinimumOutboundDataRestriction below. Without it,
|
||||
-- the data source gets ONLY_LINKS_FROM_CHAT, and a value AI Studio cannot read counts as
|
||||
-- ONLY_CONFIGURED_SERVICES. A data source with the security policy SELF_HOSTED always gets
|
||||
-- ONLY_CONFIGURED_SERVICES, whatever it says here.
|
||||
CONFIG["DATA_SOURCES"] = {}
|
||||
|
||||
-- Example: ERI v1 data source with a shared access token.
|
||||
@@ -295,7 +288,6 @@ CONFIG["DATA_SOURCES"] = {}
|
||||
-- ["AuthMethod"] = "TOKEN",
|
||||
-- ["Token"] = "ENC:v2:<base64-encoded encrypted token>",
|
||||
-- ["SecurityPolicy"] = "SELF_HOSTED",
|
||||
-- ["OutboundDataRestriction"] = "ONLY_CONFIGURED_SERVICES",
|
||||
-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>",
|
||||
-- ["MaxMatches"] = 10,
|
||||
-- }
|
||||
@@ -312,7 +304,6 @@ CONFIG["DATA_SOURCES"] = {}
|
||||
-- ["Username"] = "<shared username>",
|
||||
-- ["Password"] = "ENC:v2:<base64-encoded encrypted password>",
|
||||
-- ["SecurityPolicy"] = "SELF_HOSTED",
|
||||
-- ["OutboundDataRestriction"] = "ONLY_CONFIGURED_SERVICES",
|
||||
-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>",
|
||||
-- ["MaxMatches"] = 10,
|
||||
-- }
|
||||
@@ -328,7 +319,6 @@ CONFIG["DATA_SOURCES"] = {}
|
||||
-- ["UsernamePasswordMode"] = "OS_USERNAME_SHARED_PASSWORD",
|
||||
-- ["Password"] = "ENC:v2:<base64-encoded encrypted password>",
|
||||
-- ["SecurityPolicy"] = "SELF_HOSTED",
|
||||
-- ["OutboundDataRestriction"] = "ONLY_CONFIGURED_SERVICES",
|
||||
-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>",
|
||||
-- ["MaxMatches"] = 10,
|
||||
-- }
|
||||
@@ -464,34 +454,6 @@ CONFIG["SETTINGS"] = {}
|
||||
-- CONFIG["SETTINGS"]["DataApp.AllowUserToAddEmbeddingProvider"] = false
|
||||
-- CONFIG["SETTINGS"]["DataApp.AllowUserToAddTranscriptionProvider"] = false
|
||||
|
||||
-- Prohibit providers, e.g., when your organization must not send any data to them. The list
|
||||
-- applies to LLM providers, embedding providers, and transcription providers alike. Allowed
|
||||
-- values are the provider keys of DataConfidence.CustomConfidenceScheme, see below. A name
|
||||
-- AI Studio does not know is ignored and logged as a warning.
|
||||
--
|
||||
-- A prohibition names the operator of an API, not a family of models: prohibiting the API of
|
||||
-- a model maker keeps AI Studio away from that API, but the models of that maker may remain
|
||||
-- available through a hub which offers the models of several makers, or on a server of your
|
||||
-- own. Prohibit these as well if you need to.
|
||||
--
|
||||
-- Users can still select a prohibited provider when they add one, but the dialog then only
|
||||
-- explains that your organization does not allow it. Providers which users set up before stay
|
||||
-- in their settings, marked as prohibited, and users can delete them. AI Studio no longer
|
||||
-- offers or contacts them. Once you lift the prohibition, they work again.
|
||||
--
|
||||
-- A prohibited provider is always UNTRUSTED, whatever the confidence scheme says. A level which
|
||||
-- DataConfidence.CustomConfidenceScheme names for it is ignored and logged as a warning, and
|
||||
-- users cannot change it in the app. Once you lift the prohibition, that level applies again.
|
||||
--
|
||||
-- Replaces, does not merge: a configuration with a higher priority replaces the whole list.
|
||||
-- A configuration which prohibits one more provider has to repeat the providers of the base
|
||||
-- configuration. Otherwise, these become usable again.
|
||||
--
|
||||
-- Available since AI Studio v26.10.1. Older versions ignore this setting. Until all devices
|
||||
-- run that version, you can in addition use the CUSTOM confidence scheme, set the provider to
|
||||
-- UNTRUSTED there, and enforce a global minimum confidence level of at least VERY_LOW.
|
||||
-- CONFIG["SETTINGS"]["DataApp.ProhibitedProviders"] = { "<PROVIDER_KEY>" }
|
||||
|
||||
-- Configure the permission to add mailboxes. A mailbox is a data source, not a provider, so
|
||||
-- DataApp.AllowUserToAddProvider does not apply to it. When set to false, the menu entry for
|
||||
-- adding a mailbox stays visible but is disabled. Mailboxes the user added before stay; to keep
|
||||
@@ -688,7 +650,7 @@ CONFIG["SETTINGS"] = {}
|
||||
--
|
||||
-- Configure the default file patterns and whether subfolders as well as hidden files and folders are included.
|
||||
-- Separate multiple patterns with semicolons.
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.FilePatterns"] = "*.pdf;*.docx;*.pptx;*.xlsx;*.md;*.txt;*.jpg;*.jpeg;*.png;*.webp;*.gif;*.bmp;*.tif;*.tiff;*.mp3;*.wav;*.wave;*.aac;*.flac;*.ogg;*.opus;*.m4a;*.m4b;*.wma;*.alac;*.aif;*.aiff;*.caf;*.mp4;*.m4v;*.avi;*.mkv;*.mov;*.wmv;*.flv;*.webm"
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.FilePatterns"] = "*.pdf;*.docx;*.pptx;*.xlsx;*.md;*.txt;*.mp3;*.wav;*.wave;*.aac;*.flac;*.ogg;*.opus;*.m4a;*.m4b;*.wma;*.alac;*.aif;*.aiff;*.caf;*.mp4;*.m4v;*.avi;*.mkv;*.mov;*.wmv;*.flv;*.webm"
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.IncludeSubdirectories"] = false
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.IncludeHiddenFilesAndFolders"] = false
|
||||
--
|
||||
@@ -856,37 +818,6 @@ CONFIG["SETTINGS"] = {}
|
||||
-- documents. Providers below this level cannot be selected in the assistant.
|
||||
-- CONFIG["SETTINGS"]["DataVisualBriefing.MinimumProviderConfidence"] = "NONE"
|
||||
|
||||
-- Configure defaults for the Assistant Builder.
|
||||
-- Preselection must be enabled for the remaining Assistant Builder settings to take effect.
|
||||
-- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectOptions"] = true
|
||||
--
|
||||
-- Configure the preselected provider for building assistants.
|
||||
-- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"].
|
||||
-- Without a preselected provider, the Assistant Builder uses the app-wide default provider.
|
||||
-- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectedProvider"] = "00000000-0000-0000-0000-000000000000"
|
||||
--
|
||||
-- Configure the language in which the generated assistants answer.
|
||||
-- Allowed values are: AS_IS, EN_US, EN_GB, ZH_CN, HI_IN, ES_ES, FR_FR, DE_DE, DE_CH, DE_AT,
|
||||
-- JA_JP, RU_RU, OTHER
|
||||
-- AS_IS leaves the output language of the generated assistants open.
|
||||
-- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectedOutputLanguage"] = "AS_IS"
|
||||
--
|
||||
-- Configure a free-form language, used only when PreselectedOutputLanguage is "OTHER".
|
||||
-- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectedOtherOutputLanguage"] = ""
|
||||
--
|
||||
-- Configure the minimum provider confidence required to build assistants.
|
||||
-- Allowed values are: NONE, UNTRUSTED, UNKNOWN, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
|
||||
-- Providers below this level cannot be selected in the Assistant Builder. This minimum applies
|
||||
-- only while preselection is enabled; the app-wide minimum confidence applies in any case.
|
||||
-- CONFIG["SETTINGS"]["DataAssistantBuilder.MinimumProviderConfidence"] = "NONE"
|
||||
--
|
||||
-- Allow users to change individual managed Assistant Builder defaults locally.
|
||||
-- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectOptions.AllowUserOverride"] = true
|
||||
-- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectedProvider.AllowUserOverride"] = true
|
||||
-- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectedOutputLanguage.AllowUserOverride"] = true
|
||||
-- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectedOtherOutputLanguage.AllowUserOverride"] = true
|
||||
-- CONFIG["SETTINGS"]["DataAssistantBuilder.MinimumProviderConfidence.AllowUserOverride"] = true
|
||||
|
||||
-- Configure enterprise approvals for assistant plugins.
|
||||
-- Each approval is matched only by the current SHA-256 hash over all Lua files
|
||||
-- in the assistant plugin folder, in canonical sorted order.
|
||||
@@ -1203,9 +1134,6 @@ CONFIG["SETTINGS"] = {}
|
||||
-- SELF_HOSTED, HELMHOLTZ, GWDG
|
||||
-- Allowed confidence values are: UNTRUSTED, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
|
||||
--
|
||||
-- A provider which DataApp.ProhibitedProviders names is always UNTRUSTED. Its level in this
|
||||
-- table is ignored and logged as a warning, see DataApp.ProhibitedProviders above.
|
||||
--
|
||||
-- Replaces, does not merge: a configuration with a higher priority replaces the whole
|
||||
-- table. Every configuration that sets this must therefore list all providers it wants
|
||||
-- to cover. A partial table is not completed from the configuration below it, and the
|
||||
@@ -1250,23 +1178,6 @@ CONFIG["SETTINGS"] = {}
|
||||
-- "00000000-0000-0000-0000-000000000001",
|
||||
-- }
|
||||
|
||||
-- Configure the least strict outbound data restriction a data source may have, i.e., a local
|
||||
-- file, a local folder, or an external data source (ERI server). Each data source decides where
|
||||
-- a chat may still send data once the AI found something in it:
|
||||
-- ONLY_CONFIGURED_SERVICES Only services configured in AI Studio, such as your data sources,
|
||||
-- your Confluence, or the allowed private hosts of the tool Read Web
|
||||
-- Page. No other web pages, no web search.
|
||||
-- ONLY_LINKS_FROM_CHAT Also web pages whose addresses stand in the chat, written by the
|
||||
-- user or returned by a tool. No web search, and no addresses the AI
|
||||
-- chooses itself outside the configured services.
|
||||
-- UNRESTRICTED Every tool the user selected.
|
||||
-- A data source set to a less strict level gets this one whenever its passages reach a chat, and
|
||||
-- the dialog of the data source no longer offers the less strict levels. An external data source
|
||||
-- which only self-hosted providers may use always gets ONLY_CONFIGURED_SERVICES. A chat which
|
||||
-- found something before keeps the level it got then. The default is UNRESTRICTED, which leaves
|
||||
-- the choice to the user. New data sources start with ONLY_LINKS_FROM_CHAT either way.
|
||||
-- CONFIG["SETTINGS"]["DataSourceSecuritySettings.MinimumOutboundDataRestriction"] = "ONLY_LINKS_FROM_CHAT"
|
||||
|
||||
-- Configure the least strict outbound data restriction a mailbox may have. Mails come from
|
||||
-- strangers and may contain instructions meant for the AI, so each mailbox decides where a chat
|
||||
-- may still send data once it has read mails from it:
|
||||
@@ -1452,42 +1363,6 @@ CONFIG["CHAT_TEMPLATES"] = {}
|
||||
-- },
|
||||
-- }
|
||||
|
||||
-- An example chat template which preselects a provider, a profile, and a workspace:
|
||||
-- All three are optional and independent of each other, and leaving one out keeps what a
|
||||
-- chat starts with anyway. They are a preselection, not a limit: users change the provider
|
||||
-- and the profile in the chat as usual, and move the chat to another workspace. The IDs may
|
||||
-- name a provider or profile of this configuration, which keeps its ID on every computer, or
|
||||
-- one of another configuration of your organization.
|
||||
-- CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
|
||||
-- ["Id"] = "00000000-0000-0000-0000-000000000003",
|
||||
-- ["Name"] = "Mail Assistant",
|
||||
-- ["SystemPrompt"] = "You help <Company Name>'s employees read, sort, and answer their mails.",
|
||||
-- ["AllowProfileUsage"] = true,
|
||||
--
|
||||
-- -- Optional: the provider, and with it the model, a chat with this template starts with.
|
||||
-- -- Must be an ID from CONFIG["LLM_PROVIDERS"] or of a provider of another configuration.
|
||||
-- -- The provider has to meet the confidence level required for chats. A provider which
|
||||
-- -- does not, or an ID which resolves to nothing, is ignored, and the chat starts with
|
||||
-- -- the provider it would start with anyway.
|
||||
-- ["ProviderId"] = "11111111-1111-1111-1111-111111111111",
|
||||
--
|
||||
-- -- Optional: the profile a chat with this template starts with.
|
||||
-- -- Must be an ID from CONFIG["PROFILES"] or of a profile of another configuration.
|
||||
-- -- "00000000-0000-0000-0000-000000000000" starts the chat without a profile, whatever
|
||||
-- -- the user chose as their default. An ID which resolves to nothing is ignored, and the
|
||||
-- -- chat starts with the default profile of the user. The whole field is ignored when
|
||||
-- -- AllowProfileUsage is false.
|
||||
-- ["ProfileId"] = "22222222-2222-2222-2222-222222222222",
|
||||
--
|
||||
-- -- Optional: the workspace, by its name, that keeps the chats started with this template.
|
||||
-- -- The workspace is created with the first message of such a chat, when it does not exist
|
||||
-- -- yet. It only applies where a chat would otherwise be a disappearing chat: a chat users
|
||||
-- -- start in a workspace on purpose, for example, from the workspace list, stays there, and
|
||||
-- -- so does every chat while workspaces are switched off. An assistant chat launcher which
|
||||
-- -- names a workspace keeps its own.
|
||||
-- ["WorkspaceName"] = "Mails",
|
||||
-- }
|
||||
|
||||
-- Introduction texts shown as expansion panels on the welcome page:
|
||||
CONFIG["INTRODUCTIONS"] = {}
|
||||
|
||||
|
||||
+223
-373
File diff suppressed because it is too large.
Load diff
+209
-359
File diff suppressed because it is too large.
Load diff
@@ -175,7 +175,6 @@ internal sealed class Program
|
||||
builder.Services.AddSingleton<SettingsManager>();
|
||||
builder.Services.AddSingleton<AIGeneratedContentDisclosureService>();
|
||||
builder.Services.AddSingleton<PromptInjectionGuardService>();
|
||||
builder.Services.AddSingleton<ModelImageService>();
|
||||
builder.Services.AddSingleton<ToolSettingsService>();
|
||||
builder.Services.AddSingleton<WebPageRetrievalService>();
|
||||
builder.Services.AddSingleton<IToolImplementation, ReadWebPageTool>();
|
||||
|
||||
@@ -59,16 +59,18 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n
|
||||
Text = text,
|
||||
},
|
||||
|
||||
// Anthropic-specific image sub-content, which takes the base64 and its type apart:
|
||||
image => new SubContentImage
|
||||
// Anthropic-specific image sub-content:
|
||||
async attachment => new SubContentImage
|
||||
{
|
||||
Source = new SubContentBase64Image
|
||||
{
|
||||
Data = image.Base64,
|
||||
MediaType = image.MimeType,
|
||||
Data = await attachment.TryAsBase64(token: token) is (true, var base64Content)
|
||||
? base64Content
|
||||
: string.Empty,
|
||||
|
||||
MediaType = attachment.DetermineMimeType(),
|
||||
}
|
||||
},
|
||||
token
|
||||
}
|
||||
);
|
||||
|
||||
//
|
||||
|
||||
@@ -288,27 +288,6 @@ public static class LLMProvidersExtensions
|
||||
_ => false,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The largest image file, in bytes, which the provider accepts in a request, where it states one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The limit belongs to the API of the provider, not to the model: the same model may accept
|
||||
/// larger images through another provider. What we actually send is asked through
|
||||
/// IImageSourceExtensions.MaxImageSizeBytesFor, which also applies our own limit.
|
||||
/// </remarks>
|
||||
/// <param name="llmProvider">The provider to check.</param>
|
||||
/// <returns>The size limit of one image file, or null when the provider states none we know of.</returns>
|
||||
public static long? GetMaxImageSizeBytes(this LLMProviders llmProvider) => llmProvider switch
|
||||
{
|
||||
//
|
||||
// Anthropic accepts at most 5 MiB (5,242,880 bytes) of base64 per image. Base64 needs four
|
||||
// characters for every three bytes, which leaves 3,932,160 bytes for the image file itself:
|
||||
//
|
||||
LLMProviders.ANTHROPIC => 3_932_160,
|
||||
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new provider instance based on the provider value.
|
||||
/// </summary>
|
||||
@@ -341,16 +320,6 @@ public static class LLMProvidersExtensions
|
||||
|
||||
private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, HFInferenceProvider inferenceProvider, string configuredProviderId = "", string expertProviderApiParameter = "", bool isEnterpriseConfiguration = false, HFEndpointKind hfEndpointKind = HFEndpointKind.CHAT, ProviderCapabilityOverrides? capabilityOverrides = null, string tokenizerPath = "")
|
||||
{
|
||||
//
|
||||
// Every provider instance comes from here, for LLMs, embeddings, and transcriptions alike.
|
||||
// Whatever a caller forgot to check, no request reaches a provider the organization prohibited:
|
||||
//
|
||||
if (IsProhibitedByOrganization(provider))
|
||||
{
|
||||
LOGGER.LogWarning($"Refused to create the provider '{instanceName}' of '{provider}': the organization prohibited this provider.");
|
||||
return new NoProvider();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IProvider providerInstance = provider switch
|
||||
@@ -393,21 +362,6 @@ public static class LLMProvidersExtensions
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the organization prohibited the given provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The settings come from Program.SERVICE_PROVIDER, because none of the callers of CreateProvider
|
||||
/// hands them in. Tests which create providers without the services of the app have no settings
|
||||
/// there, and nothing is prohibited for them.
|
||||
/// </remarks>
|
||||
private static bool IsProhibitedByOrganization(LLMProviders provider)
|
||||
{
|
||||
// Declared as never null, but it is null until the app has started its services:
|
||||
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
|
||||
return Program.SERVICE_PROVIDER?.GetService<SettingsManager>() is { } settingsManager && settingsManager.IsProviderProhibited(provider);
|
||||
}
|
||||
|
||||
public static string GetCreationURL(this LLMProviders provider) => provider switch
|
||||
{
|
||||
LLMProviders.OPEN_AI => "https://platform.openai.com/signup",
|
||||
|
||||
@@ -113,14 +113,15 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
|
||||
{
|
||||
Text = text,
|
||||
},
|
||||
image => new SubContentImageUrlNested
|
||||
async attachment => new SubContentImageUrlNested
|
||||
{
|
||||
ImageUrl = new SubContentImageUrlData
|
||||
{
|
||||
Url = image.ToDataUrl(),
|
||||
Url = await attachment.TryAsBase64(token: token) is (true, var base64Content)
|
||||
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
|
||||
: string.Empty,
|
||||
},
|
||||
},
|
||||
token);
|
||||
});
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
@@ -191,11 +192,12 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
|
||||
{
|
||||
Text = text,
|
||||
},
|
||||
image => new SubContentInputImage
|
||||
async attachment => new SubContentInputImage
|
||||
{
|
||||
ImageUrl = image.ToDataUrl(),
|
||||
},
|
||||
token);
|
||||
ImageUrl = await attachment.TryAsBase64(token: token) is (true, var base64Content)
|
||||
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
|
||||
: string.Empty,
|
||||
});
|
||||
|
||||
var baseInput = new List<object> { systemPrompt };
|
||||
baseInput.AddRange(messages);
|
||||
|
||||
@@ -36,29 +36,6 @@ public static class HostExtensions
|
||||
_ => "embeddings",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The largest image file, in bytes, which the engine accepts in a chat request, where we know of one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Like the limit of a cloud provider, see LLMProvidersExtensions.GetMaxImageSizeBytes, this
|
||||
/// belongs to the API of the engine, not to the model. What we actually send is asked through
|
||||
/// IImageSourceExtensions.MaxImageSizeBytesFor.
|
||||
/// </remarks>
|
||||
/// <param name="host">The engine.</param>
|
||||
/// <returns>The size limit of one image file, or null when the engine states none we know of.</returns>
|
||||
public static long? GetMaxImageSizeBytes(this Host host) => host switch
|
||||
{
|
||||
//
|
||||
// llmman answers a chat request larger than 2 MiB with 413: its route /v1/chat/completions
|
||||
// keeps the default limit of its web framework, read on 2026-10-22 at
|
||||
// https://github.com/llmmanorg/llmman/blob/main/src/cmd/serve.rs. An image of 1 MiB takes
|
||||
// about 1.33 MiB as base64, which leaves room for the text of the conversation:
|
||||
//
|
||||
Host.LLMMAN => 1_048_576,
|
||||
|
||||
_ => null,
|
||||
};
|
||||
|
||||
public static bool IsChatSupported(this Host host)
|
||||
{
|
||||
switch (host)
|
||||
|
||||
@@ -51,46 +51,6 @@ public record ChatTemplate(
|
||||
/// </remarks>
|
||||
public DataSourceOptions? DataSourceOptions { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The profile this template preselects for a chat started with it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null means the template says nothing about profiles, so the chat keeps the profile it would
|
||||
/// start with anyway: the default of the chat settings, or the one the user picked. The ID of
|
||||
/// Profile.NO_PROFILE is a statement of its own: such chats start without a profile, whatever
|
||||
/// that default says.<br/><br/>
|
||||
/// A preselection, not a limit: the user picks another profile in the chat as usual. It means
|
||||
/// nothing for a template which does not allow profiles, and a profile which is gone leaves the
|
||||
/// chat default in place.
|
||||
/// </remarks>
|
||||
public string? ProfileId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The provider, and with it the model, this template preselects for a chat started with it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null means the template says nothing, so the chat starts with the provider it would start
|
||||
/// with anyway. A preselection, not a limit, and no way around the confidence levels either: a
|
||||
/// provider which is gone, or which may not be used for chats, leaves the chat to start as if
|
||||
/// the template said nothing.
|
||||
/// </remarks>
|
||||
public string? ProviderId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the workspace a chat started with this template is kept in.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null means the template says nothing, so the chat goes where it would go anyway. A name
|
||||
/// rather than an ID: a workspace has an ID of its own on every computer, so a configuration
|
||||
/// plugin has no other way to name one. Like for an assistant chat launcher, the workspace is
|
||||
/// created when it does not exist yet; on the chat page only with the first message of the chat,
|
||||
/// so that trying out a template creates no workspace.<br/><br/>
|
||||
/// It only fills in where a chat would belong to no workspace otherwise. A chat started in a
|
||||
/// workspace on purpose, for example from the workspace list, stays there, and so does every
|
||||
/// chat while workspaces are switched off.
|
||||
/// </remarks>
|
||||
public string? WorkspaceName { get; init; }
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatTemplate).Namespace, nameof(ChatTemplate));
|
||||
|
||||
private static readonly ILogger<ChatTemplate> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ChatTemplate>();
|
||||
@@ -107,9 +67,6 @@ public record ChatTemplate(
|
||||
AllowProfileUsage = true,
|
||||
ToolIds = null,
|
||||
DataSourceOptions = null,
|
||||
ProfileId = null,
|
||||
ProviderId = null,
|
||||
WorkspaceName = null,
|
||||
EnterpriseConfigurationPluginId = Guid.Empty,
|
||||
IsEnterpriseConfiguration = false,
|
||||
};
|
||||
@@ -124,16 +81,6 @@ public record ChatTemplate(
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Whether this template starts its chats without a profile, whatever the default says.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// That is a statement of its own, made with the ID of the no-profile entry, see ProfileId. It
|
||||
/// names no profile which could be missing on another computer. A method rather than a property,
|
||||
/// so that it does not end up in the stored settings.
|
||||
/// </remarks>
|
||||
public bool StartsWithoutProfile() => string.Equals(this.ProfileId, Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of this chat template. If it is the NO_CHAT_TEMPLATE, it returns a localized string.
|
||||
/// </summary>
|
||||
@@ -170,12 +117,12 @@ public record ChatTemplate(
|
||||
/// <param name="chatTemplate">The chat template the launcher opens its chat with.</param>
|
||||
/// <param name="launcherToolIds">The tools the launcher names itself, or null when it names none.</param>
|
||||
/// <returns>The tools to start with — null when neither says anything, which leaves the chat default in place — and whether the launcher's own choice was dropped for it.</returns>
|
||||
public static ChatTemplateToolChoice ChooseToolIds(ChatTemplate chatTemplate, IReadOnlyCollection<string>? launcherToolIds)
|
||||
public static (IReadOnlyCollection<string>? ToolIds, bool LauncherChoiceDropped) ChooseToolIds(ChatTemplate chatTemplate, IReadOnlyCollection<string>? launcherToolIds)
|
||||
{
|
||||
if (chatTemplate.ToolIds is not { } templateToolIds)
|
||||
return new(launcherToolIds, false);
|
||||
return (launcherToolIds, false);
|
||||
|
||||
return new(templateToolIds, launcherToolIds is not null);
|
||||
return (templateToolIds, launcherToolIds is not null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -190,12 +137,12 @@ public record ChatTemplate(
|
||||
/// <param name="chatTemplate">The chat template the launcher opens its chat with.</param>
|
||||
/// <param name="launcherOptions">The options built from the data sources the launcher names, or null when it names none.</param>
|
||||
/// <returns>The options to start with — null when neither says anything, which leaves the chat default in place — and whether the launcher's own choice was dropped for them.</returns>
|
||||
public static ChatTemplateDataSourceChoice ChooseDataSourceOptions(ChatTemplate chatTemplate, DataSourceOptions? launcherOptions)
|
||||
public static (DataSourceOptions? Options, bool LauncherChoiceDropped) ChooseDataSourceOptions(ChatTemplate chatTemplate, DataSourceOptions? launcherOptions)
|
||||
{
|
||||
if (chatTemplate.DataSourceOptions is not { } templateOptions)
|
||||
return new(launcherOptions, false);
|
||||
return (launcherOptions, false);
|
||||
|
||||
return new(templateOptions.CreateCopy(), launcherOptions is not null);
|
||||
return (templateOptions.CreateCopy(), launcherOptions is not null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -255,10 +202,6 @@ public record ChatTemplate(
|
||||
|
||||
var fileAttachments = ParseFileAttachments(idx, table, pluginPath);
|
||||
|
||||
var profileId = ParseOptionalId(idx, table, "ProfileId", allowEmpty: true);
|
||||
if (profileId is not null && !allowProfileUsage)
|
||||
LOGGER.LogWarning("The chat template {IdxChatTemplate} preselects a profile but does not allow profiles. Chats started with it ignore that profile.", idx);
|
||||
|
||||
template = new ChatTemplate
|
||||
{
|
||||
Num = 0, // will be set later by the PluginConfigurationObject
|
||||
@@ -271,9 +214,6 @@ public record ChatTemplate(
|
||||
AllowProfileUsage = allowProfileUsage,
|
||||
ToolIds = ParseToolIds(idx, table),
|
||||
DataSourceOptions = ParseDataSourceOptions(idx, table),
|
||||
ProfileId = profileId,
|
||||
ProviderId = ParseOptionalId(idx, table, "ProviderId", allowEmpty: false),
|
||||
WorkspaceName = ParseWorkspaceName(idx, table),
|
||||
IsEnterpriseConfiguration = true,
|
||||
EnterpriseConfigurationPluginId = configPluginId,
|
||||
};
|
||||
@@ -354,43 +294,6 @@ public record ChatTemplate(
|
||||
return toolIds;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The empty GUID is a statement for a profile — start without one — but names no provider at
|
||||
/// all, hence the switch. Anything which is no GUID cannot point at a provider or profile and is
|
||||
/// dropped, so that the template still works, with the chat defaults in its place.
|
||||
/// </remarks>
|
||||
private static string? ParseOptionalId(int idx, LuaTable table, string fieldName, bool allowEmpty)
|
||||
{
|
||||
if (!table.TryGetValue(fieldName, out var idValue))
|
||||
return null;
|
||||
|
||||
if (!idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id) || (!allowEmpty && id == Guid.Empty))
|
||||
{
|
||||
LOGGER.LogWarning("The {FieldName} of chat template {IdxChatTemplate} is not a valid ID and will be ignored.", fieldName, idx);
|
||||
return null;
|
||||
}
|
||||
|
||||
return id.ToString();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// An empty name would mean "no workspace", which is what a template says by leaving the field
|
||||
/// out. So it is dropped, with a warning, as is anything which is no text.
|
||||
/// </remarks>
|
||||
private static string? ParseWorkspaceName(int idx, LuaTable table)
|
||||
{
|
||||
if (!table.TryGetValue("WorkspaceName", out var workspaceNameValue))
|
||||
return null;
|
||||
|
||||
if (!workspaceNameValue.TryRead<string>(out var workspaceName) || string.IsNullOrWhiteSpace(workspaceName))
|
||||
{
|
||||
LOGGER.LogWarning("The WorkspaceName of chat template {IdxChatTemplate} is not a valid name and will be ignored.", idx);
|
||||
return null;
|
||||
}
|
||||
|
||||
return workspaceName.Trim();
|
||||
}
|
||||
|
||||
private static DataSourceOptions? ParseDataSourceOptions(int idx, LuaTable table)
|
||||
{
|
||||
if (!table.TryGetValue("DataSourceOptions", out var optionsValue) || !optionsValue.TryRead<LuaTable>(out var optionsTable))
|
||||
@@ -546,25 +449,22 @@ public record ChatTemplate(
|
||||
var fileAttachmentsLua = this.BuildFileAttachmentsLua(fileAttachmentPaths);
|
||||
|
||||
//
|
||||
// These may be absent entirely, because saying nothing about the profile, the provider, the
|
||||
// workspace, the tools, or the data sources is a statement of its own. They therefore bring
|
||||
// their own line break and indentation instead of sitting on a line of the template:
|
||||
// Both of these may be absent entirely, because saying nothing about tools or data sources
|
||||
// is a statement of its own. They therefore bring their own line break and indentation
|
||||
// instead of sitting on a line of the template:
|
||||
//
|
||||
var profileIdLua = BuildOptionalStringLua("ProfileId", this.ProfileId);
|
||||
var providerIdLua = BuildOptionalStringLua("ProviderId", this.ProviderId);
|
||||
var workspaceNameLua = BuildOptionalStringLua("WorkspaceName", this.WorkspaceName);
|
||||
var toolIdsLua = this.BuildToolIdsLua();
|
||||
var dataSourceOptionsLua = this.BuildDataSourceOptionsLua();
|
||||
|
||||
luaCode = $$"""
|
||||
{{this.BuildProviderAndProfileIdNote()}}{{this.BuildDataSourceIdNote()}}CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
|
||||
{{this.BuildDataSourceIdNote()}}CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
|
||||
["Id"] = "{{LuaTools.EscapeLuaString(exportId)}}",
|
||||
["Name"] = {{LuaTools.ToLuaStringLiteral(this.Name)}},
|
||||
["SystemPrompt"] = {{LuaTools.ToLuaStringLiteral(this.SystemPrompt)}},
|
||||
["PredefinedUserPrompt"] = {{LuaTools.ToLuaStringLiteral(this.PredefinedUserPrompt)}},
|
||||
["AllowProfileUsage"] = {{this.AllowProfileUsage.ToString().ToLowerInvariant()}},
|
||||
["FileAttachments"] = {{fileAttachmentsLua}},
|
||||
["ExampleConversation"] = {{exampleConversationLua}},{{profileIdLua}}{{providerIdLua}}{{workspaceNameLua}}{{toolIdsLua}}{{dataSourceOptionsLua}}
|
||||
["ExampleConversation"] = {{exampleConversationLua}},{{toolIdsLua}}{{dataSourceOptionsLua}}
|
||||
}
|
||||
""";
|
||||
return true;
|
||||
@@ -674,10 +574,6 @@ public record ChatTemplate(
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string BuildOptionalStringLua(string fieldName, string? value) => value is null
|
||||
? string.Empty
|
||||
: $"{Environment.NewLine} [\"{fieldName}\"] = {LuaTools.ToLuaStringLiteral(value)},";
|
||||
|
||||
/// <remarks>
|
||||
/// An empty set is written out as an empty table rather than being left out: the two say
|
||||
/// different things, and dropping the line would turn "no tools at all" into "whatever the
|
||||
@@ -757,26 +653,6 @@ public record ChatTemplate(
|
||||
""";
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The same holds for the provider and the profile: a configuration plugin reaches only those
|
||||
/// which it or another plugin defines, and the ones of this machine are mostly the user's own.
|
||||
/// Starting without a profile names nothing, so it needs no note.
|
||||
/// </remarks>
|
||||
private string BuildProviderAndProfileIdNote()
|
||||
{
|
||||
var namesProfile = this.ProfileId is not null && !this.StartsWithoutProfile();
|
||||
if (this.ProviderId is null && !namesProfile)
|
||||
return string.Empty;
|
||||
|
||||
return """
|
||||
-- The provider and profile IDs below are the ones of the machine this was exported from.
|
||||
-- Please check them against your CONFIG["LLM_PROVIDERS"] and CONFIG["PROFILES"]: an ID
|
||||
-- which resolves to nothing is ignored, and a chat with this template then starts with
|
||||
-- the provider or profile it would start with anyway.
|
||||
|
||||
""";
|
||||
}
|
||||
|
||||
private string BuildFileAttachmentsLua(IReadOnlyList<string>? fileAttachmentPaths)
|
||||
{
|
||||
var paths = fileAttachmentPaths ?? this.FileAttachments.Select(attachment => attachment.FilePath).ToList();
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Whose data source options a chat started by a launcher begins with, decided by ChatTemplate.ChooseDataSourceOptions.
|
||||
/// </summary>
|
||||
/// <param name="Options">The options to start with, or null when neither side names any, which leaves the chat default in place.</param>
|
||||
/// <param name="LauncherChoiceDropped">Whether the data sources the launcher names itself were dropped for the options of the chat template.</param>
|
||||
public readonly record struct ChatTemplateDataSourceChoice(DataSourceOptions? Options, bool LauncherChoiceDropped)
|
||||
{
|
||||
/// <summary>
|
||||
/// Neither side names any data sources, so the chat keeps its default options.
|
||||
/// </summary>
|
||||
public static readonly ChatTemplateDataSourceChoice NONE = new(null, false);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Whose tools a chat started by a launcher begins with, decided by ChatTemplate.ChooseToolIds.
|
||||
/// </summary>
|
||||
/// <param name="ToolIds">The tools to start with, or null when neither side names any, which leaves the chat default in place.</param>
|
||||
/// <param name="LauncherChoiceDropped">Whether the tools the launcher names itself were dropped for those of the chat template.</param>
|
||||
public readonly record struct ChatTemplateToolChoice(IReadOnlyCollection<string>? ToolIds, bool LauncherChoiceDropped)
|
||||
{
|
||||
/// <summary>
|
||||
/// Neither side names any tools, so the chat keeps its default.
|
||||
/// </summary>
|
||||
public static readonly ChatTemplateToolChoice NONE = new(null, false);
|
||||
}
|
||||
@@ -336,9 +336,9 @@ public static class ConfigurationSelectDataFactory
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The restrictions a mailbox or a data source may have.
|
||||
/// The restrictions a mailbox may have.
|
||||
/// </summary>
|
||||
/// <param name="minimumRestriction">The least strict restriction allowed, see DataMailboxes.MinimumOutboundDataRestriction and DataSourceOutboundDataExtensions.GetMinimumOutboundDataRestriction.</param>
|
||||
/// <param name="minimumRestriction">The least strict restriction the organization allows, see DataMailboxes.MinimumOutboundDataRestriction.</param>
|
||||
public static IEnumerable<ConfigurationSelectData<OutboundDataRestriction>> GetOutboundDataRestrictionData(OutboundDataRestriction minimumRestriction)
|
||||
{
|
||||
foreach (var restriction in Enum.GetValues<OutboundDataRestriction>())
|
||||
|
||||
@@ -206,11 +206,6 @@ public sealed class Data
|
||||
|
||||
public DataSlideBuilder SlideBuilder { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the managed Assistant Builder defaults.
|
||||
/// </summary>
|
||||
public DataAssistantBuilder AssistantBuilder { get; init; } = new(x => x.AssistantBuilder);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the managed Visual Briefing Assistant defaults.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
@@ -193,17 +192,6 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
|
||||
/// </summary>
|
||||
public bool AllowUserToAddTranscriptionProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToAddTranscriptionProvider, true);
|
||||
|
||||
/// <summary>
|
||||
/// The providers an organization prohibited, for LLMs, embeddings, and transcriptions alike.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A prohibition names the operator of an API, not a family of models: prohibiting the API of a
|
||||
/// model maker leaves its models reachable through a hub or a self-hosted server. Providers the
|
||||
/// user set up before stay in the settings, so they are back once the prohibition ends, but
|
||||
/// AI Studio neither offers nor contacts them, see SettingsManager.IsProviderProhibited.
|
||||
/// </remarks>
|
||||
public HashSet<LLMProviders> ProhibitedProviders { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ProhibitedProviders, []);
|
||||
|
||||
/// <summary>
|
||||
/// Should the user be allowed to add mailboxes?
|
||||
/// </summary>
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
using AIStudio.Provider;
|
||||
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the default options for the Assistant Builder.
|
||||
/// </summary>
|
||||
/// <param name="configSelection">The managed-configuration selector.</param>
|
||||
public sealed class DataAssistantBuilder(Expression<Func<Data, DataAssistantBuilder>>? configSelection = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes an unmanaged Assistant Builder settings instance.
|
||||
/// </summary>
|
||||
public DataAssistantBuilder() : this(null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether Assistant Builder defaults are preselected.
|
||||
/// </summary>
|
||||
public bool PreselectOptions { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectOptions, true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the provider used by default. An empty value uses the app default.
|
||||
/// </summary>
|
||||
public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedProvider, string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default output language for generated assistants.
|
||||
/// </summary>
|
||||
public CommonLanguages PreselectedOutputLanguage { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedOutputLanguage, CommonLanguages.AS_IS);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default custom output language.
|
||||
/// </summary>
|
||||
public string PreselectedOtherOutputLanguage { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedOtherOutputLanguage, string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum confidence a provider needs for building assistants.
|
||||
/// </summary>
|
||||
public ConfidenceLevel MinimumProviderConfidence { get; set; } = ManagedConfiguration.Register(configSelection, value => value.MinimumProviderConfidence, ConfidenceLevel.NONE);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ namespace AIStudio.Settings.DataModel;
|
||||
/// <param name="configSelection">The managed-configuration selector.</param>
|
||||
public sealed class DataBatchProcessing(Expression<Func<Data, DataBatchProcessing>>? configSelection = null)
|
||||
{
|
||||
public const string DEFAULT_FILE_PATTERNS = "*.pdf;*.docx;*.pptx;*.xlsx;*.md;*.txt;*.jpg;*.jpeg;*.png;*.webp;*.gif;*.bmp;*.tif;*.tiff;*.mp3;*.wav;*.wave;*.aac;*.flac;*.ogg;*.opus;*.m4a;*.m4b;*.wma;*.alac;*.aif;*.aiff;*.caf;*.mp4;*.m4v;*.avi;*.mkv;*.mov;*.wmv;*.flv;*.webm";
|
||||
public const string DEFAULT_FILE_PATTERNS = "*.pdf;*.docx;*.pptx;*.xlsx;*.md;*.txt;*.mp3;*.wav;*.wave;*.aac;*.flac;*.ogg;*.opus;*.m4a;*.m4b;*.wma;*.alac;*.aif;*.aiff;*.caf;*.mp4;*.m4v;*.avi;*.mkv;*.mov;*.wmv;*.flv;*.webm";
|
||||
public const int MIN_DELAY_SECONDS = 6;
|
||||
public const int MAX_DELAY_SECONDS = 300;
|
||||
public const int DEFAULT_MIN_DELAY_SECONDS = 6;
|
||||
|
||||
@@ -63,9 +63,6 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
/// <inheritdoc />
|
||||
public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED;
|
||||
|
||||
/// <inheritdoc />
|
||||
public OutboundDataRestriction OutboundDataRestriction { get; init; } = DataSourceOutboundDataExtensions.DEFAULT_OUTBOUND_DATA_RESTRICTION;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnterpriseConfiguration { get; init; }
|
||||
|
||||
@@ -265,20 +262,6 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
// Optional, so that configurations written before the setting existed stay valid; their data
|
||||
// sources get the level of a new one. A value this version cannot read counts as the
|
||||
// strictest level, as in the settings file. Only a name counts, never a number, see EnumNames.
|
||||
// A data source for self-hosted providers only is not clamped here: the level applies when
|
||||
// its passages reach a chat, see DataSourceOutboundDataExtensions.GetEffectiveOutboundDataRestriction.
|
||||
//
|
||||
var outboundDataRestriction = DataSourceOutboundDataExtensions.DEFAULT_OUTBOUND_DATA_RESTRICTION;
|
||||
if (table.TryGetValue("OutboundDataRestriction", out var outboundDataRestrictionValue) && !(outboundDataRestrictionValue.TryRead<string>(out var outboundDataRestrictionText) && EnumNames.TryParse(outboundDataRestrictionText, out outboundDataRestriction)))
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid outbound data restriction, so the strictest one applies. (Plugin ID: {configPluginId})");
|
||||
outboundDataRestriction = OutboundDataRestriction.ONLY_CONFIGURED_SERVICES;
|
||||
}
|
||||
|
||||
if (!table.TryGetValue("SelectedRetrievalId", out var selectedRetrievalIdValue) || !selectedRetrievalIdValue.TryRead<string>(out var selectedRetrievalId) || string.IsNullOrWhiteSpace(selectedRetrievalId))
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} must specify a selected retrieval ID. (Plugin ID: {configPluginId})");
|
||||
@@ -336,7 +319,6 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
Username = username,
|
||||
UsernamePasswordMode = usernamePasswordMode,
|
||||
SecurityPolicy = securityPolicy,
|
||||
OutboundDataRestriction = outboundDataRestriction,
|
||||
Version = ERIVersion.V1,
|
||||
SelectedRetrievalId = selectedRetrievalId,
|
||||
MaxMatches = (ushort)maxMatches,
|
||||
@@ -397,7 +379,6 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
{{usernameLine}}
|
||||
{{secretLine}}
|
||||
["SecurityPolicy"] = "{{this.SecurityPolicy}}",
|
||||
["OutboundDataRestriction"] = "{{this.OutboundDataRestriction}}",
|
||||
["SelectedRetrievalId"] = "{{LuaTools.EscapeLuaString(this.SelectedRetrievalId)}}",
|
||||
["MaxMatches"] = {{this.MaxMatches}},
|
||||
}
|
||||
|
||||
@@ -65,9 +65,6 @@ public readonly record struct DataSourceLocalDirectory : IInternalDataSource
|
||||
/// <inheritdoc />
|
||||
public ushort MaxMatches { get; init; } = 10;
|
||||
|
||||
/// <inheritdoc />
|
||||
public OutboundDataRestriction OutboundDataRestriction { get; init; } = DataSourceOutboundDataExtensions.DEFAULT_OUTBOUND_DATA_RESTRICTION;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<IRetrievalContext>> RetrieveDataAsync(IContent lastUserPrompt, ChatThread thread, CancellationToken token = default) =>
|
||||
Program.SERVICE_PROVIDER.GetRequiredService<DataSourceLocalRetrievalService>().RetrieveDataAsync(this, lastUserPrompt, thread, token);
|
||||
|
||||
@@ -53,9 +53,6 @@ public readonly record struct DataSourceLocalFile : IInternalDataSource
|
||||
/// <inheritdoc />
|
||||
public ushort MaxMatches { get; init; } = 10;
|
||||
|
||||
/// <inheritdoc />
|
||||
public OutboundDataRestriction OutboundDataRestriction { get; init; } = DataSourceOutboundDataExtensions.DEFAULT_OUTBOUND_DATA_RESTRICTION;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<IRetrievalContext>> RetrieveDataAsync(IContent lastUserPrompt, ChatThread thread, CancellationToken token = default) =>
|
||||
Program.SERVICE_PROVIDER.GetRequiredService<DataSourceLocalRetrievalService>().RetrieveDataAsync(this, lastUserPrompt, thread, token);
|
||||
|
||||
@@ -26,10 +26,4 @@ public enum OutboundDataOrigin
|
||||
/// in the intranet or behind a VPN, or a page of the configured wiki, whatever its address.
|
||||
/// </summary>
|
||||
INTERNAL_WEB_PAGE,
|
||||
|
||||
/// <summary>
|
||||
/// A configured data source, such as a local folder or an ERI server, whose passages reached the
|
||||
/// model: found by Semantic Search or by the classic RAG process.
|
||||
/// </summary>
|
||||
DATA_SOURCE,
|
||||
}
|
||||
@@ -5,12 +5,12 @@ namespace AIStudio.Settings.DataModel;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The source travels along so a tool which is kept from running can name it, and the user
|
||||
/// learns what stands in the way. For a mailbox or a data source, the reference is its id rather
|
||||
/// than its name, because either may be renamed after a chat read from it.
|
||||
/// learns what stands in the way. For a mailbox, the reference is its id rather than its name,
|
||||
/// because a mailbox may be renamed after a chat read from it.
|
||||
/// </remarks>
|
||||
/// <param name="Restriction">Where the chat may still send data.</param>
|
||||
/// <param name="Origin">What kind of source demands the restriction, or NONE when nothing restricts the chat.</param>
|
||||
/// <param name="Reference">Which source of its kind demands the restriction: the id of a mailbox or of a data source, or the host of a private web page. An empty text when the kind says it all, as for the wiki, or when nothing restricts the chat.</param>
|
||||
/// <param name="Reference">Which source of its kind demands the restriction: the id of a mailbox, or the host of a private web page. An empty text when the kind says it all, as for the wiki, or when nothing restricts the chat.</param>
|
||||
public sealed record OutboundDataRequirement(OutboundDataRestriction Restriction, OutboundDataOrigin Origin, string Reference)
|
||||
{
|
||||
/// <summary>
|
||||
@@ -25,15 +25,9 @@ public sealed record OutboundDataRequirement(OutboundDataRestriction Restriction
|
||||
/// <remarks>
|
||||
/// On a tie, this one is kept, whatever the origin of either, so the source which set a level
|
||||
/// first stays the one which is named, and a chat does not name another source with every
|
||||
/// search. A restriction this version does not know counts as the strictest one, and the
|
||||
/// requirement which wins carries that one instead.
|
||||
/// search.
|
||||
/// </remarks>
|
||||
/// <param name="other">The other requirement.</param>
|
||||
/// <returns>The requirement whose restriction is the stricter one.</returns>
|
||||
public OutboundDataRequirement StricterOf(OutboundDataRequirement other)
|
||||
{
|
||||
var stricter = other.Restriction.OrStrictestWhenUnknown() < this.Restriction.OrStrictestWhenUnknown() ? other : this;
|
||||
var knownRestriction = stricter.Restriction.OrStrictestWhenUnknown();
|
||||
return knownRestriction == stricter.Restriction ? stricter : stricter with { Restriction = knownRestriction };
|
||||
}
|
||||
public OutboundDataRequirement StricterOf(OutboundDataRequirement other) => other.Restriction < this.Restriction ? other : this;
|
||||
}
|
||||
@@ -16,14 +16,13 @@ public static class OutboundDataRequirementExtensions
|
||||
/// had when the chat read the content: a change made later does not loosen the chat.
|
||||
/// </remarks>
|
||||
/// <param name="requirement">What the chat demands.</param>
|
||||
/// <param name="settings">The configuration, to find the name of the mailbox or data source which demands it.</param>
|
||||
/// <param name="settings">The configuration, to find the name of the mailbox which demands it.</param>
|
||||
/// <returns>The text for the user.</returns>
|
||||
public static string GetToolBlockedMessage(this OutboundDataRequirement requirement, Data settings) => requirement.Origin switch
|
||||
{
|
||||
OutboundDataOrigin.MAILBOX => GetMailboxBlockedMessage(requirement, settings.Mailboxes),
|
||||
OutboundDataOrigin.CONFLUENCE_WIKI => string.Format(TB("This chat searched your Confluence wiki while the setting 'Where a chat may send data after reading internal pages' of the tool 'Read Web Page' was '{0}'. This tool would send data beyond that, so it is not available in this chat. A new chat can use it again."), requirement.Restriction.GetName()),
|
||||
OutboundDataOrigin.INTERNAL_WEB_PAGE => string.Format(TB("This chat read an internal web page on '{0}' while the setting 'Where a chat may send data after reading internal pages' of the tool 'Read Web Page' was '{1}'. This tool would send data beyond that, so it is not available in this chat. A new chat can use it again."), requirement.Reference, requirement.Restriction.GetName()),
|
||||
OutboundDataOrigin.DATA_SOURCE => GetDataSourceBlockedMessage(requirement, settings.DataSources),
|
||||
|
||||
// NONE keeps no tool from running, since nothing restricts such a chat. What is left is an
|
||||
// origin this version does not know:
|
||||
@@ -41,16 +40,4 @@ public static class OutboundDataRequirementExtensions
|
||||
? string.Format(TB("This chat read mails from a mailbox which has been removed since, while its setting 'Where a chat may send data after reading mails' was '{0}'. This tool would send data beyond that, so it is not available in this chat. A new chat can use it again."), requirement.Restriction.GetName())
|
||||
: string.Format(TB("This chat read mails from the mailbox '{0}' while its setting 'Where a chat may send data after reading mails' was '{1}'. This tool would send data beyond that, so it is not available in this chat. A new chat can use it again."), mailboxName, requirement.Restriction.GetName());
|
||||
}
|
||||
|
||||
private static string GetDataSourceBlockedMessage(OutboundDataRequirement requirement, IEnumerable<IDataSource> dataSources)
|
||||
{
|
||||
var dataSourceName = dataSources
|
||||
.Where(dataSource => string.Equals(dataSource.Id, requirement.Reference, StringComparison.Ordinal))
|
||||
.Select(dataSource => dataSource.Name)
|
||||
.FirstOrDefault();
|
||||
|
||||
return string.IsNullOrWhiteSpace(dataSourceName)
|
||||
? string.Format(TB("This chat found content in a data source which has been removed since, while its setting 'Where a chat may send data after reading this data source' was '{0}'. This tool would send data beyond that, so it is not available in this chat. A new chat can use it again."), requirement.Restriction.GetName())
|
||||
: string.Format(TB("This chat found content in the data source '{0}' while its setting 'Where a chat may send data after reading this data source' was '{1}'. This tool would send data beyond that, so it is not available in this chat. A new chat can use it again."), dataSourceName, requirement.Restriction.GetName());
|
||||
}
|
||||
}
|
||||
Loaded 100 of 224 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user