mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-21 15:38:00 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37bd42e41f | ||
|
|
38103121c9 | ||
|
|
9587a07556 | ||
|
|
c3bbcacaed |
No files matched your search
@@ -1,52 +1,85 @@
|
||||
# MindWork AI Studio
|
||||
<img src="app/MindWork%20AI%20Studio/wwwroot/svg/banner.svg" alt="MindWork AI Studio Banner"/>
|
||||
|
||||
Are you new here? [Read here](#what-is-ai-studio) what AI Studio is.
|
||||
|
||||
## News
|
||||
Things we are currently working on:
|
||||
<details>
|
||||
<summary>
|
||||
<h3 style="display:inline-block">
|
||||
Things we are currently working on
|
||||
</h3>
|
||||
</summary>
|
||||
|
||||
- Since November 2024: Work on RAG (integration of your data and files) has begun. We will support the integration of local and external data sources. We need to implement the following runtime (Rust) and app (.NET) steps:
|
||||
<details>
|
||||
<summary>
|
||||
<h4 style="display:inline-block">
|
||||
RAG (Retrieval-Augmented Generation)
|
||||
</h4>
|
||||
</summary>
|
||||
|
||||
- [x] ~~Runtime: Restructuring the code into meaningful modules (PR [#192](https://github.com/MindWorkAI/AI-Studio/pull/192))~~
|
||||
- [x] ~~Define the [External Retrieval Interface (ERI)](https://github.com/MindWorkAI/ERI) as a contract for integrating arbitrary external data (PR [#1](https://github.com/MindWorkAI/ERI/pull/1))~~
|
||||
- [x] ~~App: Metadata for providers (which provider offers embeddings?) (PR [#205](https://github.com/MindWorkAI/AI-Studio/pull/205))~~
|
||||
- [x] ~~App: Add an option to show preview features (PR [#222](https://github.com/MindWorkAI/AI-Studio/pull/222))~~
|
||||
- [x] ~~App: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~
|
||||
- [x] ~~App: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~
|
||||
- [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259), [#273](https://github.com/MindWorkAI/AI-Studio/pull/273))~~
|
||||
- [x] ~~Runtime: Extract data from txt / md / pdf / docx / xlsx files (PR [#374](https://github.com/MindWorkAI/AI-Studio/pull/374))~~
|
||||
- [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs)
|
||||
- [x] ~~App: Implement dialog for checking & handling [pandoc](https://pandoc.org/) installation ([PR #393](https://github.com/MindWorkAI/AI-Studio/pull/393), [PR #487](https://github.com/MindWorkAI/AI-Studio/pull/487))~~
|
||||
- [ ] App: Implement external embedding providers
|
||||
- [ ] App: Implement the process to vectorize one local file using embeddings
|
||||
- [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb)
|
||||
- [ ] 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), [#284](https://github.com/MindWorkAI/AI-Studio/pull/284), [#286](https://github.com/MindWorkAI/AI-Studio/pull/286), [#287](https://github.com/MindWorkAI/AI-Studio/pull/287))~~
|
||||
- [x] ~~App: Define a common augmentation interface for the integration of RAG processes in chats (PR [#288](https://github.com/MindWorkAI/AI-Studio/pull/288), [#289](https://github.com/MindWorkAI/AI-Studio/pull/289))~~
|
||||
- [x] ~~App: Integrate data sources in chats (PR [#282](https://github.com/MindWorkAI/AI-Studio/pull/282))~~
|
||||
Since November 2024: Work on RAG (integration of your data and files) has begun. We will support the integration of local and external data sources. We need to implement the following runtime (Rust) and app (.NET) steps:
|
||||
|
||||
- [x] ~~Runtime: Restructuring the code into meaningful modules (PR [#192](https://github.com/MindWorkAI/AI-Studio/pull/192))~~
|
||||
- [x] ~~Define the [External Retrieval Interface (ERI)](https://github.com/MindWorkAI/ERI) as a contract for integrating arbitrary external data (PR [#1](https://github.com/MindWorkAI/ERI/pull/1))~~
|
||||
- [x] ~~App: Metadata for providers (which provider offers embeddings?) (PR [#205](https://github.com/MindWorkAI/AI-Studio/pull/205))~~
|
||||
- [x] ~~App: Add an option to show preview features (PR [#222](https://github.com/MindWorkAI/AI-Studio/pull/222))~~
|
||||
- [x] ~~App: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~
|
||||
- [x] ~~App: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~
|
||||
- [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259), [#273](https://github.com/MindWorkAI/AI-Studio/pull/273))~~
|
||||
- [x] ~~Runtime: Extract data from txt / md / pdf / docx / xlsx files (PR [#374](https://github.com/MindWorkAI/AI-Studio/pull/374))~~
|
||||
- [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs)
|
||||
- [x] ~~App: Implement dialog for checking & handling [pandoc](https://pandoc.org/) installation ([PR #393](https://github.com/MindWorkAI/AI-Studio/pull/393), [PR #487](https://github.com/MindWorkAI/AI-Studio/pull/487))~~
|
||||
- [ ] App: Implement external embedding providers
|
||||
- [ ] App: Implement the process to vectorize one local file using embeddings
|
||||
- [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb)
|
||||
- [ ] 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), [#284](https://github.com/MindWorkAI/AI-Studio/pull/284), [#286](https://github.com/MindWorkAI/AI-Studio/pull/286), [#287](https://github.com/MindWorkAI/AI-Studio/pull/287))~~
|
||||
- [x] ~~App: Define a common augmentation interface for the integration of RAG processes in chats (PR [#288](https://github.com/MindWorkAI/AI-Studio/pull/288), [#289](https://github.com/MindWorkAI/AI-Studio/pull/289))~~
|
||||
- [x] ~~App: Integrate data sources in chats (PR [#282](https://github.com/MindWorkAI/AI-Studio/pull/282))~~
|
||||
|
||||
- Since September 2024: Experiments have been started on how we can work on long texts with AI Studio. Let's say you want to write a fantasy novel or create a complex project proposal and use LLM for support. The initial experiments were promising, but not yet satisfactory. We are testing further approaches until a satisfactory solution is found. The current state of our experiment is available as an experimental preview feature through your app configuration. Related PR: ~~[PR #167](https://github.com/MindWorkAI/AI-Studio/pull/167), [PR #226](https://github.com/MindWorkAI/AI-Studio/pull/226)~~, [PR #376](https://github.com/MindWorkAI/AI-Studio/pull/376).
|
||||
</details>
|
||||
|
||||
- Since March 2025: We have started developing the plugin system. There will be language plugins to offer AI Studio in other languages, configuration plugins to centrally manage certain providers and rules within an organization, and assistant plugins that allow anyone to develop their own assistants. We are using Lua as the plugin language:
|
||||
- [x] ~~Plan & implement the base plugin system ([PR #322](https://github.com/MindWorkAI/AI-Studio/pull/322))~~
|
||||
- [x] ~~Start the plugin system ([PR #372](https://github.com/MindWorkAI/AI-Studio/pull/372))~~
|
||||
- [x] ~~Added hot-reload support for plugins ([PR #377](https://github.com/MindWorkAI/AI-Studio/pull/377), [PR #391](https://github.com/MindWorkAI/AI-Studio/pull/391))~~
|
||||
- [x] ~~Add support for other languages (I18N) to AI Studio ([PR #381](https://github.com/MindWorkAI/AI-Studio/pull/381), [PR #400](https://github.com/MindWorkAI/AI-Studio/pull/400), [PR #404](https://github.com/MindWorkAI/AI-Studio/pull/404), [PR #429](https://github.com/MindWorkAI/AI-Studio/pull/429), [PR #446](https://github.com/MindWorkAI/AI-Studio/pull/446), [PR #451](https://github.com/MindWorkAI/AI-Studio/pull/451), [PR #455](https://github.com/MindWorkAI/AI-Studio/pull/455), [PR #458](https://github.com/MindWorkAI/AI-Studio/pull/458), [PR #462](https://github.com/MindWorkAI/AI-Studio/pull/462), [PR #469](https://github.com/MindWorkAI/AI-Studio/pull/469), [PR #486](https://github.com/MindWorkAI/AI-Studio/pull/486))~~
|
||||
- [x] ~~Add an I18N assistant to translate all AI Studio texts to a certain language & culture ([PR #422](https://github.com/MindWorkAI/AI-Studio/pull/422))~~
|
||||
- [x] ~~Provide MindWork AI Studio in German ([PR #430](https://github.com/MindWorkAI/AI-Studio/pull/430), [PR #446](https://github.com/MindWorkAI/AI-Studio/pull/446), [PR #451](https://github.com/MindWorkAI/AI-Studio/pull/451), [PR #455](https://github.com/MindWorkAI/AI-Studio/pull/455), [PR #458](https://github.com/MindWorkAI/AI-Studio/pull/458), [PR #462](https://github.com/MindWorkAI/AI-Studio/pull/462), [PR #469](https://github.com/MindWorkAI/AI-Studio/pull/469), [PR #486](https://github.com/MindWorkAI/AI-Studio/pull/486))~~
|
||||
- [x] ~~Add configuration plugins, which allow pre-defining some LLM providers in organizations ([PR #491](https://github.com/MindWorkAI/AI-Studio/pull/491), [PR #493](https://github.com/MindWorkAI/AI-Studio/pull/493), [PR #494](https://github.com/MindWorkAI/AI-Studio/pull/494), [PR #497](https://github.com/MindWorkAI/AI-Studio/pull/497))~~
|
||||
- [ ] Add an app store for plugins, showcasing community-contributed plugins from public GitHub and GitLab repositories. This will enable AI Studio users to discover, install, and update plugins directly within the platform.
|
||||
- [ ] Add assistant plugins
|
||||
<details>
|
||||
<summary>
|
||||
<h4 style="display:inline-block">
|
||||
Writer Mode
|
||||
</h4>
|
||||
</summary>
|
||||
|
||||
Other News:
|
||||
Since September 2024: Experiments have been started on how we can work on long texts with AI Studio. Let's say you want to write a fantasy novel or create a complex project proposal and use LLM for support. The initial experiments were promising, but not yet satisfactory. We are testing further approaches until a satisfactory solution is found. The current state of our experiment is available as an experimental preview feature through your app configuration. Related PR: ~~[PR #167](https://github.com/MindWorkAI/AI-Studio/pull/167), [PR #226](https://github.com/MindWorkAI/AI-Studio/pull/226)~~, [PR #376](https://github.com/MindWorkAI/AI-Studio/pull/376).
|
||||
|
||||
- April 2025: We have two active financial supporters: Peer `peerschuett` and Dominic `donework`. Thank you very much for your support. MindWork AI reinvests these donations by passing them on to our AI Studio dependencies ([see here](https://github.com/orgs/MindWorkAI/sponsoring)). In the event that we receive large donations, we will first sign the app ([#56](https://github.com/MindWorkAI/Planning/issues/56)). In case we receive more donations, we will look for and pay staff to develop features for AI Studio.
|
||||
</details>
|
||||
|
||||
- April 2025: The [German Aerospace Center (DLR)](https://en.wikipedia.org/wiki/German_Aerospace_Center) ([Website](https://www.dlr.de/en)) will use AI Studio at least within the scope of three projects and will also contribute to its further development. This is great news.
|
||||
<details>
|
||||
<summary>
|
||||
<h4 style="display:inline-block">
|
||||
Plugin System
|
||||
</h4>
|
||||
</summary>
|
||||
|
||||
Since March 2025: We have started developing the plugin system. There will be language plugins to offer AI Studio in other languages, configuration plugins to centrally manage certain providers and rules within an organization, and assistant plugins that allow anyone to develop their own assistants. We are using Lua as the plugin language:
|
||||
- [x] ~~Plan & implement the base plugin system ([PR #322](https://github.com/MindWorkAI/AI-Studio/pull/322))~~
|
||||
- [x] ~~Start the plugin system ([PR #372](https://github.com/MindWorkAI/AI-Studio/pull/372))~~
|
||||
- [x] ~~Added hot-reload support for plugins ([PR #377](https://github.com/MindWorkAI/AI-Studio/pull/377), [PR #391](https://github.com/MindWorkAI/AI-Studio/pull/391))~~
|
||||
- [x] ~~Add support for other languages (I18N) to AI Studio ([PR #381](https://github.com/MindWorkAI/AI-Studio/pull/381), [PR #400](https://github.com/MindWorkAI/AI-Studio/pull/400), [PR #404](https://github.com/MindWorkAI/AI-Studio/pull/404), [PR #429](https://github.com/MindWorkAI/AI-Studio/pull/429), [PR #446](https://github.com/MindWorkAI/AI-Studio/pull/446), [PR #451](https://github.com/MindWorkAI/AI-Studio/pull/451), [PR #455](https://github.com/MindWorkAI/AI-Studio/pull/455), [PR #458](https://github.com/MindWorkAI/AI-Studio/pull/458), [PR #462](https://github.com/MindWorkAI/AI-Studio/pull/462), [PR #469](https://github.com/MindWorkAI/AI-Studio/pull/469), [PR #486](https://github.com/MindWorkAI/AI-Studio/pull/486))~~
|
||||
- [x] ~~Add an I18N assistant to translate all AI Studio texts to a certain language & culture ([PR #422](https://github.com/MindWorkAI/AI-Studio/pull/422))~~
|
||||
- [x] ~~Provide MindWork AI Studio in German ([PR #430](https://github.com/MindWorkAI/AI-Studio/pull/430), [PR #446](https://github.com/MindWorkAI/AI-Studio/pull/446), [PR #451](https://github.com/MindWorkAI/AI-Studio/pull/451), [PR #455](https://github.com/MindWorkAI/AI-Studio/pull/455), [PR #458](https://github.com/MindWorkAI/AI-Studio/pull/458), [PR #462](https://github.com/MindWorkAI/AI-Studio/pull/462), [PR #469](https://github.com/MindWorkAI/AI-Studio/pull/469), [PR #486](https://github.com/MindWorkAI/AI-Studio/pull/486))~~
|
||||
- [x] ~~Add configuration plugins, which allow pre-defining some LLM providers in organizations ([PR #491](https://github.com/MindWorkAI/AI-Studio/pull/491), [PR #493](https://github.com/MindWorkAI/AI-Studio/pull/493), [PR #494](https://github.com/MindWorkAI/AI-Studio/pull/494), [PR #497](https://github.com/MindWorkAI/AI-Studio/pull/497))~~
|
||||
- [ ] Add an app store for plugins, showcasing community-contributed plugins from public GitHub and GitLab repositories. This will enable AI Studio users to discover, install, and update plugins directly within the platform.
|
||||
- [ ] Add assistant plugins
|
||||
|
||||
Features we have recently released:
|
||||
</details>
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary>
|
||||
<h3 style="display:inline-block">
|
||||
Features we have recently released
|
||||
</h3>
|
||||
</summary>
|
||||
|
||||
- v0.9.51: Added support for [Perplexity](https://www.perplexity.ai/); citations added so that LLMs can provide source references (e.g., some OpenAI models, Perplexity); added support for OpenAI's Responses API so that all text LLMs from OpenAI now work in MindWork AI Studio, including Deep Research models; web searches are now possible (some OpenAI models, Perplexity).
|
||||
- v0.9.50: Added support for self-hosted LLMs using [vLLM](https://blog.vllm.ai/2023/06/20/vllm.html).
|
||||
- v0.9.46: Released our plugin system, a German language plugin, early support for enterprise environments, and configuration plugins. Additionally, we added the Pandoc integration for future data processing and file generation.
|
||||
- v0.9.45: Added chat templates to AI Studio, allowing you to create and use a library of system prompts for your chats.
|
||||
@@ -58,6 +91,8 @@ Features we have recently released:
|
||||
- v0.9.26+: Added RAG for external data sources using our [ERI interface](https://mindworkai.org/#eri---external-retrieval-interface) as a preview feature.
|
||||
- v0.9.25: Added [xAI](https://x.ai/) as a new provider. xAI provides their Grok models for generating content.
|
||||
- v0.9.23: Added support for OpenAI `o` models (`o1`, `o1-mini`, `o3`, etc.); added also an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant as a preview feature behind the RAG feature flag. Your own ERI server can be used to gain access to, e.g., your enterprise data from within AI Studio.
|
||||
|
||||
</details>
|
||||
|
||||
## What is AI Studio?
|
||||
|
||||
@@ -72,6 +107,7 @@ MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provi
|
||||
- **Free of charge**: The app is free to use, both for personal and commercial purposes.
|
||||
- **Independence**: You are not tied to any single provider. Instead, you can choose the providers that best suit your needs. Right now, we support:
|
||||
- [OpenAI](https://openai.com/) (GPT5, GPT4.1, o1, o3, o4, etc.)
|
||||
- [Perplexity](https://www.perplexity.ai/)
|
||||
- [Mistral](https://mistral.ai/)
|
||||
- [Anthropic](https://www.anthropic.com/) (Claude)
|
||||
- [Google Gemini](https://gemini.google.com)
|
||||
@@ -92,7 +128,13 @@ MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provi
|
||||
|
||||
## **Ready to get started 🤩?** [Download the appropriate setup for your operating system here](documentation/Setup.md).
|
||||
|
||||
## Support the Project
|
||||
<details>
|
||||
<summary>
|
||||
<h2 style="display:inline-block">
|
||||
Support the Project
|
||||
</h2>
|
||||
</summary>
|
||||
|
||||
Thank you for using MindWork AI Studio and considering supporting its development 😀. Your support helps keep the project alive and ensures continuous improvements and new features.
|
||||
|
||||
We offer various ways you can support the project:
|
||||
@@ -106,7 +148,15 @@ For companies, sponsoring MindWork AI Studio is not only a way to support innova
|
||||
To view all available tiers, please visit our [GitHub Sponsors page](https://github.com/sponsors/MindWorkAI).
|
||||
Your support, whether big or small, keeps the wheels turning and is deeply appreciated ❤️.
|
||||
|
||||
## Planned Features
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<h2 style="display:inline-block">
|
||||
Planned Features
|
||||
</h2>
|
||||
</summary>
|
||||
|
||||
Here's an exciting look at some of the features we're planning to add to AI Studio in future releases:
|
||||
- **Integrating your data**: You'll be able to integrate your data into AI Studio, like your PDF or Office files, or your Markdown notes.
|
||||
- **Integration of enterprise data:** It will soon be possible to integrate data from the corporate network using a specified interface ([External Retrieval Interface](https://github.com/MindWorkAI/ERI), ERI for short). This will likely require development work by the organization in question.
|
||||
@@ -120,17 +170,45 @@ Here's an exciting look at some of the features we're planning to add to AI Stud
|
||||
|
||||
Stay tuned for more updates and enhancements to make MindWork AI Studio even more powerful and versatile 🤩.
|
||||
|
||||
## Building
|
||||
If you're interested in learning more about future plans, check out our [roadmap](https://github.com/orgs/MindWorkAI/projects/2/views/3) and our [planning issues](https://github.com/MindWorkAI/Planning/issues).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<h2 style="display:inline-block">
|
||||
Building
|
||||
</h2>
|
||||
</summary>
|
||||
|
||||
You want to know how to build MindWork AI Studio from source? [Check out the instructions here](documentation/Build.md).
|
||||
|
||||
## Enterprise IT
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<h2 style="display:inline-block">
|
||||
Enterprise IT
|
||||
</h2>
|
||||
</summary>
|
||||
|
||||
Do you want to manage AI Studio centrally from your IT department? Yes, that’s possible. [Here’s how it works.](documentation/Enterprise%20IT.md)
|
||||
|
||||
## License
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<h2 style="display:inline-block">
|
||||
License
|
||||
</h2>
|
||||
</summary>
|
||||
|
||||
MindWork AI Studio is licensed under the `FSL-1.1-MIT` license (functional source license). Here’s a simple rundown of what that means for you:
|
||||
- **Permitted Use**: Feel free to use, copy, modify, and share the software for your own projects, educational purposes, research, or even in professional services. The key is to use it in a way that doesn't compete with our offerings.
|
||||
- **Competing Use**: Our only request is that you don't create commercial products or services that replace or compete with MindWork AI Studio or any of our other offerings.
|
||||
- **No Warranties**: The software is provided "as is", without any promises from us about it working perfectly for your needs. While we strive to make it great, we can't guarantee it will be free of bugs or issues.
|
||||
- **Future License**: Good news! The license for each release of MindWork AI Studio will automatically convert to an MIT license two years from its release date. This makes it even easier for you to use the software in the future.
|
||||
|
||||
For more details, refer to the [LICENSE](LICENSE.md) file. This license structure ensures you have plenty of freedom to use and enjoy the software while protecting our work.
|
||||
For more details, refer to the [LICENSE](LICENSE.md) file. This license structure ensures you have plenty of freedom to use and enjoy the software while protecting our work.
|
||||
|
||||
</details>
|
||||
@@ -131,7 +131,7 @@ public sealed class AgentDataSourceSelection (ILogger<AgentDataSourceSelection>
|
||||
|
||||
#endregion
|
||||
|
||||
public async Task<List<SelectedDataSource>> PerformSelectionAsync(IProvider provider, IContent lastPrompt, ChatThread chatThread, AllowedSelectedDataSources dataSources, CancellationToken token = default)
|
||||
public async Task<List<SelectedDataSource>> PerformSelectionAsync(IProvider provider, IContent lastUserPrompt, ChatThread chatThread, AllowedSelectedDataSources dataSources, CancellationToken token = default)
|
||||
{
|
||||
logger.LogInformation("The AI should select the appropriate data sources.");
|
||||
|
||||
@@ -154,7 +154,7 @@ public sealed class AgentDataSourceSelection (ILogger<AgentDataSourceSelection>
|
||||
//
|
||||
// 2. Prepare the current system and user prompts as input for the agent:
|
||||
//
|
||||
var lastPromptContent = lastPrompt switch
|
||||
var lastPromptContent = lastUserPrompt switch
|
||||
{
|
||||
ContentText text => text.Text,
|
||||
|
||||
|
||||
@@ -147,12 +147,12 @@ public sealed class AgentRetrievalContextValidation (ILogger<AgentRetrievalConte
|
||||
/// <summary>
|
||||
/// Validate all retrieval contexts against the last user and the system prompt.
|
||||
/// </summary>
|
||||
/// <param name="lastPrompt">The last user prompt.</param>
|
||||
/// <param name="lastUserPrompt">The last user prompt.</param>
|
||||
/// <param name="chatThread">The chat thread.</param>
|
||||
/// <param name="retrievalContexts">All retrieval contexts to validate.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The validation results.</returns>
|
||||
public async Task<IReadOnlyList<RetrievalContextValidationResult>> ValidateRetrievalContextsAsync(IContent lastPrompt, ChatThread chatThread, IReadOnlyList<IRetrievalContext> retrievalContexts, CancellationToken token = default)
|
||||
public async Task<IReadOnlyList<RetrievalContextValidationResult>> ValidateRetrievalContextsAsync(IContent lastUserPrompt, ChatThread chatThread, IReadOnlyList<IRetrievalContext> retrievalContexts, CancellationToken token = default)
|
||||
{
|
||||
// Check if the retrieval context validation is enabled:
|
||||
if (!this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)
|
||||
@@ -178,7 +178,7 @@ public sealed class AgentRetrievalContextValidation (ILogger<AgentRetrievalConte
|
||||
await semaphore.WaitAsync(token);
|
||||
|
||||
// Start the next validation task:
|
||||
validationTasks.Add(this.ValidateRetrievalContextAsync(lastPrompt, chatThread, retrievalContext, token, semaphore));
|
||||
validationTasks.Add(this.ValidateRetrievalContextAsync(lastUserPrompt, chatThread, retrievalContext, token, semaphore));
|
||||
}
|
||||
|
||||
// Wait for all validation tasks to complete:
|
||||
@@ -193,13 +193,13 @@ public sealed class AgentRetrievalContextValidation (ILogger<AgentRetrievalConte
|
||||
/// can call this method in parallel for each retrieval context. You might use
|
||||
/// the ValidateRetrievalContextsAsync method to validate all retrieval contexts.
|
||||
/// </remarks>
|
||||
/// <param name="lastPrompt">The last user prompt.</param>
|
||||
/// <param name="lastUserPrompt">The last user prompt.</param>
|
||||
/// <param name="chatThread">The chat thread.</param>
|
||||
/// <param name="retrievalContext">The retrieval context to validate.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <param name="semaphore">The optional semaphore to limit the number of parallel validations.</param>
|
||||
/// <returns>The validation result.</returns>
|
||||
public async Task<RetrievalContextValidationResult> ValidateRetrievalContextAsync(IContent lastPrompt, ChatThread chatThread, IRetrievalContext retrievalContext, CancellationToken token = default, SemaphoreSlim? semaphore = null)
|
||||
public async Task<RetrievalContextValidationResult> ValidateRetrievalContextAsync(IContent lastUserPrompt, ChatThread chatThread, IRetrievalContext retrievalContext, CancellationToken token = default, SemaphoreSlim? semaphore = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -214,7 +214,7 @@ public sealed class AgentRetrievalContextValidation (ILogger<AgentRetrievalConte
|
||||
//
|
||||
// 1. Prepare the current system and user prompts as input for the agent:
|
||||
//
|
||||
var lastPromptContent = lastPrompt switch
|
||||
var lastPromptContent = lastUserPrompt switch
|
||||
{
|
||||
ContentText text => text.Text,
|
||||
|
||||
|
||||
@@ -4882,9 +4882,6 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T3424652889"] = "Un
|
||||
-- no model selected
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "no model selected"
|
||||
|
||||
-- Sources
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SOURCEEXTENSIONS::T2730980305"] = "Sources"
|
||||
|
||||
-- Use no chat template
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Use no chat template"
|
||||
|
||||
@@ -5641,6 +5638,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T1015418291"] = "No u
|
||||
-- Failed to install update automatically. Please try again manually.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T3709709946"] = "Failed to install update automatically. Please try again manually."
|
||||
|
||||
-- Sources provided by the data providers
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4174900468"] = "Sources provided by the data providers"
|
||||
|
||||
-- Sources provided by the AI
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4261248356"] = "Sources provided by the AI"
|
||||
|
||||
-- The hostname is not a valid HTTP(S) URL.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T1013354736"] = "The hostname is not a valid HTTP(S) URL."
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@using AIStudio.Tools
|
||||
@using MudBlazor
|
||||
@using AIStudio.Components
|
||||
@using AIStudio.Provider
|
||||
@inherits AIStudio.Components.MSGComponentBase
|
||||
<MudCard Class="@this.CardClasses" Outlined="@true">
|
||||
<MudCardHeader>
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed class ContentImage : IContent, IImageSource
|
||||
public List<Source> Sources { get; set; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatChatThread, CancellationToken token = default)
|
||||
public Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastUserPrompt, ChatThread? chatChatThread, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace AIStudio.Chat;
|
||||
/// </summary>
|
||||
public sealed class ContentText : IContent
|
||||
{
|
||||
private static readonly ILogger<ContentText> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ContentText>();
|
||||
|
||||
/// <summary>
|
||||
/// The minimum time between two streaming events, when the user
|
||||
/// enables the energy saving mode.
|
||||
@@ -39,30 +41,28 @@ public sealed class ContentText : IContent
|
||||
public List<Source> Sources { get; set; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatThread, CancellationToken token = default)
|
||||
public async Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastUserPrompt, ChatThread? chatThread, CancellationToken token = default)
|
||||
{
|
||||
if(chatThread is null)
|
||||
return new();
|
||||
|
||||
if(!chatThread.IsLLMProviderAllowed(provider))
|
||||
{
|
||||
var logger = Program.SERVICE_PROVIDER.GetService<ILogger<ContentText>>()!;
|
||||
logger.LogError("The provider is not allowed for this chat thread due to data security reasons. Skipping the AI process.");
|
||||
LOGGER.LogError("The provider is not allowed for this chat thread due to data security reasons. Skipping the AI process.");
|
||||
return chatThread;
|
||||
}
|
||||
|
||||
// Call the RAG process. Right now, we only have one RAG process:
|
||||
if (lastPrompt is not null)
|
||||
if (lastUserPrompt is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var rag = new AISrcSelWithRetCtxVal();
|
||||
chatThread = await rag.ProcessAsync(provider, lastPrompt, chatThread, token);
|
||||
chatThread = await rag.ProcessAsync(provider, lastUserPrompt, chatThread, token);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var logger = Program.SERVICE_PROVIDER.GetService<ILogger<ContentText>>()!;
|
||||
logger.LogError(e, "Skipping the RAG process due to an error.");
|
||||
LOGGER.LogError(e, "Skipping the RAG process due to an error.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,13 +41,17 @@ public interface IContent
|
||||
/// <summary>
|
||||
/// The provided sources, if any.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We cannot use ISource here because System.Text.Json does not support
|
||||
/// interface serialization. So we have to use a concrete class.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public List<Source> Sources { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Uses the provider to create the content.
|
||||
/// </summary>
|
||||
public Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatChatThread, CancellationToken token = default);
|
||||
public Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastUserPrompt, ChatThread? chatChatThread, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a deep copy
|
||||
|
||||
@@ -13,6 +13,7 @@ public partial class Changelog
|
||||
|
||||
public static readonly Log[] LOGS =
|
||||
[
|
||||
new (227, "v0.9.52, build 227 (2025-10-24 06:00 UTC)", "v0.9.52.md"),
|
||||
new (226, "v0.9.51, build 226 (2025-09-04 18:02 UTC)", "v0.9.51.md"),
|
||||
new (225, "v0.9.50, build 225 (2025-08-10 16:40 UTC)", "v0.9.50.md"),
|
||||
new (224, "v0.9.49, build 224 (2025-07-02 12:12 UTC)", "v0.9.49.md"),
|
||||
|
||||
@@ -33,8 +33,10 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
|
||||
/// <inheritdoc />
|
||||
protected override bool Stretch => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Variant Variant => Variant.Outlined;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string Label => this.OptionDescription;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -36,6 +36,7 @@ public partial class ConfigurationSelect<TConfig> : ConfigurationBaseCore
|
||||
/// <inheritdoc />
|
||||
protected override string Label => this.OptionDescription;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Variant Variant => Variant.Outlined;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -16,14 +16,14 @@
|
||||
<ConfigurationSelect OptionDescription="@T("Check for updates")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateInterval)" Data="@ConfigurationSelectDataFactory.GetUpdateIntervalData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInterval = selectedValue)" OptionHelp="@T("How often should we check for app updates?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInterval, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Update installation method")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateInstallation)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviourData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInstallation = selectedValue)" OptionHelp="@T("Should updates be installed automatically or manually?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInstallation, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Navigation bar behavior")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.NavigationBehavior)" Data="@ConfigurationSelectDataFactory.GetNavBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.NavigationBehavior = selectedValue)" OptionHelp="@T("Select the desired behavior for the navigation bar.")"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preview feature visibility")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreviewVisibility)" Data="@ConfigurationSelectDataFactory.GetPreviewVisibility()" SelectionUpdate="@this.UpdatePreviewFeatures" OptionHelp="@T("Do you want to show preview features in the app?")"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preview feature visibility")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreviewVisibility)" Data="@ConfigurationSelectDataFactory.GetPreviewVisibility()" SelectionUpdate="@this.UpdatePreviewFeatures" OptionHelp="@T("Do you want to show preview features in the app?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.PreviewVisibility, out var meta) && meta.IsLocked"/>
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.App.PreviewVisibility > PreviewVisibility.NONE)
|
||||
{
|
||||
var availablePreviewFeatures = ConfigurationSelectDataFactory.GetPreviewFeaturesData(this.SettingsManager).ToList();
|
||||
if (availablePreviewFeatures.Count > 0)
|
||||
{
|
||||
<ConfigurationMultiSelect OptionDescription="@T("Select preview features")" SelectedValues="@(() => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures.Where(x => !x.IsReleased()).ToHashSet())" Data="@availablePreviewFeatures" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedValue)" OptionHelp="@T("Which preview features would you like to enable?")"/>
|
||||
<ConfigurationMultiSelect OptionDescription="@T("Select preview features")" SelectedValues="@(() => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures.Where(x => !x.IsReleased()).ToHashSet())" Data="@availablePreviewFeatures" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedValue)" OptionHelp="@T("Which preview features would you like to enable?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.IsLocked"/>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="8.2.4" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.10" />
|
||||
<PackageReference Include="MudBlazor" Version="8.12.0" />
|
||||
<PackageReference Include="MudBlazor.Markdown" Version="8.11.0" />
|
||||
<PackageReference Include="ReverseMarkdown" Version="4.7.0" />
|
||||
|
||||
@@ -74,6 +74,18 @@ CONFIG["SETTINGS"] = {}
|
||||
-- Allowed values are: true, false
|
||||
-- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false
|
||||
|
||||
-- Configure the visibility of preview features:
|
||||
-- Allowed values are: NONE, RELEASE_CANDIDATE, BETA, ALPHA, PROTOTYPE, EXPERIMENTAL
|
||||
-- Please note:
|
||||
-- I: that this setting does not hide features that are already enabled.
|
||||
-- II: lower levels include all features of the higher levels. E.g. BETA includes RELEASE_CANDIDATE features.
|
||||
-- CONFIG["SETTINGS"]["DataApp.PreviewVisibility"] = "NONE"
|
||||
|
||||
-- Configure the enabled preview features:
|
||||
-- Allowed values are can be found in https://github.com/MindWorkAI/AI-Studio/app/MindWork%20AI%20Studio/Settings/DataModel/PreviewFeatures.cs
|
||||
-- Examples are PRE_WRITER_MODE_2024, PRE_RAG_2024, PRE_DOCUMENT_ANALYSIS_2025.
|
||||
-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024", "PRE_DOCUMENT_ANALYSIS_2025" }
|
||||
|
||||
-- Example chat templates for this configuration:
|
||||
CONFIG["CHAT_TEMPLATES"] = {}
|
||||
|
||||
|
||||
+6
-3
@@ -4884,9 +4884,6 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T3424652889"] = "Un
|
||||
-- no model selected
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "Kein Modell ausgewählt"
|
||||
|
||||
-- Sources
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SOURCEEXTENSIONS::T2730980305"] = "Quellen"
|
||||
|
||||
-- Use no chat template
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Keine Chat-Vorlage verwenden"
|
||||
|
||||
@@ -5643,6 +5640,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T1015418291"] = "Kein
|
||||
-- Failed to install update automatically. Please try again manually.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T3709709946"] = "Fehler bei der automatischen Installation des Updates. Bitte versuchen Sie es manuell erneut."
|
||||
|
||||
-- Sources provided by the data providers
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4174900468"] = "Von den Datenanbietern bereitgestellte Quellen"
|
||||
|
||||
-- Sources provided by the AI
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4261248356"] = "Von der KI bereitgestellte Quellen"
|
||||
|
||||
-- The hostname is not a valid HTTP(S) URL.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T1013354736"] = "Der Hostname ist keine gültige HTTP(S)-URL."
|
||||
|
||||
|
||||
+6
-3
@@ -4884,9 +4884,6 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T3424652889"] = "Un
|
||||
-- no model selected
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "no model selected"
|
||||
|
||||
-- Sources
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SOURCEEXTENSIONS::T2730980305"] = "Sources"
|
||||
|
||||
-- Use no chat template
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Use no chat template"
|
||||
|
||||
@@ -5643,6 +5640,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T1015418291"] = "No u
|
||||
-- Failed to install update automatically. Please try again manually.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T3709709946"] = "Failed to install update automatically. Please try again manually."
|
||||
|
||||
-- Sources provided by the data providers
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4174900468"] = "Sources provided by the data providers"
|
||||
|
||||
-- Sources provided by the AI
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4261248356"] = "Sources provided by the AI"
|
||||
|
||||
-- The hostname is not a valid HTTP(S) URL.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T1013354736"] = "The hostname is not a valid HTTP(S) URL."
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ public record ChatCompletionAnnotationStreamLine(string Id, string Object, uint
|
||||
{
|
||||
// Check if the annotation is of the expected type and extract the source information:
|
||||
if (annotation is ChatCompletionAnnotatingURL urlAnnotation)
|
||||
sources.Add(new Source(urlAnnotation.UrlCitation.Title, urlAnnotation.UrlCitation.URL));
|
||||
sources.Add(new Source(urlAnnotation.UrlCitation.Title, urlAnnotation.UrlCitation.URL, SourceOrigin.LLM));
|
||||
|
||||
//
|
||||
// Check for the unexpected annotation type of the Responses API.
|
||||
@@ -46,7 +46,7 @@ public record ChatCompletionAnnotationStreamLine(string Id, string Object, uint
|
||||
// we are calling the chat completion endpoint.
|
||||
//
|
||||
if (annotation is ResponsesAnnotatingUrlCitationData citationData)
|
||||
sources.Add(new Source(citationData.Title, citationData.URL));
|
||||
sources.Add(new Source(citationData.Title, citationData.URL, SourceOrigin.LLM));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,11 +32,11 @@ public sealed record ResponsesAnnotationStreamLine(string Type, int AnnotationIn
|
||||
// into that type, even though we are calling the Responses API endpoint.
|
||||
//
|
||||
if (this.Annotation is ChatCompletionAnnotatingURL urlAnnotation)
|
||||
return [new Source(urlAnnotation.UrlCitation.Title, urlAnnotation.UrlCitation.URL)];
|
||||
return [new Source(urlAnnotation.UrlCitation.Title, urlAnnotation.UrlCitation.URL, SourceOrigin.LLM)];
|
||||
|
||||
// Check for the expected annotation type of the Responses API:
|
||||
if (this.Annotation is ResponsesAnnotatingUrlCitationData urlCitationData)
|
||||
return [new Source(urlCitationData.Title, urlCitationData.URL)];
|
||||
return [new Source(urlCitationData.Title, urlCitationData.URL, SourceOrigin.LLM)];
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -5,4 +5,4 @@ namespace AIStudio.Provider.Perplexity;
|
||||
/// </summary>
|
||||
/// <param name="Title">The title of the search result.</param>
|
||||
/// <param name="URL">The URL of the search result.</param>
|
||||
public sealed record SearchResult(string Title, string URL) : Source(Title, URL);
|
||||
public sealed record SearchResult(string Title, string URL) : Source(Title, URL, SourceOrigin.LLM);
|
||||
@@ -60,7 +60,7 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
|
||||
/// <summary>
|
||||
/// The enabled preview features.
|
||||
/// </summary>
|
||||
public HashSet<PreviewFeatures> EnabledPreviewFeatures { get; set; } = new();
|
||||
public HashSet<PreviewFeatures> EnabledPreviewFeatures { get; set; } = ManagedConfiguration.Register(configSelection, n => n.EnabledPreviewFeatures, []);
|
||||
|
||||
/// <summary>
|
||||
/// Should we preselect a provider for the entire app?
|
||||
|
||||
@@ -58,7 +58,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
public ushort MaxMatches { get; init; } = 10;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<IRetrievalContext>> RetrieveDataAsync(IContent lastPrompt, ChatThread thread, CancellationToken token = default)
|
||||
public async Task<IReadOnlyList<IRetrievalContext>> RetrieveDataAsync(IContent lastUserPrompt, ChatThread thread, CancellationToken token = default)
|
||||
{
|
||||
// Important: Do not dispose the RustService here, as it is a singleton.
|
||||
var rustService = Program.SERVICE_PROVIDER.GetRequiredService<RustService>();
|
||||
@@ -70,8 +70,8 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
{
|
||||
var retrievalRequest = new RetrievalRequest
|
||||
{
|
||||
LatestUserPromptType = lastPrompt.ToERIContentType,
|
||||
LatestUserPrompt = lastPrompt switch
|
||||
LatestUserPromptType = lastUserPrompt.ToERIContentType,
|
||||
LatestUserPrompt = lastUserPrompt switch
|
||||
{
|
||||
ContentText text => text.Text,
|
||||
ContentImage image => await image.AsBase64(token),
|
||||
@@ -103,7 +103,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
Links = eriContext.Links,
|
||||
Category = eriContext.Type.ToRetrievalContentCategory(),
|
||||
MatchedText = eriContext.MatchedContent,
|
||||
DataSourceName = this.Name,
|
||||
DataSourceName = eriContext.Name,
|
||||
SurroundingContent = eriContext.SurroundingContent,
|
||||
});
|
||||
break;
|
||||
@@ -117,7 +117,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
Source = eriContext.MatchedContent,
|
||||
Category = eriContext.Type.ToRetrievalContentCategory(),
|
||||
SourceType = ContentImageSource.BASE64,
|
||||
DataSourceName = this.Name,
|
||||
DataSourceName = eriContext.Name,
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ public readonly record struct DataSourceLocalDirectory : IInternalDataSource
|
||||
public ushort MaxMatches { get; init; } = 10;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<IRetrievalContext>> RetrieveDataAsync(IContent lastPrompt, ChatThread thread, CancellationToken token = default)
|
||||
public Task<IReadOnlyList<IRetrievalContext>> RetrieveDataAsync(IContent lastUserPrompt, ChatThread thread, CancellationToken token = default)
|
||||
{
|
||||
IReadOnlyList<IRetrievalContext> retrievalContext = new List<IRetrievalContext>();
|
||||
return Task.FromResult(retrievalContext);
|
||||
|
||||
@@ -34,7 +34,7 @@ public readonly record struct DataSourceLocalFile : IInternalDataSource
|
||||
public ushort MaxMatches { get; init; } = 10;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<IRetrievalContext>> RetrieveDataAsync(IContent lastPrompt, ChatThread thread, CancellationToken token = default)
|
||||
public Task<IReadOnlyList<IRetrievalContext>> RetrieveDataAsync(IContent lastUserPrompt, ChatThread thread, CancellationToken token = default)
|
||||
{
|
||||
IReadOnlyList<IRetrievalContext> retrievalContext = new List<IRetrievalContext>();
|
||||
return Task.FromResult(retrievalContext);
|
||||
|
||||
@@ -48,9 +48,9 @@ public interface IDataSource
|
||||
/// <summary>
|
||||
/// Perform the data retrieval process.
|
||||
/// </summary>
|
||||
/// <param name="lastPrompt">The last prompt from the chat.</param>
|
||||
/// <param name="lastUserPrompt">The last user prompt from the chat.</param>
|
||||
/// <param name="thread">The chat thread.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The retrieved data context.</returns>
|
||||
public Task<IReadOnlyList<IRetrievalContext>> RetrieveDataAsync(IContent lastPrompt, ChatThread thread, CancellationToken token = default);
|
||||
public Task<IReadOnlyList<IRetrievalContext>> RetrieveDataAsync(IContent lastUserPrompt, ChatThread thread, CancellationToken token = default);
|
||||
}
|
||||
@@ -0,0 +1,736 @@
|
||||
using System.Globalization;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
using Lua;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
public static partial class ManagedConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to process the configuration settings from a Lua table for enum types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the configuration is successfully processed, it updates the configuration metadata with the configured value.
|
||||
/// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID.
|
||||
/// The setting's value is set to the configured value.
|
||||
/// </remarks>
|
||||
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
|
||||
/// <param name="settings">The Lua table containing the settings to process.</param>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="dryRun">When true, the method will not apply any changes, but only check if the configuration can be read.</param>
|
||||
/// <param name="_">An unused parameter to help with type inference for enum types. You might ignore it when calling the method.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
||||
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
|
||||
public static bool TryProcessConfiguration<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, TValue>> propertyExpression,
|
||||
Guid configPluginId,
|
||||
LuaTable settings,
|
||||
bool dryRun,
|
||||
TValue? _ = default)
|
||||
where TValue : Enum
|
||||
{
|
||||
//
|
||||
// Handle configured enum values
|
||||
//
|
||||
|
||||
// Check if that configuration was registered:
|
||||
if(!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
var successful = false;
|
||||
var configuredValue = configMeta.Default;
|
||||
|
||||
// Step 1 -- try to read the Lua value out of the Lua table:
|
||||
if(settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredEnumValue))
|
||||
{
|
||||
// Step 2 -- try to read the Lua value as a string:
|
||||
if(configuredEnumValue.TryRead<string>(out var configuredEnumText))
|
||||
{
|
||||
// Step 3 -- try to parse the string as the enum type:
|
||||
if (Enum.TryParse(typeof(TValue), configuredEnumText, true, out var configuredEnum))
|
||||
{
|
||||
configuredValue = (TValue)configuredEnum;
|
||||
successful = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(dryRun)
|
||||
return successful;
|
||||
|
||||
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to process the configuration settings from a Lua table for ISpanParsable types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the configuration is successfully processed, it updates the configuration metadata with the configured value.
|
||||
/// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID.
|
||||
/// The setting's value is set to the configured value.
|
||||
/// </remarks>
|
||||
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
|
||||
/// <param name="settings">The Lua table containing the settings to process.</param>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="dryRun">When true, the method will not apply any changes, but only check if the configuration can be read.</param>
|
||||
/// <param name="_">An unused parameter to help with type inference. You might ignore it when calling the method.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
||||
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
|
||||
public static bool TryProcessConfiguration<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, TValue>> propertyExpression,
|
||||
Guid configPluginId,
|
||||
LuaTable settings,
|
||||
bool dryRun,
|
||||
ISpanParsable<TValue>? _ = null)
|
||||
where TValue : struct, ISpanParsable<TValue>
|
||||
{
|
||||
//
|
||||
// Handle configured ISpanParsable values
|
||||
//
|
||||
|
||||
// Check if that configuration was registered:
|
||||
if(!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
var successful = false;
|
||||
var configuredValue = configMeta.Default;
|
||||
|
||||
// Step 1 -- try to read the Lua value out of the Lua table:
|
||||
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaValue))
|
||||
{
|
||||
// Step 2a -- try to read the Lua value as a string:
|
||||
if (configuredLuaValue.Type is LuaValueType.String && configuredLuaValue.TryRead<string>(out var configuredLuaValueText))
|
||||
{
|
||||
// Step 3 -- try to parse the string as the target type:
|
||||
if (TValue.TryParse(configuredLuaValueText, CultureInfo.InvariantCulture, out var configuredParsedValue))
|
||||
{
|
||||
configuredValue = configuredParsedValue;
|
||||
successful = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2b -- try to read the Lua value:
|
||||
if(configuredLuaValue.TryRead<TValue>(out var configuredLuaValueInstance))
|
||||
{
|
||||
configuredValue = configuredLuaValueInstance;
|
||||
successful = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(dryRun)
|
||||
return successful;
|
||||
|
||||
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to process the configuration settings from a Lua table for string values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the configuration is successfully processed, it updates the configuration metadata with the configured value.
|
||||
/// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID.
|
||||
/// The setting's value is set to the configured value.
|
||||
/// </remarks>
|
||||
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
|
||||
/// <param name="settings">The Lua table containing the settings to process.</param>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="dryRun">When true, the method will not apply any changes, but only check if the configuration can be read.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
|
||||
public static bool TryProcessConfiguration<TClass>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, string>> propertyExpression,
|
||||
Guid configPluginId,
|
||||
LuaTable settings,
|
||||
bool dryRun)
|
||||
{
|
||||
//
|
||||
// Handle configured string values
|
||||
//
|
||||
|
||||
// Check if that configuration was registered:
|
||||
if(!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
var successful = false;
|
||||
var configuredValue = configMeta.Default;
|
||||
|
||||
// Step 1 -- try to read the Lua value out of the Lua table:
|
||||
if(settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredTextValue))
|
||||
{
|
||||
// Step 2 -- try to read the Lua value as a string:
|
||||
if(configuredTextValue.TryRead<string>(out var configuredText))
|
||||
{
|
||||
configuredValue = configuredText;
|
||||
successful = true;
|
||||
}
|
||||
}
|
||||
|
||||
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to process the configuration settings from a Lua table for ISpanParsable list types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the configuration is successfully processed, it updates the configuration metadata with the configured value.
|
||||
/// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID.
|
||||
/// The setting's value is set to the configured value.
|
||||
/// </remarks>
|
||||
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
|
||||
/// <param name="settings">The Lua table containing the settings to process.</param>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="dryRun">When true, the method will not apply any changes but only check if the configuration can be read.</param>
|
||||
/// <param name="_">An unused parameter to help with type inference for ISpanParsable types. You might ignore it when calling the method.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the property within the configuration class. It is also the type of the list
|
||||
/// elements, which must implement ISpanParsable.</typeparam>
|
||||
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
|
||||
|
||||
// ReSharper disable MethodOverloadWithOptionalParameter
|
||||
public static bool TryProcessConfiguration<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, IList<TValue>>> propertyExpression,
|
||||
Guid configPluginId,
|
||||
LuaTable settings,
|
||||
bool dryRun,
|
||||
ISpanParsable<TValue>? _ = null)
|
||||
where TValue : ISpanParsable<TValue>
|
||||
{
|
||||
//
|
||||
// Handle configured ISpanParsable lists
|
||||
//
|
||||
|
||||
// Check if that configuration was registered:
|
||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
var successful = false;
|
||||
var configuredValue = configMeta.Default;
|
||||
|
||||
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
|
||||
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
|
||||
configuredLuaList.Type is LuaValueType.Table &&
|
||||
configuredLuaList.TryRead<LuaTable>(out var valueTable))
|
||||
{
|
||||
// Determine the length of the Lua table and prepare a list to hold the parsed values:
|
||||
var len = valueTable.ArrayLength;
|
||||
var list = new List<TValue>(len);
|
||||
|
||||
// Iterate over each entry in the Lua table:
|
||||
for (var index = 1; index <= len; index++)
|
||||
{
|
||||
// Retrieve the Lua value at the current index:
|
||||
var value = valueTable[index];
|
||||
|
||||
// Step 2a -- try to read the Lua value as a string:
|
||||
if (value.Type is LuaValueType.String && value.TryRead<string>(out var configuredLuaValueText))
|
||||
{
|
||||
// Step 3 -- try to parse the string as the target type:
|
||||
if (TValue.TryParse(configuredLuaValueText, CultureInfo.InvariantCulture, out var configuredParsedValue))
|
||||
list.Add(configuredParsedValue);
|
||||
}
|
||||
|
||||
// Step 2b -- try to read the Lua value:
|
||||
if (value.TryRead<TValue>(out var configuredLuaValueInstance))
|
||||
list.Add(configuredLuaValueInstance);
|
||||
}
|
||||
|
||||
configuredValue = list;
|
||||
successful = true;
|
||||
}
|
||||
|
||||
if (dryRun)
|
||||
return successful;
|
||||
|
||||
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
|
||||
}
|
||||
|
||||
// ReSharper restore MethodOverloadWithOptionalParameter
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to process the configuration settings from a Lua table for enum list types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the configuration is successfully processed, it updates the configuration metadata with the configured value.
|
||||
/// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID.
|
||||
/// The setting's value is set to the configured value.
|
||||
/// </remarks>
|
||||
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
|
||||
/// <param name="settings">The Lua table containing the settings to process.</param>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="dryRun">When true, the method will not apply any changes but only check if the configuration can be read.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the property within the configuration class. It is also the type of the list
|
||||
/// elements, which must be an enum.</typeparam>
|
||||
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
|
||||
public static bool TryProcessConfiguration<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, IList<TValue>>> propertyExpression,
|
||||
Guid configPluginId,
|
||||
LuaTable settings,
|
||||
bool dryRun)
|
||||
where TValue : Enum
|
||||
{
|
||||
//
|
||||
// Handle configured enum lists
|
||||
//
|
||||
|
||||
// Check if that configuration was registered:
|
||||
if(!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
var successful = false;
|
||||
var configuredValue = configMeta.Default;
|
||||
|
||||
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
|
||||
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
|
||||
configuredLuaList.Type is LuaValueType.Table &&
|
||||
configuredLuaList.TryRead<LuaTable>(out var valueTable))
|
||||
{
|
||||
// Determine the length of the Lua table and prepare a list to hold the parsed values:
|
||||
var len = valueTable.ArrayLength;
|
||||
var list = new List<TValue>(len);
|
||||
|
||||
// Iterate over each entry in the Lua table:
|
||||
for (var index = 1; index <= len; index++)
|
||||
{
|
||||
// Retrieve the Lua value at the current index:
|
||||
var value = valueTable[index];
|
||||
|
||||
// Step 2 -- try to read the Lua value as a string:
|
||||
if (value.Type is LuaValueType.String && value.TryRead<string>(out var configuredLuaValueText))
|
||||
{
|
||||
// Step 3 -- try to parse the string as the target type:
|
||||
if (Enum.TryParse(typeof(TValue), configuredLuaValueText, true, out var configuredEnum))
|
||||
list.Add((TValue)configuredEnum);
|
||||
}
|
||||
}
|
||||
|
||||
configuredValue = list;
|
||||
successful = true;
|
||||
}
|
||||
|
||||
if(dryRun)
|
||||
return successful;
|
||||
|
||||
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to process the configuration settings from a Lua table for string list types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the configuration is successfully processed, it updates the configuration metadata with the configured value.
|
||||
/// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID.
|
||||
/// The setting's value is set to the configured value.
|
||||
/// </remarks>
|
||||
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
|
||||
/// <param name="settings">The Lua table containing the settings to process.</param>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="dryRun">When true, the method will not apply any changes but only check if the configuration can be read.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
|
||||
public static bool TryProcessConfiguration<TClass>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, IList<string>>> propertyExpression,
|
||||
Guid configPluginId,
|
||||
LuaTable settings,
|
||||
bool dryRun)
|
||||
{
|
||||
//
|
||||
// Handle configured string lists
|
||||
//
|
||||
|
||||
// Check if that configuration was registered:
|
||||
if(!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
var successful = false;
|
||||
var configuredValue = configMeta.Default;
|
||||
|
||||
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
|
||||
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
|
||||
configuredLuaList.Type is LuaValueType.Table &&
|
||||
configuredLuaList.TryRead<LuaTable>(out var valueTable))
|
||||
{
|
||||
// Determine the length of the Lua table and prepare a list to hold the parsed values:
|
||||
var len = valueTable.ArrayLength;
|
||||
var list = new List<string>(len);
|
||||
|
||||
// Iterate over each entry in the Lua table:
|
||||
for (var index = 1; index <= len; index++)
|
||||
{
|
||||
// Retrieve the Lua value at the current index:
|
||||
var value = valueTable[index];
|
||||
|
||||
// Step 2 -- try to read the Lua value as a string:
|
||||
if (value.Type is LuaValueType.String && value.TryRead<string>(out var configuredLuaValueText))
|
||||
list.Add(configuredLuaValueText);
|
||||
}
|
||||
|
||||
configuredValue = list;
|
||||
successful = true;
|
||||
}
|
||||
|
||||
if(dryRun)
|
||||
return successful;
|
||||
|
||||
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to process the configuration settings from a Lua table for ISpanParsable set types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the configuration is successfully processed, it updates the configuration metadata with the configured value.
|
||||
/// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID.
|
||||
/// The setting's value is set to the configured value.
|
||||
/// </remarks>
|
||||
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
|
||||
/// <param name="settings">The Lua table containing the settings to process.</param>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="dryRun">When true, the method will not apply any changes but only check if the configuration can be read.</param>
|
||||
/// <param name="_">An unused parameter to help with type inference for ISpanParsable types. You might ignore it when calling the method.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the property within the configuration class. It is also the type of the set
|
||||
/// elements, which must implement ISpanParsable.</typeparam>
|
||||
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
|
||||
|
||||
// ReSharper disable MethodOverloadWithOptionalParameter
|
||||
public static bool TryProcessConfiguration<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
|
||||
Guid configPluginId,
|
||||
LuaTable settings,
|
||||
bool dryRun,
|
||||
ISpanParsable<TValue>? _ = null)
|
||||
where TValue : ISpanParsable<TValue>
|
||||
{
|
||||
//
|
||||
// Handle configured ISpanParsable sets
|
||||
//
|
||||
|
||||
// Check if that configuration was registered:
|
||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
var successful = false;
|
||||
var configuredValue = configMeta.Default;
|
||||
|
||||
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
|
||||
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
|
||||
configuredLuaList.Type is LuaValueType.Table &&
|
||||
configuredLuaList.TryRead<LuaTable>(out var valueTable))
|
||||
{
|
||||
// Determine the length of the Lua table and prepare a set to hold the parsed values:
|
||||
var len = valueTable.ArrayLength;
|
||||
var set = new HashSet<TValue>(len);
|
||||
|
||||
// Iterate over each entry in the Lua table:
|
||||
for (var index = 1; index <= len; index++)
|
||||
{
|
||||
// Retrieve the Lua value at the current index:
|
||||
var value = valueTable[index];
|
||||
|
||||
// Step 2a -- try to read the Lua value as a string:
|
||||
if (value.Type is LuaValueType.String && value.TryRead<string>(out var configuredLuaValueText))
|
||||
{
|
||||
// Step 3 -- try to parse the string as the target type:
|
||||
if (TValue.TryParse(configuredLuaValueText, CultureInfo.InvariantCulture, out var configuredParsedValue))
|
||||
set.Add(configuredParsedValue);
|
||||
}
|
||||
|
||||
// Step 2b -- try to read the Lua value:
|
||||
if (value.TryRead<TValue>(out var configuredLuaValueInstance))
|
||||
set.Add(configuredLuaValueInstance);
|
||||
}
|
||||
|
||||
configuredValue = set;
|
||||
successful = true;
|
||||
}
|
||||
|
||||
if (dryRun)
|
||||
return successful;
|
||||
|
||||
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
|
||||
}
|
||||
|
||||
// ReSharper restore MethodOverloadWithOptionalParameter
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to process the configuration settings from a Lua table for enum set types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the configuration is successfully processed, it updates the configuration metadata with the configured value.
|
||||
/// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID.
|
||||
/// The setting's value is set to the configured value.
|
||||
/// </remarks>
|
||||
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
|
||||
/// <param name="settings">The Lua table containing the settings to process.</param>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="dryRun">When true, the method will not apply any changes but only check if the configuration can be read.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the property within the configuration class. It is also the type of the set
|
||||
/// elements, which must be an enum.</typeparam>
|
||||
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
|
||||
public static bool TryProcessConfiguration<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
|
||||
Guid configPluginId,
|
||||
LuaTable settings,
|
||||
bool dryRun)
|
||||
where TValue : Enum
|
||||
{
|
||||
//
|
||||
// Handle configured enum sets
|
||||
//
|
||||
|
||||
// Check if that configuration was registered:
|
||||
if(!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
var successful = false;
|
||||
var configuredValue = configMeta.Default;
|
||||
|
||||
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
|
||||
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
|
||||
configuredLuaList.Type is LuaValueType.Table &&
|
||||
configuredLuaList.TryRead<LuaTable>(out var valueTable))
|
||||
{
|
||||
// Determine the length of the Lua table and prepare a set to hold the parsed values:
|
||||
var len = valueTable.ArrayLength;
|
||||
var set = new HashSet<TValue>(len);
|
||||
|
||||
// Iterate over each entry in the Lua table:
|
||||
for (var index = 1; index <= len; index++)
|
||||
{
|
||||
// Retrieve the Lua value at the current index:
|
||||
var value = valueTable[index];
|
||||
|
||||
// Step 2 -- try to read the Lua value as a string:
|
||||
if (value.Type is LuaValueType.String && value.TryRead<string>(out var configuredLuaValueText))
|
||||
{
|
||||
// Step 3 -- try to parse the string as the target type:
|
||||
if (Enum.TryParse(typeof(TValue), configuredLuaValueText, true, out var configuredEnum))
|
||||
set.Add((TValue)configuredEnum);
|
||||
}
|
||||
}
|
||||
|
||||
configuredValue = set;
|
||||
successful = true;
|
||||
}
|
||||
|
||||
if(dryRun)
|
||||
return successful;
|
||||
|
||||
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to process the configuration settings from a Lua table for string set types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the configuration is successfully processed, it updates the configuration metadata with the configured value.
|
||||
/// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID.
|
||||
/// The setting's value is set to the configured value.
|
||||
/// </remarks>
|
||||
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
|
||||
/// <param name="settings">The Lua table containing the settings to process.</param>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="dryRun">When true, the method will not apply any changes but only check if the configuration can be read.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
|
||||
public static bool TryProcessConfiguration<TClass>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, ISet<string>>> propertyExpression,
|
||||
Guid configPluginId,
|
||||
LuaTable settings,
|
||||
bool dryRun)
|
||||
{
|
||||
//
|
||||
// Handle configured string sets
|
||||
//
|
||||
|
||||
// Check if that configuration was registered:
|
||||
if(!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
var successful = false;
|
||||
var configuredValue = configMeta.Default;
|
||||
|
||||
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
|
||||
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
|
||||
configuredLuaList.Type is LuaValueType.Table &&
|
||||
configuredLuaList.TryRead<LuaTable>(out var valueTable))
|
||||
{
|
||||
// Determine the length of the Lua table and prepare a set to hold the parsed values:
|
||||
var len = valueTable.ArrayLength;
|
||||
var set = new HashSet<string>(len);
|
||||
|
||||
// Iterate over each entry in the Lua table:
|
||||
for (var index = 1; index <= len; index++)
|
||||
{
|
||||
// Retrieve the Lua value at the current index:
|
||||
var value = valueTable[index];
|
||||
|
||||
// Step 2 -- try to read the Lua value as a string:
|
||||
if (value.Type is LuaValueType.String && value.TryRead<string>(out var configuredLuaValueText))
|
||||
set.Add(configuredLuaValueText);
|
||||
}
|
||||
|
||||
configuredValue = set;
|
||||
successful = true;
|
||||
}
|
||||
|
||||
if(dryRun)
|
||||
return successful;
|
||||
|
||||
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to process the configuration settings from a Lua table for string dictionary types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the configuration is successfully processed, it updates the configuration metadata with the configured value.
|
||||
/// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID.
|
||||
/// The setting's value is set to the configured value.
|
||||
/// </remarks>
|
||||
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
|
||||
/// <param name="settings">The Lua table containing the settings to process.</param>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="dryRun">When true, the method will not apply any changes but only check if the configuration can be read.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
|
||||
public static bool TryProcessConfiguration<TClass>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, IDictionary<string, string>>> propertyExpression,
|
||||
Guid configPluginId,
|
||||
LuaTable settings,
|
||||
bool dryRun)
|
||||
{
|
||||
//
|
||||
// Handle configured string dictionaries (both keys and values are strings)
|
||||
//
|
||||
|
||||
// Check if that configuration was registered:
|
||||
if(!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
var successful = false;
|
||||
var configuredValue = configMeta.Default;
|
||||
|
||||
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
|
||||
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
|
||||
configuredLuaList.Type is LuaValueType.Table &&
|
||||
configuredLuaList.TryRead<LuaTable>(out var valueTable))
|
||||
{
|
||||
// Determine the length of the Lua table and prepare a dictionary to hold the parsed key-value pairs.
|
||||
// Instead of using ArrayLength, we use HashMapCount to get the number of key-value pairs:
|
||||
var len = valueTable.HashMapCount;
|
||||
if (len > 0)
|
||||
configuredValue.Clear();
|
||||
|
||||
// In order to iterate over all key-value pairs in the Lua table, we have to use TryGetNext.
|
||||
// Thus, we initialize the previous key variable to Nil and keep calling TryGetNext until
|
||||
// there are no more pairs:
|
||||
var previousKey = LuaValue.Nil;
|
||||
while(valueTable.TryGetNext(previousKey, out var pair))
|
||||
{
|
||||
// Update the previous key for the next iteration:
|
||||
previousKey = pair.Key;
|
||||
|
||||
// Try to read both the key and the value as strings:
|
||||
var hadKey = pair.Key.TryRead<string>(out var key);
|
||||
var hadValue = pair.Value.TryRead<string>(out var value);
|
||||
|
||||
// If both key and value were read successfully, add them to the dictionary:
|
||||
if (hadKey && hadValue)
|
||||
configuredValue[key] = value;
|
||||
}
|
||||
|
||||
successful = true;
|
||||
}
|
||||
|
||||
if(dryRun)
|
||||
return successful;
|
||||
|
||||
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the parsed configuration value based on whether the parsing was successful and whether it's a dry run.
|
||||
/// </summary>
|
||||
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
|
||||
/// <param name="dryRun">When true, no changes will be applied.</param>
|
||||
/// <param name="successful">Indicates whether the configuration value was successfully parsed.</param>
|
||||
/// <param name="configMeta">The configuration metadata.</param>
|
||||
/// <param name="configuredValue">>The parsed configuration value.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the configuration property value.</typeparam>
|
||||
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
|
||||
private static bool HandleParsedValue<TClass, TValue>(
|
||||
Guid configPluginId,
|
||||
bool dryRun,
|
||||
bool successful,
|
||||
ConfigMeta<TClass, TValue> configMeta,
|
||||
TValue configuredValue)
|
||||
{
|
||||
if(dryRun)
|
||||
return successful;
|
||||
|
||||
switch (successful)
|
||||
{
|
||||
case true:
|
||||
//
|
||||
// Case: the setting was configured, and we could read the value successfully.
|
||||
//
|
||||
|
||||
// Set the configured value and lock the managed state:
|
||||
configMeta.SetValue(configuredValue);
|
||||
configMeta.LockManagedState(configPluginId);
|
||||
break;
|
||||
|
||||
case false when configMeta.IsLocked && configMeta.MangedByConfigPluginId == configPluginId:
|
||||
//
|
||||
// Case: the setting was configured previously, but we could not read the value successfully.
|
||||
// This happens when the setting was removed from the configuration plugin. We handle that
|
||||
// case only when the setting was locked and managed by the same configuration plugin.
|
||||
//
|
||||
// The other case, when the setting was locked and managed by a different configuration plugin,
|
||||
// is handled by the IsConfigurationLeftOver method, which checks if the configuration plugin
|
||||
// is still available. If it is not available, it resets the managed state of the
|
||||
// configuration setting, allowing it to be reconfigured by a different plugin or left unchanged.
|
||||
//
|
||||
configMeta.ResetManagedState();
|
||||
break;
|
||||
|
||||
case false:
|
||||
//
|
||||
// Case: the setting was not configured, or we could not read the value successfully.
|
||||
// We do not change the setting, and it remains at whatever value it had before.
|
||||
//
|
||||
break;
|
||||
}
|
||||
|
||||
return successful;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
public static partial class ManagedConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a configuration setting with a default value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When called from the JSON deserializer, the configSelection parameter will be null.
|
||||
/// In this case, the method will return the default value without registering the setting.
|
||||
/// </remarks>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="defaultValue">The default value to use when the setting is not configured.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
||||
/// <returns>The default value.</returns>
|
||||
public static TValue Register<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>>? configSelection,
|
||||
Expression<Func<TClass, TValue>> propertyExpression,
|
||||
TValue defaultValue)
|
||||
where TValue : struct
|
||||
{
|
||||
// When called from the JSON deserializer by using the standard constructor,
|
||||
// we ignore the register call and return the default value:
|
||||
if (configSelection is null)
|
||||
return defaultValue;
|
||||
|
||||
var configPath = Path(configSelection, propertyExpression);
|
||||
|
||||
// If the metadata already exists for this configuration path, we return the default value:
|
||||
if (METADATA.ContainsKey(configPath))
|
||||
return defaultValue;
|
||||
|
||||
// Not registered yet, so we register it now:
|
||||
METADATA[configPath] = new ConfigMeta<TClass, TValue>(configSelection, propertyExpression)
|
||||
{
|
||||
Default = defaultValue,
|
||||
};
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a configuration setting with a default value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When called from the JSON deserializer, the configSelection parameter will be null.
|
||||
/// In this case, the method will return the default value without registering the setting.
|
||||
/// </remarks>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="defaultValue">The default value to use when the setting is not configured.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <returns>The default value.</returns>
|
||||
public static string Register<TClass>(
|
||||
Expression<Func<Data, TClass>>? configSelection,
|
||||
Expression<Func<TClass, string>> propertyExpression,
|
||||
string defaultValue)
|
||||
{
|
||||
// When called from the JSON deserializer by using the standard constructor,
|
||||
// we ignore the register call and return the default value:
|
||||
if(configSelection is null)
|
||||
return defaultValue;
|
||||
|
||||
var configPath = Path(configSelection, propertyExpression);
|
||||
|
||||
// If the metadata already exists for this configuration path, we return the default value:
|
||||
if (METADATA.ContainsKey(configPath))
|
||||
return defaultValue;
|
||||
|
||||
// Not registered yet, so we register it now:
|
||||
METADATA[configPath] = new ConfigMeta<TClass, string>(configSelection, propertyExpression)
|
||||
{
|
||||
Default = defaultValue,
|
||||
};
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a configuration setting with a default value for a IList of TValue.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the configSelection parameter is null, the method returns a list containing the default value
|
||||
/// without registering the configuration setting.
|
||||
/// </remarks>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="defaultValue">The default value to use when the setting is not configured.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the elements in the list within the configuration class.</typeparam>
|
||||
/// <returns>A list containing the default value.</returns>
|
||||
public static List<TValue> Register<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>>? configSelection,
|
||||
Expression<Func<TClass, IList<TValue>>> propertyExpression,
|
||||
TValue defaultValue)
|
||||
{
|
||||
// When called from the JSON deserializer by using the standard constructor,
|
||||
// we ignore the register call and return the default value:
|
||||
if(configSelection is null)
|
||||
return [defaultValue];
|
||||
|
||||
var configPath = Path(configSelection, propertyExpression);
|
||||
|
||||
// If the metadata already exists for this configuration path, we return the default value:
|
||||
if (METADATA.ContainsKey(configPath))
|
||||
return [defaultValue];
|
||||
|
||||
// Not registered yet, so we register it now:
|
||||
METADATA[configPath] = new ConfigMeta<TClass, IList<TValue>>(configSelection, propertyExpression)
|
||||
{
|
||||
Default = [defaultValue],
|
||||
};
|
||||
|
||||
return [defaultValue];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a configuration setting with multiple default values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When called with a null configSelection parameter, the method ignores the register call and directly returns the default values.
|
||||
/// If the configuration path already exists in the metadata, the method also returns the default values without registering new metadata.
|
||||
/// </remarks>
|
||||
/// <param name="configSelection">The expression used to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression used to select the property within the configuration class.</param>
|
||||
/// <param name="defaultValues">The list of default values to be used when the configuration setting is not defined.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the elements within the property list.</typeparam>
|
||||
/// <returns>The list of default values.</returns>
|
||||
public static List<TValue> Register<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>>? configSelection,
|
||||
Expression<Func<TClass, IList<TValue>>> propertyExpression,
|
||||
IList<TValue> defaultValues)
|
||||
{
|
||||
// When called from the JSON deserializer by using the standard constructor,
|
||||
// we ignore the register call and return the default value:
|
||||
if(configSelection is null)
|
||||
return [..defaultValues];
|
||||
|
||||
var configPath = Path(configSelection, propertyExpression);
|
||||
|
||||
// If the metadata already exists for this configuration path, we return the default value:
|
||||
if (METADATA.ContainsKey(configPath))
|
||||
return [..defaultValues];
|
||||
|
||||
// Not registered yet, so we register it now:
|
||||
METADATA[configPath] = new ConfigMeta<TClass, IList<TValue>>(configSelection, propertyExpression)
|
||||
{
|
||||
Default = [..defaultValues],
|
||||
};
|
||||
|
||||
return [..defaultValues];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a configuration setting with a default value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When called with a null configSelection, this method returns the default value without registering the setting.
|
||||
/// </remarks>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the set within the configuration class.</param>
|
||||
/// <param name="defaultValue">The default value to use when the setting is not configured.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the values within the set.</typeparam>
|
||||
/// <returns>A set containing the default value.</returns>
|
||||
public static HashSet<TValue> Register<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>>? configSelection,
|
||||
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
|
||||
TValue defaultValue)
|
||||
{
|
||||
// When called from the JSON deserializer by using the standard constructor,
|
||||
// we ignore the register call and return the default value:
|
||||
if (configSelection is null)
|
||||
return [defaultValue];
|
||||
|
||||
var configPath = Path(configSelection, propertyExpression);
|
||||
|
||||
// If the metadata already exists for this configuration path, we return the default value:
|
||||
if (METADATA.ContainsKey(configPath))
|
||||
return [defaultValue];
|
||||
|
||||
// Not registered yet, so we register it now:
|
||||
METADATA[configPath] = new ConfigMeta<TClass, ISet<TValue>>(configSelection, propertyExpression)
|
||||
{
|
||||
Default = new HashSet<TValue> { defaultValue },
|
||||
};
|
||||
|
||||
return [defaultValue];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a configuration setting with a collection of default values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the method is invoked with a null configSelection, the configuration path
|
||||
/// is ignored, and the specified default values are returned without registration.
|
||||
/// </remarks>
|
||||
/// <param name="configSelection">The expression that selects the configuration class from the root Data model.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="defaultValues">The default collection of values to use when the setting is not configured.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class from which the property is selected.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the elements in the collection associated with the configuration property.</typeparam>
|
||||
/// <returns>A set containing the default values.</returns>
|
||||
public static HashSet<TValue> Register<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>>? configSelection,
|
||||
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
|
||||
IList<TValue> defaultValues)
|
||||
{
|
||||
// When called from the JSON deserializer by using the standard constructor,
|
||||
// we ignore the register call and return the default value:
|
||||
if (configSelection is null)
|
||||
return [..defaultValues];
|
||||
|
||||
var configPath = Path(configSelection, propertyExpression);
|
||||
|
||||
// If the metadata already exists for this configuration path, we return the default value:
|
||||
if (METADATA.ContainsKey(configPath))
|
||||
return [..defaultValues];
|
||||
|
||||
// Not registered yet, so we register it now:
|
||||
METADATA[configPath] = new ConfigMeta<TClass, ISet<TValue>>(configSelection, propertyExpression)
|
||||
{
|
||||
Default = new HashSet<TValue>(defaultValues),
|
||||
};
|
||||
|
||||
return [..defaultValues];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a configuration setting with a default dictionary of string key-value pairs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the method is invoked with a null configSelection, the configuration path
|
||||
/// is ignored, and the specified default values are returned without registration.
|
||||
/// </remarks>
|
||||
/// <param name="configSelection">The expression that selects the configuration class from the root Data model.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="defaultValues">The default dictionary of values to use when the setting is not configured.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class from which the property is selected.</typeparam>
|
||||
/// <typeparam name="TDict">>The type of the dictionary within the configuration class.</typeparam>
|
||||
/// <returns>A dictionary containing the default values.</returns>
|
||||
public static TDict Register<TClass, TDict>(
|
||||
Expression<Func<Data, TClass>>? configSelection,
|
||||
Expression<Func<TClass, IDictionary<string, string>>> propertyExpression,
|
||||
TDict defaultValues)
|
||||
where TDict : IDictionary<string, string>, new()
|
||||
{
|
||||
// When called from the JSON deserializer by using the standard constructor,
|
||||
// we ignore the register call and return the default value:
|
||||
if (configSelection is null)
|
||||
return new();
|
||||
|
||||
var configPath = Path(configSelection, propertyExpression);
|
||||
|
||||
// If the metadata already exists for this configuration path, we return the default value:
|
||||
if (METADATA.ContainsKey(configPath))
|
||||
return defaultValues;
|
||||
|
||||
// Not registered yet, so we register it now:
|
||||
METADATA[configPath] = new ConfigMeta<TClass, IDictionary<string, string>>(configSelection, propertyExpression)
|
||||
{
|
||||
Default = defaultValues,
|
||||
};
|
||||
|
||||
return defaultValues;
|
||||
}
|
||||
}
|
||||
@@ -4,62 +4,34 @@ using System.Linq.Expressions;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using Lua;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
public static class ManagedConfiguration
|
||||
public static partial class ManagedConfiguration
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, IConfig> METADATA = new();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a configuration setting with a default value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When called from the JSON deserializer, the configSelection parameter will be null.
|
||||
/// In this case, the method will return the default value without registering the setting.
|
||||
/// </remarks>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="defaultValue">The default value to use when the setting is not configured.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
||||
/// <returns>The default value.</returns>
|
||||
public static TValue Register<TClass, TValue>(Expression<Func<Data, TClass>>? configSelection, Expression<Func<TClass, TValue>> propertyExpression, TValue defaultValue)
|
||||
{
|
||||
// When called from the JSON deserializer by using the standard constructor,
|
||||
// we ignore the register call and return the default value:
|
||||
if(configSelection is null)
|
||||
return defaultValue;
|
||||
|
||||
var configPath = Path(configSelection, propertyExpression);
|
||||
|
||||
// If the metadata already exists for this configuration path, we return the default value:
|
||||
if (METADATA.ContainsKey(configPath))
|
||||
return defaultValue;
|
||||
|
||||
METADATA[configPath] = new ConfigMeta<TClass, TValue>(configSelection, propertyExpression)
|
||||
{
|
||||
Default = defaultValue,
|
||||
};
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the configuration metadata for a given configuration selection and property expression.
|
||||
/// Attempts to retrieve the configuration metadata for a given configuration selection and
|
||||
/// property expression (enum-based).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When no configuration metadata is found, it returns a NoConfig instance with the default value set to default(TValue).
|
||||
/// This allows the caller to handle the absence of configuration gracefully. In such cases, the return value of the method will be false.
|
||||
/// When no configuration metadata is found, it returns a NoConfig instance with the default
|
||||
/// value set to default(TValue). This allows the caller to handle the absence of configuration
|
||||
/// gracefully. In such cases, the return value of the method will be false.
|
||||
/// </remarks>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="configMeta">The output parameter that will hold the configuration metadata if found.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the
|
||||
/// configuration class.</param>
|
||||
/// <param name="configMeta">The output parameter that will hold the configuration metadata
|
||||
/// if found.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
||||
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
||||
public static bool TryGet<TClass, TValue>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, TValue>> propertyExpression, out ConfigMeta<TClass, TValue> configMeta)
|
||||
public static bool TryGet<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, TValue>> propertyExpression,
|
||||
out ConfigMeta<TClass, TValue> configMeta)
|
||||
where TValue : Enum
|
||||
{
|
||||
var configPath = Path(configSelection, propertyExpression);
|
||||
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, TValue> meta)
|
||||
@@ -77,77 +49,185 @@ public static class ManagedConfiguration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to process the configuration settings from a Lua table.
|
||||
/// Attempts to retrieve the configuration metadata for a given configuration selection and
|
||||
/// property expression (string-based).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the configuration is successfully processed, it updates the configuration metadata with the configured value.
|
||||
/// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID.
|
||||
/// The setting's value is set to the configured value.
|
||||
/// When no configuration metadata is found, it returns a NoConfig instance with the default
|
||||
/// value set to default(TValue). This allows the caller to handle the absence of configuration
|
||||
/// gracefully. In such cases, the return value of the method will be false.
|
||||
/// </remarks>
|
||||
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
|
||||
/// <param name="settings">The Lua table containing the settings to process.</param>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||
/// <param name="dryRun">When true, the method will not apply any changes, but only check if the configuration can be read.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the
|
||||
/// configuration class.</param>
|
||||
/// <param name="configMeta">The output parameter that will hold the configuration metadata
|
||||
/// if found.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
||||
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
|
||||
public static bool TryProcessConfiguration<TClass, TValue>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, TValue>> propertyExpression, Guid configPluginId, LuaTable settings, bool dryRun)
|
||||
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
||||
public static bool TryGet<TClass>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, string>> propertyExpression,
|
||||
out ConfigMeta<TClass, string> configMeta)
|
||||
{
|
||||
if(!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
var (configuredValue, successful) = configMeta.Default switch
|
||||
var configPath = Path(configSelection, propertyExpression);
|
||||
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, string> meta)
|
||||
{
|
||||
Enum => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredEnumValue) && configuredEnumValue.TryRead<string>(out var configuredEnumText) && Enum.TryParse(typeof(TValue), configuredEnumText, true, out var configuredEnum) ? ((TValue)configuredEnum, true) : (configMeta.Default, false),
|
||||
Guid => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredGuidValue) && configuredGuidValue.TryRead<string>(out var configuredGuidText) && Guid.TryParse(configuredGuidText, out var configuredGuid) ? ((TValue)(object)configuredGuid, true) : (configMeta.Default, false),
|
||||
|
||||
string => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredTextValue) && configuredTextValue.TryRead<string>(out var configuredText) ? ((TValue)(object)configuredText, true) : (configMeta.Default, false),
|
||||
bool => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredBoolValue) && configuredBoolValue.TryRead<bool>(out var configuredState) ? ((TValue)(object)configuredState, true) : (configMeta.Default, false),
|
||||
|
||||
int => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredIntValue) && configuredIntValue.TryRead<int>(out var configuredInt) ? ((TValue)(object)configuredInt, true) : (configMeta.Default, false),
|
||||
double => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredDoubleValue) && configuredDoubleValue.TryRead<double>(out var configuredDouble) ? ((TValue)(object)configuredDouble, true) : (configMeta.Default, false),
|
||||
float => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredFloatValue) && configuredFloatValue.TryRead<float>(out var configuredFloat) ? ((TValue)(object)configuredFloat, true) : (configMeta.Default, false),
|
||||
|
||||
_ => (configMeta.Default, false),
|
||||
};
|
||||
|
||||
if(dryRun)
|
||||
return successful;
|
||||
|
||||
switch (successful)
|
||||
{
|
||||
case true:
|
||||
//
|
||||
// Case: the setting was configured, and we could read the value successfully.
|
||||
//
|
||||
configMeta.SetValue(configuredValue);
|
||||
configMeta.LockManagedState(configPluginId);
|
||||
break;
|
||||
|
||||
case false when configMeta.IsLocked && configMeta.MangedByConfigPluginId == configPluginId:
|
||||
//
|
||||
// Case: the setting was configured previously, but we could not read the value successfully.
|
||||
// This happens when the setting was removed from the configuration plugin. We handle that
|
||||
// case only when the setting was locked and managed by the same configuration plugin.
|
||||
//
|
||||
// The other case, when the setting was locked and managed by a different configuration plugin,
|
||||
// is handled by the IsConfigurationLeftOver method, which checks if the configuration plugin
|
||||
// is still available. If it is not available, it resets the managed state of the
|
||||
// configuration setting, allowing it to be reconfigured by a different plugin or left unchanged.
|
||||
//
|
||||
configMeta.ResetManagedState();
|
||||
break;
|
||||
|
||||
case false:
|
||||
//
|
||||
// Case: the setting was not configured, or we could not read the value successfully.
|
||||
// We do not change the setting, and it remains at whatever value it had before.
|
||||
//
|
||||
break;
|
||||
configMeta = meta;
|
||||
return true;
|
||||
}
|
||||
|
||||
return successful;
|
||||
configMeta = new NoConfig<TClass, string>(configSelection, propertyExpression)
|
||||
{
|
||||
Default = string.Empty,
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the configuration metadata for a given configuration selection and
|
||||
/// property expression (ISpanParsable-based).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When no configuration metadata is found, it returns a NoConfig instance with the default
|
||||
/// value set to default(TValue). This allows the caller to handle the absence of configuration
|
||||
/// gracefully. In such cases, the return value of the method will be false.
|
||||
/// </remarks>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the
|
||||
/// configuration class.</param>
|
||||
/// <param name="configMeta">The output parameter that will hold the configuration metadata
|
||||
/// if found.</param>
|
||||
/// <param name="_">An optional parameter to help with method overload resolution.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
||||
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
||||
|
||||
// ReSharper disable MethodOverloadWithOptionalParameter
|
||||
public static bool TryGet<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, TValue>> propertyExpression,
|
||||
out ConfigMeta<TClass, TValue> configMeta,
|
||||
ISpanParsable<TValue>? _ = null)
|
||||
where TValue : struct, ISpanParsable<TValue>
|
||||
{
|
||||
var configPath = Path(configSelection, propertyExpression);
|
||||
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, TValue> meta)
|
||||
{
|
||||
configMeta = meta;
|
||||
return true;
|
||||
}
|
||||
|
||||
configMeta = new NoConfig<TClass, TValue>(configSelection, propertyExpression)
|
||||
{
|
||||
Default = default!,
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
// ReSharper restore MethodOverloadWithOptionalParameter
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the configuration metadata for a list-based setting.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When no configuration metadata is found, it returns a NoConfig instance with the default
|
||||
/// value set to an empty list. This allows the caller to handle the absence of configuration
|
||||
/// gracefully. In such cases, the return value of the method will be false.
|
||||
/// </remarks>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the
|
||||
/// configuration class.</param>
|
||||
/// <param name="configMeta">The output parameter that will hold the configuration metadata
|
||||
/// if found.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
||||
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
||||
public static bool TryGet<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, IList<TValue>>> propertyExpression,
|
||||
out ConfigMeta<TClass, IList<TValue>> configMeta)
|
||||
{
|
||||
var configPath = Path(configSelection, propertyExpression);
|
||||
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, IList<TValue>> meta)
|
||||
{
|
||||
configMeta = meta;
|
||||
return true;
|
||||
}
|
||||
|
||||
configMeta = new NoConfig<TClass, IList<TValue>>(configSelection, propertyExpression)
|
||||
{
|
||||
Default = [],
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the configuration metadata for a set-based setting.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When no configuration metadata is found, it returns a NoConfig instance with the default
|
||||
/// value set to an empty set. This allows the caller to handle the absence of configuration
|
||||
/// gracefully. In such cases, the return value of the method will be false.
|
||||
/// </remarks>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the
|
||||
/// configuration class.</param>
|
||||
/// <param name="configMeta">The output parameter that will hold the configuration metadata
|
||||
/// if found.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
||||
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
||||
public static bool TryGet<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
|
||||
out ConfigMeta<TClass, ISet<TValue>> configMeta)
|
||||
{
|
||||
var configPath = Path(configSelection, propertyExpression);
|
||||
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, ISet<TValue>> meta)
|
||||
{
|
||||
configMeta = meta;
|
||||
return true;
|
||||
}
|
||||
|
||||
configMeta = new NoConfig<TClass, ISet<TValue>>(configSelection, propertyExpression)
|
||||
{
|
||||
Default = new HashSet<TValue>(),
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the configuration metadata for a string dictionary-based setting.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When no configuration metadata is found, it returns a NoConfig instance with the default
|
||||
/// value set to an empty dictionary. This allows the caller to handle the absence of configuration
|
||||
/// gracefully. In such cases, the return value of the method will be false.
|
||||
/// </remarks>
|
||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||
/// <param name="propertyExpression">The expression to select the property within the
|
||||
/// configuration class.</param>
|
||||
/// <param name="configMeta">The output parameter that will hold the configuration metadata
|
||||
/// if found.</param>
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
||||
public static bool TryGet<TClass>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, IDictionary<string, string>>> propertyExpression,
|
||||
out ConfigMeta<TClass, IDictionary<string, string>> configMeta)
|
||||
{
|
||||
var configPath = Path(configSelection, propertyExpression);
|
||||
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, IDictionary<string, string>> meta)
|
||||
{
|
||||
configMeta = meta;
|
||||
return true;
|
||||
}
|
||||
|
||||
configMeta = new NoConfig<TClass, IDictionary<string, string>>(configSelection, propertyExpression)
|
||||
{
|
||||
Default = new Dictionary<string, string>(),
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -162,23 +242,135 @@ public static class ManagedConfiguration
|
||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
||||
/// <returns>True if the configuration setting is left over and was reset, otherwise false.</returns>
|
||||
public static bool IsConfigurationLeftOver<TClass, TValue>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, TValue>> propertyExpression, IEnumerable<IAvailablePlugin> availablePlugins)
|
||||
public static bool IsConfigurationLeftOver<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, TValue>> propertyExpression,
|
||||
IEnumerable<IAvailablePlugin> availablePlugins)
|
||||
where TValue : Enum
|
||||
{
|
||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
if(configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
||||
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
||||
return false;
|
||||
|
||||
// Check if the configuration plugin ID is valid against the available plugin IDs:
|
||||
|
||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId);
|
||||
if (plugin is null)
|
||||
{
|
||||
// Remove the locked state:
|
||||
configMeta.ResetManagedState();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsConfigurationLeftOver<TClass>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, string>> propertyExpression,
|
||||
IEnumerable<IAvailablePlugin> availablePlugins)
|
||||
{
|
||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
||||
return false;
|
||||
|
||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId);
|
||||
if (plugin is null)
|
||||
{
|
||||
configMeta.ResetManagedState();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ReSharper disable MethodOverloadWithOptionalParameter
|
||||
public static bool IsConfigurationLeftOver<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, TValue>> propertyExpression,
|
||||
IEnumerable<IAvailablePlugin> availablePlugins,
|
||||
ISpanParsable<TValue>? _ = null)
|
||||
where TValue : struct, ISpanParsable<TValue>
|
||||
{
|
||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
||||
return false;
|
||||
|
||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId);
|
||||
if (plugin is null)
|
||||
{
|
||||
configMeta.ResetManagedState();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ReSharper restore MethodOverloadWithOptionalParameter
|
||||
|
||||
public static bool IsConfigurationLeftOver<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, IList<TValue>>> propertyExpression,
|
||||
IEnumerable<IAvailablePlugin> availablePlugins)
|
||||
{
|
||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
||||
return false;
|
||||
|
||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId);
|
||||
if (plugin is null)
|
||||
{
|
||||
configMeta.ResetManagedState();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsConfigurationLeftOver<TClass, TValue>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
|
||||
IEnumerable<IAvailablePlugin> availablePlugins)
|
||||
{
|
||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
||||
return false;
|
||||
|
||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId);
|
||||
if (plugin is null)
|
||||
{
|
||||
configMeta.ResetManagedState();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsConfigurationLeftOver<TClass>(
|
||||
Expression<Func<Data, TClass>> configSelection,
|
||||
Expression<Func<TClass, IDictionary<string, string>>> propertyExpression,
|
||||
IEnumerable<IAvailablePlugin> availablePlugins)
|
||||
{
|
||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
||||
return false;
|
||||
|
||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId);
|
||||
if (plugin is null)
|
||||
{
|
||||
configMeta.ResetManagedState();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -1,4 +1,4 @@
|
||||
namespace AIStudio.Provider;
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Data model for a source used in the response.
|
||||
@@ -14,4 +14,9 @@ public interface ISource
|
||||
/// The URL of the source.
|
||||
/// </summary>
|
||||
public string URL { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The origin of the source, whether it was provided by the AI or by the RAG process.
|
||||
/// </summary>
|
||||
public SourceOrigin Origin { get; }
|
||||
}
|
||||
@@ -61,6 +61,12 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
// Config: allow the user to add providers?
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: preview features visibility
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreviewVisibility, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: enabled preview features
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.EnabledPreviewFeatures, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Handle configured LLM providers:
|
||||
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, x => x.NextProviderNum, mainTable, this.Id, ref this.configObjects, dryRun);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Text;
|
||||
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
using Lua;
|
||||
using Lua.Standard;
|
||||
@@ -138,16 +137,24 @@ public static partial class PluginFactory
|
||||
if(PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, configObjectList))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check for update behavior:
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver<DataApp, UpdateInterval>(x => x.App, x => x.UpdateInterval, AVAILABLE_PLUGINS))
|
||||
// Check for the update interval:
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UpdateInterval, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check for update installation behavior:
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver<DataApp, UpdateInstallation>(x => x.App, x => x.UpdateInstallation, AVAILABLE_PLUGINS))
|
||||
// Check for the update installation method:
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UpdateInstallation, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check for users allowed to added providers:
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver<DataApp, bool>(x => x.App, x => x.AllowUserToAddProvider, AVAILABLE_PLUGINS))
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.AllowUserToAddProvider, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check for preview visibility:
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreviewVisibility, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check for enabled preview features:
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.EnabledPreviewFeatures, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
if (wasConfigurationChanged)
|
||||
|
||||
@@ -10,6 +10,8 @@ namespace AIStudio.Tools.RAG.AugmentationProcesses;
|
||||
|
||||
public sealed class AugmentationOne : IAugmentationProcess
|
||||
{
|
||||
private static readonly ILogger<AugmentationOne> LOGGER = Program.LOGGER_FACTORY.CreateLogger<AugmentationOne>();
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AugmentationOne).Namespace, nameof(AugmentationOne));
|
||||
|
||||
#region Implementation of IAugmentationProcess
|
||||
@@ -24,14 +26,13 @@ public sealed class AugmentationOne : IAugmentationProcess
|
||||
public string Description => TB("This is the standard augmentation process, which uses all retrieval contexts to augment the chat thread.");
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ChatThread> ProcessAsync(IProvider provider, IContent lastPrompt, ChatThread chatThread, IReadOnlyList<IRetrievalContext> retrievalContexts, CancellationToken token = default)
|
||||
public async Task<ChatThread> ProcessAsync(IProvider provider, IContent lastUserPrompt, ChatThread chatThread, IReadOnlyList<IRetrievalContext> retrievalContexts, CancellationToken token = default)
|
||||
{
|
||||
var logger = Program.SERVICE_PROVIDER.GetService<ILogger<AugmentationOne>>()!;
|
||||
var settings = Program.SERVICE_PROVIDER.GetService<SettingsManager>()!;
|
||||
|
||||
if(retrievalContexts.Count == 0)
|
||||
{
|
||||
logger.LogWarning("No retrieval contexts were issued. Skipping the augmentation process.");
|
||||
LOGGER.LogWarning("No retrieval contexts were issued. Skipping the augmentation process.");
|
||||
return chatThread;
|
||||
}
|
||||
|
||||
@@ -45,7 +46,7 @@ public sealed class AugmentationOne : IAugmentationProcess
|
||||
validationAgent.SetLLMProvider(provider);
|
||||
|
||||
// Let's validate all retrieval contexts:
|
||||
var validationResults = await validationAgent.ValidateRetrievalContextsAsync(lastPrompt, chatThread, retrievalContexts, token);
|
||||
var validationResults = await validationAgent.ValidateRetrievalContextsAsync(lastUserPrompt, chatThread, retrievalContexts, token);
|
||||
|
||||
//
|
||||
// Now, filter the retrieval contexts to the most relevant ones:
|
||||
@@ -57,7 +58,7 @@ public sealed class AugmentationOne : IAugmentationProcess
|
||||
retrievalContexts = validationResults.Where(x => x.RetrievalContext is not null && x.Confidence >= threshold).Select(x => x.RetrievalContext!).ToList();
|
||||
}
|
||||
|
||||
logger.LogInformation($"Starting the augmentation process over {numTotalRetrievalContexts:###,###,###,###} retrieval contexts.");
|
||||
LOGGER.LogInformation($"Starting the augmentation process over {numTotalRetrievalContexts:###,###,###,###} retrieval contexts.");
|
||||
|
||||
//
|
||||
// We build a huge prompt from all retrieval contexts:
|
||||
|
||||
+8
-9
@@ -9,6 +9,8 @@ namespace AIStudio.Tools.RAG.DataSourceSelectionProcesses;
|
||||
|
||||
public class AgenticSrcSelWithDynHeur : IDataSourceSelectionProcess
|
||||
{
|
||||
private static readonly ILogger<AgenticSrcSelWithDynHeur> LOGGER = Program.LOGGER_FACTORY.CreateLogger<AgenticSrcSelWithDynHeur>();
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AgenticSrcSelWithDynHeur).Namespace, nameof(AgenticSrcSelWithDynHeur));
|
||||
|
||||
#region Implementation of IDataSourceSelectionProcess
|
||||
@@ -23,15 +25,12 @@ public class AgenticSrcSelWithDynHeur : IDataSourceSelectionProcess
|
||||
public string Description => TB("Automatically selects the appropriate data sources based on the last prompt. Applies a heuristic reduction at the end to reduce the number of data sources.");
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DataSelectionResult> SelectDataSourcesAsync(IProvider provider, IContent lastPrompt, ChatThread chatThread, AllowedSelectedDataSources dataSources, CancellationToken token = default)
|
||||
public async Task<DataSelectionResult> SelectDataSourcesAsync(IProvider provider, IContent lastUserPrompt, ChatThread chatThread, AllowedSelectedDataSources dataSources, CancellationToken token = default)
|
||||
{
|
||||
var proceedWithRAG = true;
|
||||
IReadOnlyList<IDataSource> selectedDataSources = [];
|
||||
IReadOnlyList<DataSourceAgentSelected> finalAISelection = [];
|
||||
|
||||
// Get the logger:
|
||||
var logger = Program.SERVICE_PROVIDER.GetService<ILogger<AgenticSrcSelWithDynHeur>>()!;
|
||||
|
||||
// Get the settings manager:
|
||||
var settings = Program.SERVICE_PROVIDER.GetService<SettingsManager>()!;
|
||||
|
||||
@@ -41,12 +40,12 @@ public class AgenticSrcSelWithDynHeur : IDataSourceSelectionProcess
|
||||
try
|
||||
{
|
||||
// Let the AI agent do its work:
|
||||
var aiSelectedDataSources = await selectionAgent.PerformSelectionAsync(provider, lastPrompt, chatThread, dataSources, token);
|
||||
var aiSelectedDataSources = await selectionAgent.PerformSelectionAsync(provider, lastUserPrompt, 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.");
|
||||
LOGGER.LogWarning("The AI did not select any data sources. The RAG process is skipped.");
|
||||
proceedWithRAG = false;
|
||||
|
||||
return new(proceedWithRAG, selectedDataSources);
|
||||
@@ -54,7 +53,7 @@ public class AgenticSrcSelWithDynHeur : IDataSourceSelectionProcess
|
||||
|
||||
// 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}.");
|
||||
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:
|
||||
@@ -69,7 +68,7 @@ public class AgenticSrcSelWithDynHeur : IDataSourceSelectionProcess
|
||||
|
||||
var numHallucinatedSources = totalAISelectedDataSources - aiSelectedDataSources.Count;
|
||||
if (numHallucinatedSources > 0)
|
||||
logger.LogWarning($"The AI hallucinated {numHallucinatedSources} data source(s). We ignore them.");
|
||||
LOGGER.LogWarning($"The AI hallucinated {numHallucinatedSources} data source(s). We ignore them.");
|
||||
|
||||
if (aiSelectedDataSources.Count > 3)
|
||||
{
|
||||
@@ -85,7 +84,7 @@ public class AgenticSrcSelWithDynHeur : IDataSourceSelectionProcess
|
||||
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}.");
|
||||
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()!;
|
||||
|
||||
@@ -24,10 +24,10 @@ public interface IAugmentationProcess
|
||||
/// Starts the augmentation process.
|
||||
/// </summary>
|
||||
/// <param name="provider">The LLM provider. Gets used, e.g., for automatic retrieval context validation.</param>
|
||||
/// <param name="lastPrompt">The last prompt that was issued by the user.</param>
|
||||
/// <param name="lastUserPrompt">The last user prompt that was issued by the user.</param>
|
||||
/// <param name="chatThread">The chat thread.</param>
|
||||
/// <param name="retrievalContexts">The retrieval contexts that were issued by the retrieval process.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The altered chat thread.</returns>
|
||||
public Task<ChatThread> ProcessAsync(IProvider provider, IContent lastPrompt, ChatThread chatThread, IReadOnlyList<IRetrievalContext> retrievalContexts, CancellationToken token = default);
|
||||
public Task<ChatThread> ProcessAsync(IProvider provider, IContent lastUserPrompt, ChatThread chatThread, IReadOnlyList<IRetrievalContext> retrievalContexts, CancellationToken token = default);
|
||||
}
|
||||
@@ -24,10 +24,10 @@ public interface IDataSourceSelectionProcess
|
||||
/// Starts the data source selection process.
|
||||
/// </summary>
|
||||
/// <param name="provider">The LLM provider. Used as default for data selection agents.</param>
|
||||
/// <param name="lastPrompt">The last prompt that was issued by the user.</param>
|
||||
/// <param name="lastUserPrompt">The last prompt that was issued by the user.</param>
|
||||
/// <param name="chatThread">The chat thread.</param>
|
||||
/// <param name="dataSources">The allowed data sources yielded by the data source service.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns></returns>
|
||||
public Task<DataSelectionResult> SelectDataSourcesAsync(IProvider provider, IContent lastPrompt, ChatThread chatThread, AllowedSelectedDataSources dataSources, CancellationToken token = default);
|
||||
public Task<DataSelectionResult> SelectDataSourcesAsync(IProvider provider, IContent lastUserPrompt, ChatThread chatThread, AllowedSelectedDataSources dataSources, CancellationToken token = default);
|
||||
}
|
||||
@@ -24,9 +24,9 @@ public interface IRagProcess
|
||||
/// Starts the RAG process.
|
||||
/// </summary>
|
||||
/// <param name="provider">The LLM provider. Used to check whether the data sources are allowed to be used by this LLM.</param>
|
||||
/// <param name="lastPrompt">The last prompt that was issued by the user.</param>
|
||||
/// <param name="lastUserPrompt">The last user prompt that was issued by the user.</param>
|
||||
/// <param name="chatThread">The chat thread.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The altered chat thread.</returns>
|
||||
public Task<ChatThread> ProcessAsync(IProvider provider, IContent lastPrompt, ChatThread chatThread, CancellationToken token = default);
|
||||
public Task<ChatThread> ProcessAsync(IProvider provider, IContent lastUserPrompt, ChatThread chatThread, CancellationToken token = default);
|
||||
}
|
||||
@@ -9,9 +9,9 @@ public interface IRetrievalContext
|
||||
/// The name of the data source.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Depending on the configuration, the AI is selecting the appropriate data source.
|
||||
/// In order to inform the user about where the information is coming from, the data
|
||||
/// source name is necessary.
|
||||
/// This is not the name the user chooses but the name of the source where
|
||||
/// the match was found. This could be a document or database name, a website
|
||||
/// or a directory on a remote server, etc.
|
||||
/// </remarks>
|
||||
public string DataSourceName { get; init; }
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace AIStudio.Tools.RAG.RAGProcesses;
|
||||
|
||||
public sealed class AISrcSelWithRetCtxVal : IRagProcess
|
||||
{
|
||||
private static readonly ILogger<AISrcSelWithRetCtxVal> LOGGER = Program.LOGGER_FACTORY.CreateLogger<AISrcSelWithRetCtxVal>();
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AISrcSelWithRetCtxVal).Namespace, nameof(AISrcSelWithRetCtxVal));
|
||||
|
||||
#region Implementation of IRagProcess
|
||||
@@ -25,9 +27,8 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
|
||||
public string Description => TB("This RAG process filters data sources, automatically selects appropriate sources, optionally allows manual source selection, retrieves data, and automatically validates the retrieval context.");
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ChatThread> ProcessAsync(IProvider provider, IContent lastPrompt, ChatThread chatThread, CancellationToken token = default)
|
||||
public async Task<ChatThread> ProcessAsync(IProvider provider, IContent lastUserPrompt, ChatThread chatThread, CancellationToken token = default)
|
||||
{
|
||||
var logger = Program.SERVICE_PROVIDER.GetService<ILogger<AISrcSelWithRetCtxVal>>()!;
|
||||
var settings = Program.SERVICE_PROVIDER.GetService<SettingsManager>()!;
|
||||
var dataSourceService = Program.SERVICE_PROVIDER.GetService<DataSourceService>()!;
|
||||
|
||||
@@ -36,7 +37,7 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
|
||||
//
|
||||
if (chatThread.DataSourceOptions.IsEnabled())
|
||||
{
|
||||
logger.LogInformation("Data sources are enabled for this chat.");
|
||||
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:
|
||||
@@ -49,13 +50,13 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
|
||||
//
|
||||
if(chatThread.Blocks.Count == 0)
|
||||
{
|
||||
logger.LogError("The chat thread is empty. Skipping the RAG process.");
|
||||
LOGGER.LogError("The chat thread is empty. Skipping the RAG process.");
|
||||
return chatThread;
|
||||
}
|
||||
|
||||
if (chatThread.Blocks.Last().Role != ChatRole.AI)
|
||||
{
|
||||
logger.LogError("The last block in the chat thread is not the AI block. There is something wrong with the chat thread. Skipping the RAG process.");
|
||||
LOGGER.LogError("The last block in the chat thread is not the AI block. There is something wrong with the chat thread. Skipping the RAG process.");
|
||||
return chatThread;
|
||||
}
|
||||
|
||||
@@ -82,7 +83,7 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
|
||||
if (chatThread.DataSourceOptions.AutomaticDataSourceSelection)
|
||||
{
|
||||
var dataSourceSelectionProcess = new AgenticSrcSelWithDynHeur();
|
||||
var result = await dataSourceSelectionProcess.SelectDataSourcesAsync(provider, lastPrompt, chatThread, dataSources, token);
|
||||
var result = await dataSourceSelectionProcess.SelectDataSourcesAsync(provider, lastUserPrompt, chatThread, dataSources, token);
|
||||
proceedWithRAG = result.ProceedWithRAG;
|
||||
selectedDataSources = result.SelectedDataSources;
|
||||
}
|
||||
@@ -92,12 +93,12 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
|
||||
// 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}.");
|
||||
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.");
|
||||
LOGGER.LogWarning("No data sources are selected. The RAG process is skipped.");
|
||||
proceedWithRAG = false;
|
||||
}
|
||||
else
|
||||
@@ -148,7 +149,7 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
|
||||
};
|
||||
|
||||
if (previousDataSecurity != chatThread.DataSecurity)
|
||||
logger.LogInformation($"The data security of the chat thread was updated from '{previousDataSecurity}' to '{chatThread.DataSecurity}'.");
|
||||
LOGGER.LogInformation($"The data security of the chat thread was updated from '{previousDataSecurity}' to '{chatThread.DataSecurity}'.");
|
||||
}
|
||||
|
||||
//
|
||||
@@ -162,7 +163,7 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
|
||||
//
|
||||
var retrievalTasks = new List<Task<IReadOnlyList<IRetrievalContext>>>(selectedDataSources.Count);
|
||||
foreach (var dataSource in selectedDataSources)
|
||||
retrievalTasks.Add(dataSource.RetrieveDataAsync(lastPrompt, chatThreadWithoutWaitingAIBlock, token));
|
||||
retrievalTasks.Add(dataSource.RetrieveDataAsync(lastUserPrompt, chatThreadWithoutWaitingAIBlock, token));
|
||||
|
||||
//
|
||||
// Wait for all retrieval tasks to finish:
|
||||
@@ -175,7 +176,7 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "An error occurred during the retrieval process.");
|
||||
LOGGER.LogError(e, "An error occurred during the retrieval process.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,8 +187,38 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
|
||||
if (proceedWithRAG)
|
||||
{
|
||||
var augmentationProcess = new AugmentationOne();
|
||||
chatThread = await augmentationProcess.ProcessAsync(provider, lastPrompt, chatThread, dataContexts, token);
|
||||
chatThread = await augmentationProcess.ProcessAsync(provider, lastUserPrompt, chatThread, dataContexts, token);
|
||||
}
|
||||
|
||||
//
|
||||
// Add sources from the selected data
|
||||
//
|
||||
|
||||
// We know that the last block is the AI answer block (cf. check above):
|
||||
var aiAnswerBlock = chatThread.Blocks.Last();
|
||||
var aiAnswerSources = aiAnswerBlock.Content?.Sources;
|
||||
|
||||
// It should never happen that the AI answer block does not contain a content part.
|
||||
// Just in case, we check this:
|
||||
if(aiAnswerSources is null)
|
||||
return chatThread;
|
||||
|
||||
var ragSources = new List<ISource>();
|
||||
foreach (var retrievalContext in dataContexts)
|
||||
{
|
||||
var title = retrievalContext.DataSourceName;
|
||||
if(string.IsNullOrWhiteSpace(title))
|
||||
continue;
|
||||
|
||||
var link = retrievalContext.Path;
|
||||
if(!link.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
ragSources.Add(new Source(title, link, SourceOrigin.RAG));
|
||||
}
|
||||
|
||||
// Merge the sources, avoiding duplicates:
|
||||
aiAnswerSources.MergeSources(ragSources);
|
||||
}
|
||||
|
||||
return chatThread;
|
||||
|
||||
@@ -115,7 +115,14 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
|
||||
var response = await this.rust.CheckForUpdate();
|
||||
if (response.UpdateIsAvailable)
|
||||
{
|
||||
if (this.settingsManager.ConfigurationData.App.UpdateInstallation is UpdateInstallation.AUTOMATIC)
|
||||
// ReSharper disable RedundantAssignment
|
||||
var isDevEnvironment = false;
|
||||
#if DEBUG
|
||||
isDevEnvironment = true;
|
||||
#endif
|
||||
// ReSharper restore RedundantAssignment
|
||||
|
||||
if (!isDevEnvironment && this.settingsManager.ConfigurationData.App.UpdateInstallation is UpdateInstallation.AUTOMATIC)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
namespace AIStudio.Provider;
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Data model for a source used in the response.
|
||||
/// </summary>
|
||||
/// <param name="Title">The title of the source.</param>
|
||||
/// <param name="URL">The URL of the source.</param>
|
||||
public record Source(string Title, string URL) : ISource;
|
||||
public record Source(string Title, string URL, SourceOrigin Origin) : ISource;
|
||||
+36
-4
@@ -2,7 +2,7 @@ using System.Text;
|
||||
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace AIStudio.Provider;
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public static class SourceExtensions
|
||||
{
|
||||
@@ -16,11 +16,43 @@ public static class SourceExtensions
|
||||
public static string ToMarkdown(this IList<Source> sources)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("## ");
|
||||
sb.AppendLine(TB("Sources"));
|
||||
|
||||
var ragSources = new List<ISource>();
|
||||
var sourceNum = 0;
|
||||
var addedLLMHeaders = false;
|
||||
foreach (var source in sources)
|
||||
{
|
||||
switch (source.Origin)
|
||||
{
|
||||
case SourceOrigin.RAG:
|
||||
ragSources.Add(source);
|
||||
break;
|
||||
|
||||
case SourceOrigin.LLM:
|
||||
if (!addedLLMHeaders)
|
||||
{
|
||||
sb.Append("## ");
|
||||
sb.AppendLine(TB("Sources provided by the AI"));
|
||||
addedLLMHeaders = true;
|
||||
}
|
||||
|
||||
sb.Append($"- [{++sourceNum}] ");
|
||||
sb.Append('[');
|
||||
sb.Append(source.Title);
|
||||
sb.Append("](");
|
||||
sb.Append(source.URL);
|
||||
sb.AppendLine(")");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(ragSources.Count == 0)
|
||||
return sb.ToString();
|
||||
|
||||
sb.AppendLine();
|
||||
sb.Append("## ");
|
||||
sb.AppendLine(TB("Sources provided by the data providers"));
|
||||
|
||||
foreach (var source in ragSources)
|
||||
{
|
||||
sb.Append($"- [{++sourceNum}] ");
|
||||
sb.Append('[');
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the origin of a source, whether it was provided by the LLM or by the RAG process.
|
||||
/// </summary>
|
||||
public enum SourceOrigin
|
||||
{
|
||||
/// <summary>
|
||||
/// The LLM provided the source.
|
||||
/// </summary>
|
||||
LLM,
|
||||
|
||||
/// <summary>
|
||||
/// The source was provided by the RAG process.
|
||||
/// </summary>
|
||||
RAG,
|
||||
}
|
||||
@@ -30,18 +30,18 @@
|
||||
},
|
||||
"Microsoft.Extensions.FileProviders.Embedded": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.8, )",
|
||||
"resolved": "9.0.8",
|
||||
"contentHash": "LVm1o08C5eSB+WvhOEtp/cm/I3s7bCoYisTBYKgR3KEfHXkKfvkLPXfg4CNHYkxPqZ4T/XuGENDLtbhDywGFHA==",
|
||||
"requested": "[9.0.10, )",
|
||||
"resolved": "9.0.10",
|
||||
"contentHash": "XEXMhaBc6zwSRfvO3uPaexHPpP6s9A0XjZhzTdn5C7s+JXwYyX8yD5a0Uh2ZmD/Ozv3P/ZpU6L1UcDZANWipqA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.8"
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.10"
|
||||
}
|
||||
},
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.8, )",
|
||||
"resolved": "9.0.8",
|
||||
"contentHash": "rd1CbIsMtVPtZNTIVD6Xydue//klYOOQIDpRgu3BHtv17AlpRs74/6QFbcYgMm/jL+naVU2T3OFLxVSLV5lQLQ=="
|
||||
"requested": "[9.0.10, )",
|
||||
"resolved": "9.0.10",
|
||||
"contentHash": "sseaSJcBxKEpkc59hnB00b3NmJdGvJLfj74HK+nucHxERxbZSUREuWKjC9ywc+HdzJvJyiP2eiyEOROaGSfcPw=="
|
||||
},
|
||||
"MudBlazor": {
|
||||
"type": "Direct",
|
||||
@@ -152,10 +152,10 @@
|
||||
},
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.8",
|
||||
"contentHash": "4zZbQ4w+hCMm9J+z5NOj3giIPT2MhZxx05HX/MGuAmDBbjOuXlYIIRN+t4V6OLxy5nXZIcXO+dQMB/OWubuDkw==",
|
||||
"resolved": "9.0.10",
|
||||
"contentHash": "3+cLxZKUWBbpfIXLLuKcEok9C91PsV1h5xxfUsEnLSXXLNMiPDfrhpb1xajNFcejFPs9Ck/Fi3z71hYDqFBwYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "9.0.8"
|
||||
"Microsoft.Extensions.Primitives": "9.0.10"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Localization": {
|
||||
@@ -193,8 +193,8 @@
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.8",
|
||||
"contentHash": "tizSIOEsIgSNSSh+hKeUVPK7xmTIjR8s+mJWOu1KXV3htvNQiPMFRMO17OdI1y/4ZApdBVk49u/08QGC9yvLug=="
|
||||
"resolved": "9.0.10",
|
||||
"contentHash": "3pl8D1O5ZwMpDkZAT2uXrhQ6NipkwEgDLMFuURiHTf72TvkoMP61QYH3Vk1yrzVHnHBdNZk3cQACz8Zc7YGNhQ=="
|
||||
},
|
||||
"Microsoft.JSInterop": {
|
||||
"type": "Transitive",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
- Added support for citations to the chat interface. This feature is invisible unless an LLM model is streaming citations or sources.
|
||||
- Added the Responses API according to the OpenAI documentation. It is currently only used by OpenAI, but we could use the API for other providers as soon as someone offers it. This means that all text-based LLMs from OpenAI can now be used in MindWork AI Studio. For example, the Deep Research models for comprehensive research tasks.
|
||||
- Added support for web searches. Currently supported by some OpenAI models (e.g., GPT5, newer Omni models, Deep Research models) and Perplexity. Used sources are displayed visually in the chat interface.
|
||||
- Added support for the LLM provider [Perplexity](https://www.perplexity.ai/). It can be used for web searches, deep research, and general chat tasks. Thanks `peerschuett` for the contribution.
|
||||
- Improved memory usage in several areas of the app.
|
||||
- Improved plugin management for configuration plugins so that hot reload detects when a provider or chat template has been removed.
|
||||
- Improved the dialog for naming chats and workspaces to ensure valid inputs are entered.
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
# v0.9.52, build 227 (2025-09-xx xx:xx UTC)
|
||||
# v0.9.52, build 227 (2025-10-24 06:00 UTC)
|
||||
- Added a feature so that matching results from data sources (local data sources as well as external ones via the ERI interface) are now also displayed at the end of a chat. All sources that come directly from the AI (like web searches) appear first, followed by those that come from the data sources. This source display works regardless of whether the AI actually used these sources, so users always get all the relevant information.
|
||||
- Added the ability to manage the preview feature visibility and enabled preview features by using enterprise IT configurations.
|
||||
- Improved developer experience by detecting development environments and disabling update prompts in those environments.
|
||||
- Fixed an issue where external data sources using the ERI interface weren't using the correct source names during the augmentation phase.
|
||||
@@ -0,0 +1 @@
|
||||
# v0.9.53, build 228 (2025-10-xx xx:xx UTC)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
+8
-8
@@ -1,11 +1,11 @@
|
||||
0.9.51
|
||||
2025-09-04 18:02:17 UTC
|
||||
226
|
||||
9.0.109 (commit 08d4728191)
|
||||
9.0.8 (commit aae90fa090)
|
||||
1.89.0 (commit 29483883e)
|
||||
0.9.52
|
||||
2025-10-24 06:00:03 UTC
|
||||
227
|
||||
9.0.111 (commit 9c374e8914)
|
||||
9.0.10 (commit e1f19886fe)
|
||||
1.90.0 (commit 1159e78c4)
|
||||
8.12.0
|
||||
1.8.1
|
||||
ce243913ed4, release
|
||||
38103121c9a, release
|
||||
osx-arm64
|
||||
137.0.7215.0
|
||||
137.0.7215.0
|
||||
Generated
+1
-1
@@ -2599,7 +2599,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mindwork-ai-studio"
|
||||
version = "0.9.51"
|
||||
version = "0.9.52"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arboard",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mindwork-ai-studio"
|
||||
version = "0.9.51"
|
||||
version = "0.9.52"
|
||||
edition = "2021"
|
||||
description = "MindWork AI Studio"
|
||||
authors = ["Thorsten Sommer"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use log::{error, info, warn};
|
||||
use log::{error, info, trace, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
use rocket::{get, post};
|
||||
use rocket::serde::json::Json;
|
||||
@@ -12,7 +12,7 @@ use tauri::api::dialog::blocking::FileDialogBuilder;
|
||||
use tokio::time;
|
||||
use crate::api_token::APIToken;
|
||||
use crate::dotnet::stop_dotnet_server;
|
||||
use crate::environment::{is_prod, CONFIG_DIRECTORY, DATA_DIRECTORY};
|
||||
use crate::environment::{is_prod, is_dev, CONFIG_DIRECTORY, DATA_DIRECTORY};
|
||||
use crate::log::switch_to_file_logging;
|
||||
use crate::pdfium::PDFIUM_LIB_PATH;
|
||||
|
||||
@@ -78,20 +78,31 @@ pub fn start_tauri() {
|
||||
info!(Source = "Tauri"; "Updater: update is pending!");
|
||||
}
|
||||
|
||||
tauri::UpdaterEvent::DownloadProgress { chunk_length, content_length } => {
|
||||
info!(Source = "Tauri"; "Updater: downloaded {} of {:?}", chunk_length, content_length);
|
||||
tauri::UpdaterEvent::DownloadProgress { chunk_length, content_length: _ } => {
|
||||
trace!(Source = "Tauri"; "Updater: downloading chunk of {chunk_length} bytes");
|
||||
}
|
||||
|
||||
tauri::UpdaterEvent::Downloaded => {
|
||||
info!(Source = "Tauri"; "Updater: update has been downloaded!");
|
||||
warn!(Source = "Tauri"; "Try to stop the .NET server now...");
|
||||
stop_dotnet_server();
|
||||
|
||||
if is_prod() {
|
||||
stop_dotnet_server();
|
||||
} else {
|
||||
warn!(Source = "Tauri"; "Development environment detected; do not stop the .NET server.");
|
||||
}
|
||||
}
|
||||
|
||||
tauri::UpdaterEvent::Updated => {
|
||||
info!(Source = "Tauri"; "Updater: app has been updated");
|
||||
warn!(Source = "Tauri"; "Try to restart the app now...");
|
||||
app_handle.restart();
|
||||
|
||||
if is_prod() {
|
||||
app_handle.restart();
|
||||
} else {
|
||||
warn!(Source = "Tauri"; "Development environment detected; do not restart the app.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
tauri::UpdaterEvent::AlreadyUpToDate => {
|
||||
@@ -157,6 +168,16 @@ pub async fn change_location_to(url: &str) {
|
||||
/// Checks for updates.
|
||||
#[get("/updates/check")]
|
||||
pub async fn check_for_update(_token: APIToken) -> Json<CheckUpdateResponse> {
|
||||
if is_dev() {
|
||||
warn!(Source = "Updater"; "The app is running in development mode; skipping update check.");
|
||||
return Json(CheckUpdateResponse {
|
||||
update_is_available: false,
|
||||
error: false,
|
||||
new_version: String::from(""),
|
||||
changelog: String::from(""),
|
||||
});
|
||||
}
|
||||
|
||||
let app_handle = MAIN_WINDOW.lock().unwrap().as_ref().unwrap().app_handle();
|
||||
let response = app_handle.updater().check().await;
|
||||
match response {
|
||||
@@ -212,6 +233,11 @@ pub struct CheckUpdateResponse {
|
||||
/// Installs the update.
|
||||
#[get("/updates/install")]
|
||||
pub async fn install_update(_token: APIToken) {
|
||||
if is_dev() {
|
||||
warn!(Source = "Updater"; "The app is running in development mode; skipping update installation.");
|
||||
return;
|
||||
}
|
||||
|
||||
let cloned_response_option = CHECK_UPDATE_RESPONSE.lock().unwrap().clone();
|
||||
match cloned_response_option {
|
||||
Some(update_response) => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "MindWork AI Studio",
|
||||
"version": "0.9.51"
|
||||
"version": "0.9.52"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
||||
Reference in new issue
Block a user