mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-03-12 23:09:06 +00:00
Added an agent to select the appropriate data sources for any prompt (#284)
This commit is contained in:
parent
657fda4961
commit
98810ce884
@ -19,7 +19,7 @@ Things we are currently working on:
|
|||||||
- [ ] App: Implement the process to vectorize one local file using embeddings
|
- [ ] App: Implement the process to vectorize one local file using embeddings
|
||||||
- [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb)
|
- [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb)
|
||||||
- [ ] App: Implement the continuous process of vectorizing data
|
- [ ] App: Implement the continuous process of vectorizing data
|
||||||
- [x] ~~App: Define a common retrieval context interface for the integration of RAG processes in chats (PR [#281](https://github.com/MindWorkAI/AI-Studio/pull/281))~~
|
- [x] ~~App: Define a common retrieval context interface for the integration of RAG processes in chats (PR [#281](https://github.com/MindWorkAI/AI-Studio/pull/281), [#284](https://github.com/MindWorkAI/AI-Studio/pull/284))~~
|
||||||
- [ ] App: Define a common augmentation interface for the integration of RAG processes in chats
|
- [ ] App: Define a common augmentation interface for the integration of RAG processes in chats
|
||||||
- [x] ~~App: Integrate data sources in chats (PR [#282](https://github.com/MindWorkAI/AI-Studio/pull/282))~~
|
- [x] ~~App: Integrate data sources in chats (PR [#282](https://github.com/MindWorkAI/AI-Studio/pull/282))~~
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RAG/@EntryIndexedValue">RAG</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=groq/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=groq/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
@ -1,3 +1,5 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
@ -9,6 +11,11 @@ namespace AIStudio.Agents;
|
|||||||
|
|
||||||
public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager settingsManager, DataSourceService dataSourceService, ThreadSafeRandom rng) : IAgent
|
public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager settingsManager, DataSourceService dataSourceService, ThreadSafeRandom rng) : IAgent
|
||||||
{
|
{
|
||||||
|
protected static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
|
};
|
||||||
|
|
||||||
protected DataSourceService DataSourceService { get; init; } = dataSourceService;
|
protected DataSourceService DataSourceService { get; init; } = dataSourceService;
|
||||||
|
|
||||||
protected SettingsManager SettingsManager { get; init; } = settingsManager;
|
protected SettingsManager SettingsManager { get; init; } = settingsManager;
|
||||||
|
416
app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs
Normal file
416
app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Provider;
|
||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
|
using AIStudio.Tools.ERIClient;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
namespace AIStudio.Agents;
|
||||||
|
|
||||||
|
public sealed class AgentDataSourceSelection (ILogger<AgentDataSourceSelection> logger, ILogger<AgentBase> baseLogger, SettingsManager settingsManager, DataSourceService dataSourceService, ThreadSafeRandom rng) : AgentBase(baseLogger, settingsManager, dataSourceService, rng)
|
||||||
|
{
|
||||||
|
private static readonly ContentBlock EMPTY_BLOCK = new()
|
||||||
|
{
|
||||||
|
Content = null,
|
||||||
|
ContentType = ContentType.NONE,
|
||||||
|
Role = ChatRole.AGENT,
|
||||||
|
Time = DateTimeOffset.UtcNow,
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly List<ContentBlock> answers = new();
|
||||||
|
|
||||||
|
#region Overrides of AgentBase
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Type Type => Type.SYSTEM;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string Id => "Data Source Selection";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string JobDescription =>
|
||||||
|
"""
|
||||||
|
You receive a system and a user prompt, as well as a list of possible data sources as input.
|
||||||
|
Your task is to select the appropriate data sources for the given task. You may choose none,
|
||||||
|
one, or multiple sources, depending on what best fits the system and user prompt. You need
|
||||||
|
to estimate and assess which source, based on its description, might be helpful in
|
||||||
|
processing the prompts.
|
||||||
|
|
||||||
|
Your response is a JSON list in the following format:
|
||||||
|
|
||||||
|
```
|
||||||
|
[
|
||||||
|
{"id": "The data source ID", "reason": "Why did you choose this source?", "confidence": 0.87},
|
||||||
|
{"id": "The data source ID", "reason": "Why did you choose this source?", "confidence": 0.54}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
You express your confidence as a floating-point number between 0.0 (maximum uncertainty) and
|
||||||
|
1.0 (you are absolutely certain that this source is needed).
|
||||||
|
|
||||||
|
The JSON schema is:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"confidence": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"reason",
|
||||||
|
"confidence"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When no data source is needed, you return an empty JSON list `[]`. You do not ask any
|
||||||
|
follow-up questions. You do not address the user. Your response consists solely of
|
||||||
|
the JSON list.
|
||||||
|
""";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string SystemPrompt(string availableDataSources) => $"""
|
||||||
|
{this.JobDescription}
|
||||||
|
|
||||||
|
{availableDataSources}
|
||||||
|
""";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Settings.Provider? ProviderSettings { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data source selection agent does not work with context. Use
|
||||||
|
/// the process input method instead.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The chat thread without any changes.</returns>
|
||||||
|
public override Task<ChatThread> ProcessContext(ChatThread chatThread, IDictionary<string, string> additionalData) => Task.FromResult(chatThread);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<ContentBlock> ProcessInput(ContentBlock input, IDictionary<string, string> additionalData)
|
||||||
|
{
|
||||||
|
if (input.Content is not ContentText text)
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
if(text.InitialRemoteWait || text.IsStreaming)
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
if(string.IsNullOrWhiteSpace(text.Text))
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
if(!additionalData.TryGetValue("availableDataSources", out var availableDataSources) || string.IsNullOrWhiteSpace(availableDataSources))
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
var thread = this.CreateChatThread(this.SystemPrompt(availableDataSources));
|
||||||
|
var time = this.AddUserRequest(thread, text.Text);
|
||||||
|
await this.AddAIResponseAsync(thread, time);
|
||||||
|
|
||||||
|
var answer = thread.Blocks[^1];
|
||||||
|
|
||||||
|
this.answers.Add(answer);
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <inheritdoc />
|
||||||
|
public override Task<bool> MadeDecision(ContentBlock input) => Task.FromResult(true);
|
||||||
|
|
||||||
|
// <inheritdoc />
|
||||||
|
public override IReadOnlyCollection<ContentBlock> GetContext() => [];
|
||||||
|
|
||||||
|
// <inheritdoc />
|
||||||
|
public override IReadOnlyCollection<ContentBlock> GetAnswers() => this.answers;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public async Task<List<SelectedDataSource>> PerformSelectionAsync(IProvider provider, IContent lastPrompt, ChatThread chatThread, AllowedSelectedDataSources dataSources, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
logger.LogInformation("The AI should select the appropriate data sources.");
|
||||||
|
|
||||||
|
//
|
||||||
|
// 1. Which LLM provider should the agent use?
|
||||||
|
//
|
||||||
|
|
||||||
|
// We start with the provider currently selected by the user:
|
||||||
|
var agentProvider = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == provider.Id);
|
||||||
|
|
||||||
|
// If the user preselected an agent provider, we try to use this one:
|
||||||
|
if (this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions)
|
||||||
|
{
|
||||||
|
var configuredAgentProvider = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider);
|
||||||
|
|
||||||
|
// If the configured agent provider is available, we use it:
|
||||||
|
if (configuredAgentProvider != default)
|
||||||
|
agentProvider = configuredAgentProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign the provider settings to the agent:
|
||||||
|
logger.LogInformation($"The agent for the data source selection uses the provider '{agentProvider.InstanceName}' ({agentProvider.UsedLLMProvider.ToName()}, confidence={agentProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level.GetName()}).");
|
||||||
|
this.ProviderSettings = agentProvider;
|
||||||
|
|
||||||
|
//
|
||||||
|
// 2. Prepare the current system and user prompts as input for the agent:
|
||||||
|
//
|
||||||
|
var lastPromptContent = lastPrompt switch
|
||||||
|
{
|
||||||
|
ContentText text => text.Text,
|
||||||
|
|
||||||
|
// Image prompts may be empty, e.g., when the image is too large:
|
||||||
|
ContentImage image => await image.AsBase64(token),
|
||||||
|
|
||||||
|
// Other content types are not supported yet:
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(lastPromptContent))
|
||||||
|
{
|
||||||
|
logger.LogWarning("The last prompt is empty. The AI cannot select data sources.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 3. Prepare the allowed data sources as input for the agent:
|
||||||
|
//
|
||||||
|
var additionalData = new Dictionary<string, string>();
|
||||||
|
logger.LogInformation("Preparing the list of allowed data sources for the agent to choose from.");
|
||||||
|
|
||||||
|
// Notice: We do not dispose the Rust service here. The Rust service is a singleton
|
||||||
|
// and will be disposed when the application shuts down:
|
||||||
|
var rustService = Program.SERVICE_PROVIDER.GetService<RustService>()!;
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("The following data sources are available for selection:");
|
||||||
|
foreach (var ds in dataSources.AllowedDataSources)
|
||||||
|
{
|
||||||
|
switch (ds)
|
||||||
|
{
|
||||||
|
case DataSourceLocalDirectory localDirectory:
|
||||||
|
sb.AppendLine($"- Id={ds.Id}, name='{localDirectory.Name}', type=local directory, path='{localDirectory.Path}'");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DataSourceLocalFile localFile:
|
||||||
|
sb.AppendLine($"- Id={ds.Id}, name='{localFile.Name}', type=local file, path='{localFile.FilePath}'");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IERIDataSource eriDataSource:
|
||||||
|
var eriServerDescription = string.Empty;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Call the ERI server to get the server description:
|
||||||
|
//
|
||||||
|
using var eriClient = ERIClientFactory.Get(eriDataSource.Version, eriDataSource)!;
|
||||||
|
var authResponse = await eriClient.AuthenticateAsync(eriDataSource, rustService, token);
|
||||||
|
if (authResponse.Successful)
|
||||||
|
{
|
||||||
|
var serverDescriptionResponse = await eriClient.GetDataSourceInfoAsync(token);
|
||||||
|
if (serverDescriptionResponse.Successful)
|
||||||
|
{
|
||||||
|
eriServerDescription = serverDescriptionResponse.Data.Description;
|
||||||
|
|
||||||
|
// Remove all line breaks from the description:
|
||||||
|
eriServerDescription = eriServerDescription.Replace("\n", " ").Replace("\r", " ");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
logger.LogWarning($"Was not able to retrieve the server description from the ERI data source '{eriDataSource.Name}'. Message: {serverDescriptionResponse.Message}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
logger.LogWarning($"Was not able to authenticate with the ERI data source '{eriDataSource.Name}'. Message: {authResponse.Message}");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogWarning($"The ERI data source '{eriDataSource.Name}' is not available. Thus, we cannot retrieve the server description. Error: {e.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Append the ERI data source to the list. Use the server description if available:
|
||||||
|
//
|
||||||
|
if (string.IsNullOrWhiteSpace(eriServerDescription))
|
||||||
|
sb.AppendLine($"- Id={ds.Id}, name='{eriDataSource.Name}', type=external data source");
|
||||||
|
else
|
||||||
|
sb.AppendLine($"- Id={ds.Id}, name='{eriDataSource.Name}', type=external data source, description='{eriServerDescription}'");
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Prepared the list of allowed data sources for the agent.");
|
||||||
|
additionalData.Add("availableDataSources", sb.ToString());
|
||||||
|
|
||||||
|
//
|
||||||
|
// 4. Let the agent select the data sources:
|
||||||
|
//
|
||||||
|
var prompt = $"""
|
||||||
|
The system prompt is:
|
||||||
|
|
||||||
|
```
|
||||||
|
{chatThread.SystemPrompt}
|
||||||
|
```
|
||||||
|
|
||||||
|
The user prompt is:
|
||||||
|
|
||||||
|
```
|
||||||
|
{lastPromptContent}
|
||||||
|
```
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Call the agent:
|
||||||
|
var aiResponse = await this.ProcessInput(new ContentBlock
|
||||||
|
{
|
||||||
|
Time = DateTimeOffset.UtcNow,
|
||||||
|
ContentType = ContentType.TEXT,
|
||||||
|
Role = ChatRole.USER,
|
||||||
|
Content = new ContentText
|
||||||
|
{
|
||||||
|
Text = prompt,
|
||||||
|
},
|
||||||
|
}, additionalData);
|
||||||
|
|
||||||
|
if(aiResponse.Content is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("The agent did not return a response.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (aiResponse)
|
||||||
|
{
|
||||||
|
|
||||||
|
//
|
||||||
|
// 5. Parse the agent response:
|
||||||
|
//
|
||||||
|
case { ContentType: ContentType.TEXT, Content: ContentText textContent }:
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// What we expect is a JSON list of SelectedDataSource objects:
|
||||||
|
//
|
||||||
|
var selectedDataSourcesJson = textContent.Text;
|
||||||
|
|
||||||
|
//
|
||||||
|
// We know how bad LLM may be in generating JSON without surrounding text.
|
||||||
|
// Thus, we expect the worst and try to extract the JSON list from the text:
|
||||||
|
//
|
||||||
|
var json = this.ExtractJson(selectedDataSourcesJson);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var aiSelectedDataSources = JsonSerializer.Deserialize<List<SelectedDataSource>>(json, JSON_SERIALIZER_OPTIONS);
|
||||||
|
return aiSelectedDataSources ?? [];
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
logger.LogWarning("The agent answered with an invalid or unexpected JSON format.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case { ContentType: ContentType.TEXT }:
|
||||||
|
logger.LogWarning("The agent answered with an unexpected inner content type.");
|
||||||
|
return [];
|
||||||
|
|
||||||
|
case { ContentType: ContentType.NONE }:
|
||||||
|
logger.LogWarning("The agent did not return a response.");
|
||||||
|
return [];
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.LogWarning($"The agent answered with an unexpected content type '{aiResponse.ContentType}'.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts the JSON list from the given text. The text may contain additional
|
||||||
|
/// information around the JSON list. The method tries to extract the JSON list
|
||||||
|
/// from the text.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Algorithm: The method searches for the first line that contains only a '[' character.
|
||||||
|
/// Then, it searches for the first line that contains only a ']' character. The method
|
||||||
|
/// returns the text between these two lines (including the brackets). When the method
|
||||||
|
/// cannot find the JSON list, it returns an empty string.
|
||||||
|
/// <br/><br/>
|
||||||
|
/// This overload is using strings instead of spans. We can use this overload in any
|
||||||
|
/// async method. Thus, it is a wrapper around the span-based method. Yes, we are losing
|
||||||
|
/// the memory efficiency of the span-based method, but we still gain the performance
|
||||||
|
/// of the span-based method: the entire search algorithm is span-based.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="text">The text that may contain the JSON list.</param>
|
||||||
|
/// <returns>The extracted JSON list.</returns>
|
||||||
|
private string ExtractJson(string text) => ExtractJson(text.AsSpan()).ToString();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts the JSON list from the given text. The text may contain additional
|
||||||
|
/// information around the JSON list. The method tries to extract the JSON list
|
||||||
|
/// from the text.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Algorithm: The method searches for the first line that contains only a '[' character.
|
||||||
|
/// Then, it searches for the first line that contains only a ']' character. The method
|
||||||
|
/// returns the text between these two lines (including the brackets). When the method
|
||||||
|
/// cannot find the JSON list, it returns an empty string.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="text">The text that may contain the JSON list.</param>
|
||||||
|
/// <returns>The extracted JSON list.</returns>
|
||||||
|
private static ReadOnlySpan<char> ExtractJson(ReadOnlySpan<char> text)
|
||||||
|
{
|
||||||
|
var startIndex = -1;
|
||||||
|
var endIndex = -1;
|
||||||
|
var foundStart = false;
|
||||||
|
var foundEnd = false;
|
||||||
|
var lineStart = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i <= text.Length; i++)
|
||||||
|
{
|
||||||
|
// Handle the end of the line or the end of the text:
|
||||||
|
if (i == text.Length || text[i] == '\n')
|
||||||
|
{
|
||||||
|
if (IsCharacterAloneInLine(text, lineStart, i, '[') && !foundStart)
|
||||||
|
{
|
||||||
|
startIndex = lineStart;
|
||||||
|
foundStart = true;
|
||||||
|
}
|
||||||
|
else if (IsCharacterAloneInLine(text, lineStart, i, ']') && foundStart && !foundEnd)
|
||||||
|
{
|
||||||
|
endIndex = i;
|
||||||
|
foundEnd = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineStart = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundStart && foundEnd)
|
||||||
|
{
|
||||||
|
// Adjust endIndex for slicing, ensuring it's within bounds:
|
||||||
|
return text.Slice(startIndex, Math.Min(text.Length, endIndex + 1) - startIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReadOnlySpan<char>.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsCharacterAloneInLine(ReadOnlySpan<char> text, int lineStart, int lineEnd, char character)
|
||||||
|
{
|
||||||
|
for (var i = lineStart; i < lineEnd; i++)
|
||||||
|
if (!char.IsWhiteSpace(text[i]) && text[i] != character)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
9
app/MindWork AI Studio/Agents/SelectedDataSource.cs
Normal file
9
app/MindWork AI Studio/Agents/SelectedDataSource.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace AIStudio.Agents;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a selected data source, chosen by the agent.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Id">The data source ID.</param>
|
||||||
|
/// <param name="Reason">The reason for selecting the data source.</param>
|
||||||
|
/// <param name="Confidence">The confidence of the agent in the selection.</param>
|
||||||
|
public readonly record struct SelectedDataSource(string Id, string Reason, float Confidence);
|
@ -1,3 +1,4 @@
|
|||||||
|
using AIStudio.Components;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
using AIStudio.Settings.DataModel;
|
using AIStudio.Settings.DataModel;
|
||||||
|
|
||||||
@ -33,6 +34,11 @@ public sealed record ChatThread
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DataSourceOptions DataSourceOptions { get; set; } = new();
|
public DataSourceOptions DataSourceOptions { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The AI-selected data sources for this chat thread.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<DataSourceAgentSelected> AISelectedDataSources { get; set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
|
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -49,4 +49,62 @@ public sealed class ContentImage : IContent
|
|||||||
/// The image source.
|
/// The image source.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string Source { get; set; }
|
public required string Source { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read the image content as a base64 string.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="token">The cancellation token.</param>
|
||||||
|
/// <returns>The image content as a base64 string; might be empty.</returns>
|
||||||
|
public async Task<string> AsBase64(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
switch (this.SourceType)
|
||||||
|
{
|
||||||
|
case ContentImageSource.BASE64:
|
||||||
|
return this.Source;
|
||||||
|
|
||||||
|
case ContentImageSource.URL:
|
||||||
|
{
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
using var response = await httpClient.GetAsync(this.Source, HttpCompletionOption.ResponseHeadersRead, token);
|
||||||
|
if(response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
// Read the length of the content:
|
||||||
|
var lengthBytes = response.Content.Headers.ContentLength;
|
||||||
|
if(lengthBytes > 10_000_000)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var bytes = await response.Content.ReadAsByteArrayAsync(token);
|
||||||
|
return Convert.ToBase64String(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ContentImageSource.LOCAL_PATH:
|
||||||
|
if(File.Exists(this.Source))
|
||||||
|
{
|
||||||
|
// Read the content length:
|
||||||
|
var length = new FileInfo(this.Source).Length;
|
||||||
|
if(length > 10_000_000)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var bytes = await File.ReadAllBytesAsync(this.Source, token);
|
||||||
|
return Convert.ToBase64String(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,5 +1,7 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
using AIStudio.Agents;
|
||||||
|
using AIStudio.Components;
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
using AIStudio.Tools.Services;
|
using AIStudio.Tools.Services;
|
||||||
@ -41,11 +43,19 @@ public sealed class ContentText : IContent
|
|||||||
if(chatThread is null)
|
if(chatThread is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var logger = Program.SERVICE_PROVIDER.GetService<ILogger<ContentText>>()!;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Check if the user wants to bind any data sources to the chat:
|
// 1. Check if the user wants to bind any data sources to the chat:
|
||||||
//
|
//
|
||||||
if (chatThread.DataSourceOptions.IsEnabled())
|
if (chatThread.DataSourceOptions.IsEnabled() && lastPrompt is not null)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation("Data sources are enabled for this chat.");
|
||||||
|
|
||||||
|
// Across the different code-branches, we keep track of whether it
|
||||||
|
// makes sense to proceed with the RAG process:
|
||||||
|
var proceedWithRAG = true;
|
||||||
|
|
||||||
//
|
//
|
||||||
// When the user wants to bind data sources to the chat, we
|
// When the user wants to bind data sources to the chat, we
|
||||||
// have to check if the data sources are available for the
|
// have to check if the data sources are available for the
|
||||||
@ -61,16 +71,144 @@ public sealed class ContentText : IContent
|
|||||||
//
|
//
|
||||||
if (chatThread.DataSourceOptions.AutomaticDataSourceSelection)
|
if (chatThread.DataSourceOptions.AutomaticDataSourceSelection)
|
||||||
{
|
{
|
||||||
// TODO: Start agent based on allowed data sources.
|
// Get the agent for the data source selection:
|
||||||
|
var selectionAgent = Program.SERVICE_PROVIDER.GetService<AgentDataSourceSelection>()!;
|
||||||
|
|
||||||
|
// Let the AI agent do its work:
|
||||||
|
IReadOnlyList<DataSourceAgentSelected> finalAISelection = [];
|
||||||
|
var aiSelectedDataSources = await selectionAgent.PerformSelectionAsync(provider, lastPrompt, chatThread, dataSources, token);
|
||||||
|
|
||||||
|
// Check if the AI selected any data sources:
|
||||||
|
if(aiSelectedDataSources.Count is 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning("The AI did not select any data sources. The RAG process is skipped.");
|
||||||
|
proceedWithRAG = false;
|
||||||
|
|
||||||
|
// Send the selected data sources to the data source selection component.
|
||||||
|
// Then, the user can see which data sources were selected by the AI.
|
||||||
|
await MessageBus.INSTANCE.SendMessage(null, Event.RAG_AUTO_DATA_SOURCES_SELECTED, finalAISelection);
|
||||||
|
chatThread.AISelectedDataSources = finalAISelection;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Log the selected data sources:
|
||||||
|
var selectedDataSourceInfo = aiSelectedDataSources.Select(ds => $"[Id={ds.Id}, reason={ds.Reason}, confidence={ds.Confidence}]").Aggregate((a, b) => $"'{a}', '{b}'");
|
||||||
|
logger.LogInformation($"The AI selected the data sources automatically. {aiSelectedDataSources.Count} data source(s) are selected: {selectedDataSourceInfo}.");
|
||||||
|
|
||||||
|
//
|
||||||
|
// Check how many data sources were hallucinated by the AI:
|
||||||
|
//
|
||||||
|
var totalAISelectedDataSources = aiSelectedDataSources.Count;
|
||||||
|
|
||||||
|
// Filter out the data sources that are not available:
|
||||||
|
aiSelectedDataSources = aiSelectedDataSources.Where(x => settings.ConfigurationData.DataSources.FirstOrDefault(ds => ds.Id == x.Id) is not null).ToList();
|
||||||
|
|
||||||
|
// Store the real AI-selected data sources:
|
||||||
|
finalAISelection = aiSelectedDataSources.Select(x => new DataSourceAgentSelected { DataSource = settings.ConfigurationData.DataSources.First(ds => ds.Id == x.Id), AIDecision = x, Selected = false }).ToList();
|
||||||
|
|
||||||
|
var numHallucinatedSources = totalAISelectedDataSources - aiSelectedDataSources.Count;
|
||||||
|
if(numHallucinatedSources > 0)
|
||||||
|
logger.LogWarning($"The AI hallucinated {numHallucinatedSources} data source(s). We ignore them.");
|
||||||
|
|
||||||
|
if (aiSelectedDataSources.Count > 3)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// We have more than 3 data sources. Let's filter by confidence.
|
||||||
|
// In order to do that, we must identify the lower and upper
|
||||||
|
// bounds of the confidence interval:
|
||||||
|
//
|
||||||
|
var confidenceValues = aiSelectedDataSources.Select(x => x.Confidence).ToList();
|
||||||
|
var lowerBound = confidenceValues.Min();
|
||||||
|
var upperBound = confidenceValues.Max();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Next, we search for a threshold so that we have between 2 and 3
|
||||||
|
// data sources. When not possible, we take all data sources.
|
||||||
|
//
|
||||||
|
var threshold = 0.0f;
|
||||||
|
|
||||||
|
// Check the case where the confidence values are too close:
|
||||||
|
if (upperBound - lowerBound >= 0.01)
|
||||||
|
{
|
||||||
|
var previousThreshold = 0.0f;
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
threshold = lowerBound + (upperBound - lowerBound) * i / 10;
|
||||||
|
var numMatches = aiSelectedDataSources.Count(x => x.Confidence >= threshold);
|
||||||
|
if (numMatches <= 1)
|
||||||
|
{
|
||||||
|
threshold = previousThreshold;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numMatches is <= 3 and >= 2)
|
||||||
|
break;
|
||||||
|
|
||||||
|
previousThreshold = threshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Filter the data sources by the threshold:
|
||||||
|
//
|
||||||
|
aiSelectedDataSources = aiSelectedDataSources.Where(x => x.Confidence >= threshold).ToList();
|
||||||
|
foreach (var dataSource in finalAISelection)
|
||||||
|
if(aiSelectedDataSources.Any(x => x.Id == dataSource.DataSource.Id))
|
||||||
|
dataSource.Selected = true;
|
||||||
|
|
||||||
|
logger.LogInformation($"The AI selected {aiSelectedDataSources.Count} data source(s) with a confidence of at least {threshold}.");
|
||||||
|
|
||||||
|
// Transform the final data sources to the actual data sources:
|
||||||
|
selectedDataSources = aiSelectedDataSources.Select(x => settings.ConfigurationData.DataSources.FirstOrDefault(ds => ds.Id == x.Id)).Where(ds => ds is not null).ToList()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have max. 3 data sources. We take all of them:
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Transform the selected data sources to the actual data sources:
|
||||||
|
selectedDataSources = aiSelectedDataSources.Select(x => settings.ConfigurationData.DataSources.FirstOrDefault(ds => ds.Id == x.Id)).Where(ds => ds is not null).ToList()!;
|
||||||
|
|
||||||
|
// Mark the data sources as selected:
|
||||||
|
foreach (var dataSource in finalAISelection)
|
||||||
|
dataSource.Selected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the selected data sources to the data source selection component.
|
||||||
|
// Then, the user can see which data sources were selected by the AI.
|
||||||
|
await MessageBus.INSTANCE.SendMessage(null, Event.RAG_AUTO_DATA_SOURCES_SELECTED, finalAISelection);
|
||||||
|
chatThread.AISelectedDataSources = finalAISelection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// No, the user made the choice manually:
|
||||||
|
//
|
||||||
|
var selectedDataSourceInfo = selectedDataSources.Select(ds => ds.Name).Aggregate((a, b) => $"'{a}', '{b}'");
|
||||||
|
logger.LogInformation($"The user selected the data sources manually. {selectedDataSources.Count} data source(s) are selected: {selectedDataSourceInfo}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(selectedDataSources.Count == 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning("No data sources are selected. The RAG process is skipped.");
|
||||||
|
proceedWithRAG = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Trigger the retrieval part of the (R)AG process:
|
// Trigger the retrieval part of the (R)AG process:
|
||||||
//
|
//
|
||||||
|
if (proceedWithRAG)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Perform the augmentation of the R(A)G process:
|
// Perform the augmentation of the R(A)G process:
|
||||||
//
|
//
|
||||||
|
if (proceedWithRAG)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the last time we got a response. We use this later
|
// Store the last time we got a response. We use this later
|
||||||
|
@ -111,7 +111,7 @@
|
|||||||
|
|
||||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||||
{
|
{
|
||||||
<DataSourceSelection @ref="@this.dataSourceSelectionComponent" PopoverTriggerMode="PopoverTriggerMode.BUTTON" PopoverButtonClasses="ma-3" LLMProvider="@this.Provider" DataSourceOptions="@this.GetCurrentDataSourceOptions()" DataSourceOptionsChanged="@(async options => await this.SetCurrentDataSourceOptions(options))"/>
|
<DataSourceSelection @ref="@this.dataSourceSelectionComponent" PopoverTriggerMode="PopoverTriggerMode.BUTTON" PopoverButtonClasses="ma-3" LLMProvider="@this.Provider" DataSourceOptions="@this.GetCurrentDataSourceOptions()" DataSourceOptionsChanged="@(async options => await this.SetCurrentDataSourceOptions(options))" DataSourcesAISelected="@this.GetAgentSelectedDataSources()"/>
|
||||||
}
|
}
|
||||||
</MudToolBar>
|
</MudToolBar>
|
||||||
</FooterContent>
|
</FooterContent>
|
||||||
|
@ -305,6 +305,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<DataSourceAgentSelected> GetAgentSelectedDataSources()
|
||||||
|
{
|
||||||
|
if (this.ChatThread is null)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return this.ChatThread.AISelectedDataSources;
|
||||||
|
}
|
||||||
|
|
||||||
private DataSourceOptions GetCurrentDataSourceOptions()
|
private DataSourceOptions GetCurrentDataSourceOptions()
|
||||||
{
|
{
|
||||||
if (this.ChatThread is not null)
|
if (this.ChatThread is not null)
|
||||||
@ -481,6 +489,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
|
|
||||||
// Disable the stream state:
|
// Disable the stream state:
|
||||||
this.isStreaming = false;
|
this.isStreaming = false;
|
||||||
|
|
||||||
|
// Update the UI:
|
||||||
this.StateHasChanged();
|
this.StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -674,7 +684,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
|
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
|
||||||
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
||||||
this.WorkspaceName(this.currentWorkspaceName);
|
this.WorkspaceName(this.currentWorkspaceName);
|
||||||
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions);
|
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
25
app/MindWork AI Studio/Components/DataSourceAgentSelected.cs
Normal file
25
app/MindWork AI Studio/Components/DataSourceAgentSelected.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using AIStudio.Agents;
|
||||||
|
using AIStudio.Settings;
|
||||||
|
|
||||||
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A data structure to combine the data source and the underlying AI decision.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DataSourceAgentSelected
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The data source.
|
||||||
|
/// </summary>
|
||||||
|
public required IDataSource DataSource { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The AI decision, which led to the selection of the data source.
|
||||||
|
/// </summary>
|
||||||
|
public required SelectedDataSource AIDecision { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether the data source is part of the final selection for the RAG process.
|
||||||
|
/// </summary>
|
||||||
|
public bool Selected { get; set; }
|
||||||
|
}
|
@ -24,7 +24,7 @@
|
|||||||
<MudText Typo="Typo.h5">Data Source Selection</MudText>
|
<MudText Typo="Typo.h5">Data Source Selection</MudText>
|
||||||
</CardHeaderContent>
|
</CardHeaderContent>
|
||||||
</MudCardHeader>
|
</MudCardHeader>
|
||||||
<MudCardContent Style="max-height: 60vh; overflow: auto;">
|
<MudCardContent Style="min-width: 24em; max-height: 60vh; max-width: 45vw; overflow: auto;">
|
||||||
@if (this.waitingForDataSources)
|
@if (this.waitingForDataSources)
|
||||||
{
|
{
|
||||||
<MudSkeleton Width="30%" Height="42px;"/>
|
<MudSkeleton Width="30%" Height="42px;"/>
|
||||||
@ -38,8 +38,11 @@
|
|||||||
{
|
{
|
||||||
<MudTextSwitch Label="AI-based data source selection" Value="@this.aiBasedSourceSelection" LabelOn="Yes, let the AI decide which data sources are needed." LabelOff="No, I manually decide which data source to use." ValueChanged="@this.AutoModeChanged"/>
|
<MudTextSwitch Label="AI-based data source selection" Value="@this.aiBasedSourceSelection" LabelOn="Yes, let the AI decide which data sources are needed." LabelOff="No, I manually decide which data source to use." ValueChanged="@this.AutoModeChanged"/>
|
||||||
<MudTextSwitch Label="AI-based data validation" Value="@this.aiBasedValidation" LabelOn="Yes, let the AI validate & filter the retrieved data." LabelOff="No, use all data retrieved from the data sources." ValueChanged="@this.ValidationModeChanged"/>
|
<MudTextSwitch Label="AI-based data validation" Value="@this.aiBasedValidation" LabelOn="Yes, let the AI validate & filter the retrieved data." LabelOff="No, use all data retrieved from the data sources." ValueChanged="@this.ValidationModeChanged"/>
|
||||||
|
|
||||||
|
@if (this.aiBasedSourceSelection is false || this.DataSourcesAISelected.Count == 0)
|
||||||
|
{
|
||||||
<MudField Label="Available Data Sources" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.aiBasedSourceSelection">
|
<MudField Label="Available Data Sources" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.aiBasedSourceSelection">
|
||||||
<MudList T="IDataSource" SelectionMode="MudBlazor.SelectionMode.MultiSelection" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))" Style="max-height: 14em;">
|
<MudList T="IDataSource" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))" Style="max-height: 14em;">
|
||||||
@foreach (var source in this.availableDataSources)
|
@foreach (var source in this.availableDataSources)
|
||||||
{
|
{
|
||||||
<MudListItem Value="@source">
|
<MudListItem Value="@source">
|
||||||
@ -49,6 +52,41 @@
|
|||||||
</MudList>
|
</MudList>
|
||||||
</MudField>
|
</MudField>
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudExpansionPanels MultiExpansion="@false" Class="mt-3" Style="max-height: 14em;">
|
||||||
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.TouchApp" HeaderText="Available Data Sources">
|
||||||
|
<MudList T="IDataSource" SelectionMode="MudBlazor.SelectionMode.SingleSelection" SelectedValues="@this.selectedDataSources" Style="max-height: 14em;">
|
||||||
|
@foreach (var source in this.availableDataSources)
|
||||||
|
{
|
||||||
|
<MudListItem Value="@source">
|
||||||
|
@source.Name
|
||||||
|
</MudListItem>
|
||||||
|
}
|
||||||
|
</MudList>
|
||||||
|
</ExpansionPanel>
|
||||||
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Filter" HeaderText="AI-Selected Data Sources">
|
||||||
|
<MudList T="DataSourceAgentSelected" SelectionMode="MudBlazor.SelectionMode.MultiSelection" ReadOnly="@true" SelectedValues="@this.GetSelectedDataSourcesWithAI()" Style="max-height: 14em;">
|
||||||
|
@foreach (var source in this.DataSourcesAISelected)
|
||||||
|
{
|
||||||
|
<MudListItem Value="@source">
|
||||||
|
<ChildContent>
|
||||||
|
<MudText Typo="Typo.body1">
|
||||||
|
@source.DataSource.Name
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudProgressLinear Color="Color.Info" Min="0" Max="1" Value="@source.AIDecision.Confidence"/>
|
||||||
|
<MudJustifiedText Typo="Typo.body2">
|
||||||
|
@this.GetAIReasoning(source)
|
||||||
|
</MudJustifiedText>
|
||||||
|
</ChildContent>
|
||||||
|
</MudListItem>
|
||||||
|
}
|
||||||
|
</MudList>
|
||||||
|
</ExpansionPanel>
|
||||||
|
</MudExpansionPanels>
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
<MudCardActions>
|
<MudCardActions>
|
||||||
@ -79,7 +117,7 @@ else if (this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE)
|
|||||||
<MudTextSwitch Label="AI-based data source selection" Value="@this.aiBasedSourceSelection" LabelOn="Yes, let the AI decide which data sources are needed." LabelOff="No, I manually decide which data source to use." ValueChanged="@this.AutoModeChanged"/>
|
<MudTextSwitch Label="AI-based data source selection" Value="@this.aiBasedSourceSelection" LabelOn="Yes, let the AI decide which data sources are needed." LabelOff="No, I manually decide which data source to use." ValueChanged="@this.AutoModeChanged"/>
|
||||||
<MudTextSwitch Label="AI-based data validation" Value="@this.aiBasedValidation" LabelOn="Yes, let the AI validate & filter the retrieved data." LabelOff="No, use all data retrieved from the data sources." ValueChanged="@this.ValidationModeChanged"/>
|
<MudTextSwitch Label="AI-based data validation" Value="@this.aiBasedValidation" LabelOn="Yes, let the AI validate & filter the retrieved data." LabelOff="No, use all data retrieved from the data sources." ValueChanged="@this.ValidationModeChanged"/>
|
||||||
<MudField Label="Available Data Sources" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.aiBasedSourceSelection">
|
<MudField Label="Available Data Sources" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.aiBasedSourceSelection">
|
||||||
<MudList T="IDataSource" SelectionMode="MudBlazor.SelectionMode.MultiSelection" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))">
|
<MudList T="IDataSource" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))">
|
||||||
@foreach (var source in this.availableDataSources)
|
@foreach (var source in this.availableDataSources)
|
||||||
{
|
{
|
||||||
<MudListItem Value="@source">
|
<MudListItem Value="@source">
|
||||||
|
@ -26,6 +26,9 @@ public partial class DataSourceSelection : ComponentBase, IMessageBusReceiver, I
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<DataSourceOptions> DataSourceOptionsChanged { get; set; }
|
public EventCallback<DataSourceOptions> DataSourceOptionsChanged { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public IReadOnlyList<DataSourceAgentSelected> DataSourcesAISelected { get; set; } = [];
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string ConfigurationHeaderMessage { get; set; } = string.Empty;
|
public string ConfigurationHeaderMessage { get; set; } = string.Empty;
|
||||||
|
|
||||||
@ -58,7 +61,7 @@ public partial class DataSourceSelection : ComponentBase, IMessageBusReceiver, I
|
|||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
this.MessageBus.RegisterComponent(this);
|
this.MessageBus.RegisterComponent(this);
|
||||||
this.MessageBus.ApplyFilters(this, [], [ Event.COLOR_THEME_CHANGED ]);
|
this.MessageBus.ApplyFilters(this, [], [ Event.COLOR_THEME_CHANGED, Event.RAG_AUTO_DATA_SOURCES_SELECTED ]);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Load the settings:
|
// Load the settings:
|
||||||
@ -129,9 +132,17 @@ public partial class DataSourceSelection : ComponentBase, IMessageBusReceiver, I
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public void ChangeOptionWithoutSaving(DataSourceOptions options)
|
private SelectionMode GetListSelectionMode() => this.aiBasedSourceSelection ? MudBlazor.SelectionMode.SingleSelection : MudBlazor.SelectionMode.MultiSelection;
|
||||||
|
|
||||||
|
private IReadOnlyCollection<DataSourceAgentSelected> GetSelectedDataSourcesWithAI() => this.DataSourcesAISelected.Where(n => n.Selected).ToList();
|
||||||
|
|
||||||
|
private string GetAIReasoning(DataSourceAgentSelected source) => $"AI reasoning (confidence {source.AIDecision.Confidence:P0}): {source.AIDecision.Reason}";
|
||||||
|
|
||||||
|
public void ChangeOptionWithoutSaving(DataSourceOptions options, IReadOnlyList<DataSourceAgentSelected>? aiSelectedDataSources = null)
|
||||||
{
|
{
|
||||||
this.DataSourceOptions = options;
|
this.DataSourceOptions = options;
|
||||||
|
this.DataSourcesAISelected = aiSelectedDataSources ?? [];
|
||||||
|
|
||||||
this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection;
|
this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection;
|
||||||
this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation;
|
this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation;
|
||||||
this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources;
|
this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources;
|
||||||
@ -237,6 +248,13 @@ public partial class DataSourceSelection : ComponentBase, IMessageBusReceiver, I
|
|||||||
this.showDataSourceSelection = false;
|
this.showDataSourceSelection = false;
|
||||||
this.StateHasChanged();
|
this.StateHasChanged();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case Event.RAG_AUTO_DATA_SOURCES_SELECTED:
|
||||||
|
if(data is IReadOnlyList<DataSourceAgentSelected> aiSelectedDataSources)
|
||||||
|
this.DataSourcesAISelected = aiSelectedDataSources;
|
||||||
|
|
||||||
|
this.StateHasChanged();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
@inherits SettingsPanelBase
|
||||||
|
|
||||||
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.SelectAll" HeaderText="Agent: Data Source Selection Options">
|
||||||
|
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||||
|
<MudText Typo="Typo.body1" Class="mb-3">
|
||||||
|
Use Case: this agent is used to select the appropriate data sources for the current prompt.
|
||||||
|
</MudText>
|
||||||
|
<ConfigurationOption OptionDescription="Preselect data source selection options?" LabelOn="Options are preselected" LabelOff="No options are preselected" State="@(() => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions = updatedState)" OptionHelp="When enabled, you can preselect some agent options. This is might be useful when you prefer a LLM."/>
|
||||||
|
<ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider = selectedValue)"/>
|
||||||
|
</MudPaper>
|
||||||
|
</ExpansionPanel>
|
@ -0,0 +1,3 @@
|
|||||||
|
namespace AIStudio.Components.Settings;
|
||||||
|
|
||||||
|
public partial class SettingsPanelAgentDataSourceSelection : SettingsPanelBase;
|
@ -27,6 +27,7 @@
|
|||||||
<SettingsPanelSynonyms AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelSynonyms AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
<SettingsPanelMyTasks AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelMyTasks AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
<SettingsPanelAssistantBias AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelAssistantBias AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
|
<SettingsPanelAgentDataSourceSelection AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
<SettingsPanelAgentContentCleaner AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelAgentContentCleaner AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
</MudExpansionPanels>
|
</MudExpansionPanels>
|
||||||
</InnerScrolling>
|
</InnerScrolling>
|
||||||
|
@ -21,6 +21,7 @@ internal sealed class Program
|
|||||||
public static RustService RUST_SERVICE = null!;
|
public static RustService RUST_SERVICE = null!;
|
||||||
public static Encryption ENCRYPTION = null!;
|
public static Encryption ENCRYPTION = null!;
|
||||||
public static string API_TOKEN = null!;
|
public static string API_TOKEN = null!;
|
||||||
|
public static IServiceProvider SERVICE_PROVIDER = null!;
|
||||||
|
|
||||||
public static async Task Main(string[] args)
|
public static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
@ -117,6 +118,7 @@ internal sealed class Program
|
|||||||
builder.Services.AddSingleton<ThreadSafeRandom>();
|
builder.Services.AddSingleton<ThreadSafeRandom>();
|
||||||
builder.Services.AddSingleton<DataSourceService>();
|
builder.Services.AddSingleton<DataSourceService>();
|
||||||
builder.Services.AddTransient<HTMLParser>();
|
builder.Services.AddTransient<HTMLParser>();
|
||||||
|
builder.Services.AddTransient<AgentDataSourceSelection>();
|
||||||
builder.Services.AddTransient<AgentTextContentCleaner>();
|
builder.Services.AddTransient<AgentTextContentCleaner>();
|
||||||
builder.Services.AddHostedService<UpdateService>();
|
builder.Services.AddHostedService<UpdateService>();
|
||||||
builder.Services.AddHostedService<TemporaryChatService>();
|
builder.Services.AddHostedService<TemporaryChatService>();
|
||||||
@ -148,6 +150,10 @@ internal sealed class Program
|
|||||||
var programLogger = app.Services.GetRequiredService<ILogger<Program>>();
|
var programLogger = app.Services.GetRequiredService<ILogger<Program>>();
|
||||||
programLogger.LogInformation("Starting the AI Studio server.");
|
programLogger.LogInformation("Starting the AI Studio server.");
|
||||||
|
|
||||||
|
// Store the service provider (DI). We need it later for some classes,
|
||||||
|
// which are not part of the request pipeline:
|
||||||
|
SERVICE_PROVIDER = app.Services;
|
||||||
|
|
||||||
// Initialize the encryption service:
|
// Initialize the encryption service:
|
||||||
programLogger.LogInformation("Initializing the encryption service.");
|
programLogger.LogInformation("Initializing the encryption service.");
|
||||||
var encryptionLogger = app.Services.GetRequiredService<ILogger<Encryption>>();
|
var encryptionLogger = app.Services.GetRequiredService<ILogger<Encryption>>();
|
||||||
@ -196,5 +202,8 @@ internal sealed class Program
|
|||||||
};
|
};
|
||||||
|
|
||||||
await serverTask;
|
await serverTask;
|
||||||
|
|
||||||
|
RUST_SERVICE.Dispose();
|
||||||
|
programLogger.LogInformation("The AI Studio server was stopped.");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -74,6 +74,8 @@ public sealed class Data
|
|||||||
|
|
||||||
public DataTextContentCleaner TextContentCleaner { get; init; } = new();
|
public DataTextContentCleaner TextContentCleaner { get; init; } = new();
|
||||||
|
|
||||||
|
public DataAgentDataSourceSelection AgentDataSourceSelection { get; init; } = new();
|
||||||
|
|
||||||
public DataAgenda Agenda { get; init; } = new();
|
public DataAgenda Agenda { get; init; } = new();
|
||||||
|
|
||||||
public DataGrammarSpelling GrammarSpelling { get; init; } = new();
|
public DataGrammarSpelling GrammarSpelling { get; init; } = new();
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
namespace AIStudio.Settings.DataModel;
|
||||||
|
|
||||||
|
public sealed class DataAgentDataSourceSelection
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Preselect any text content cleaner options?
|
||||||
|
/// </summary>
|
||||||
|
public bool PreselectAgentOptions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Preselect a text content cleaner provider?
|
||||||
|
/// </summary>
|
||||||
|
public string PreselectedAgentProvider { get; set; } = string.Empty;
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
|
|
||||||
|
using AIStudio.Assistants.ERI;
|
||||||
using AIStudio.Tools.ERIClient.DataModel;
|
using AIStudio.Tools.ERIClient.DataModel;
|
||||||
|
|
||||||
namespace AIStudio.Settings.DataModel;
|
namespace AIStudio.Settings.DataModel;
|
||||||
@ -39,4 +40,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED;
|
public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ERIVersion Version { get; init; } = ERIVersion.V1;
|
||||||
}
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
using AIStudio.Assistants.ERI;
|
||||||
using AIStudio.Tools.ERIClient.DataModel;
|
using AIStudio.Tools.ERIClient.DataModel;
|
||||||
|
|
||||||
namespace AIStudio.Settings;
|
namespace AIStudio.Settings;
|
||||||
@ -23,4 +24,9 @@ public interface IERIDataSource : IExternalDataSource
|
|||||||
/// The username to use for authentication, when the auth. method is USERNAME_PASSWORD.
|
/// The username to use for authentication, when the auth. method is USERNAME_PASSWORD.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Username { get; init; }
|
public string Username { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ERI specification to use.
|
||||||
|
/// </summary>
|
||||||
|
public ERIVersion Version { get; init; }
|
||||||
}
|
}
|
@ -23,6 +23,9 @@ public enum Event
|
|||||||
WORKSPACE_LOADED_CHAT_CHANGED,
|
WORKSPACE_LOADED_CHAT_CHANGED,
|
||||||
WORKSPACE_TOGGLE_OVERLAY,
|
WORKSPACE_TOGGLE_OVERLAY,
|
||||||
|
|
||||||
|
// RAG events:
|
||||||
|
RAG_AUTO_DATA_SOURCES_SELECTED,
|
||||||
|
|
||||||
// Send events:
|
// Send events:
|
||||||
SEND_TO_GRAMMAR_SPELLING_ASSISTANT,
|
SEND_TO_GRAMMAR_SPELLING_ASSISTANT,
|
||||||
SEND_TO_ICON_FINDER_ASSISTANT,
|
SEND_TO_ICON_FINDER_ASSISTANT,
|
||||||
|
@ -2,5 +2,6 @@
|
|||||||
- Added the possibility to select data sources for chats. This preview feature is hidden behind the RAG feature flag, check your app options in case you want to enable it.
|
- Added the possibility to select data sources for chats. This preview feature is hidden behind the RAG feature flag, check your app options in case you want to enable it.
|
||||||
- Added an option to all data sources to select a local security policy. This preview feature is hidden behind the RAG feature flag.
|
- Added an option to all data sources to select a local security policy. This preview feature is hidden behind the RAG feature flag.
|
||||||
- Added an option to preselect data sources and options for new chats. This preview feature is hidden behind the RAG feature flag.
|
- Added an option to preselect data sources and options for new chats. This preview feature is hidden behind the RAG feature flag.
|
||||||
|
- Added an agent to select the appropriate data sources for any prompt. This preview feature is hidden behind the RAG feature flag.
|
||||||
- Improved confidence card for small spaces.
|
- Improved confidence card for small spaces.
|
||||||
- Fixed a bug in which 'APP_SETTINGS' appeared as a valid destination in the "send to" menu.
|
- Fixed a bug in which 'APP_SETTINGS' appeared as a valid destination in the "send to" menu.
|
Loading…
Reference in New Issue
Block a user