mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-04 21:49:40 +00:00
Added opt-in sharing of feature usage with self-hosted servers (#1020)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
90c90910b3
commit
50aa11a34c
27 files changed
+531
-12
No files matched your search
@@ -37,6 +37,15 @@ public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager setti
|
||||
/// </summary>
|
||||
protected abstract Type Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The component this agent runs as.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Its requests name it, e.g., in the User-Agent for self-hosted servers. Without it, an agent
|
||||
/// working for an assistant would be counted as chat.
|
||||
/// </remarks>
|
||||
protected abstract Tools.Components Component { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the agent.
|
||||
/// </summary>
|
||||
@@ -75,9 +84,10 @@ public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager setti
|
||||
Name = string.Empty,
|
||||
SystemPrompt = systemPrompt,
|
||||
Blocks = [],
|
||||
RuntimeComponent = this.Component,
|
||||
};
|
||||
|
||||
protected UserRequest AddUserRequest(ChatThread thread, string request)
|
||||
protected static UserRequest AddUserRequest(ChatThread thread, string request)
|
||||
{
|
||||
var time = DateTimeOffset.Now;
|
||||
var lastUserPrompt = new ContentText
|
||||
|
||||
@@ -18,6 +18,9 @@ public sealed class AgentDataSourceSelection (ILogger<AgentDataSourceSelection>
|
||||
/// <inheritdoc />
|
||||
protected override Type Type => Type.SYSTEM;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Tools.Components Component => Tools.Components.AGENT_DATA_SOURCE_SELECTION;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Id => "Data Source Selection";
|
||||
|
||||
@@ -110,7 +113,7 @@ public sealed class AgentDataSourceSelection (ILogger<AgentDataSourceSelection>
|
||||
return EMPTY_BLOCK;
|
||||
|
||||
var thread = this.CreateChatThread(this.SystemPrompt(availableDataSources));
|
||||
var userRequest = this.AddUserRequest(thread, text.Text);
|
||||
var userRequest = AddUserRequest(thread, text.Text);
|
||||
await this.AddAIResponseAsync(thread, userRequest.UserPrompt, userRequest.Time);
|
||||
|
||||
var answer = thread.Blocks[^1];
|
||||
|
||||
@@ -16,6 +16,9 @@ public sealed class AgentRetrievalContextValidation (ILogger<AgentRetrievalConte
|
||||
/// <inheritdoc />
|
||||
protected override Type Type => Type.WORKER;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Tools.Components Component => Tools.Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Id => "Retrieval Context Validation";
|
||||
|
||||
@@ -97,7 +100,7 @@ public sealed class AgentRetrievalContextValidation (ILogger<AgentRetrievalConte
|
||||
return EMPTY_BLOCK;
|
||||
|
||||
var thread = this.CreateChatThread(this.SystemPrompt(retrievalContext));
|
||||
var userRequest = this.AddUserRequest(thread, text.Text);
|
||||
var userRequest = AddUserRequest(thread, text.Text);
|
||||
await this.AddAIResponseAsync(thread, userRequest.UserPrompt, userRequest.Time);
|
||||
|
||||
return thread.Blocks[^1];
|
||||
|
||||
@@ -15,6 +15,8 @@ public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsM
|
||||
|
||||
protected override Type Type => Type.SYSTEM;
|
||||
|
||||
protected override Tools.Components Component => Tools.Components.AGENT_TEXT_CONTENT_CLEANER;
|
||||
|
||||
public override string Id => "Text Content Cleaner";
|
||||
|
||||
protected override string JobDescription =>
|
||||
@@ -65,7 +67,7 @@ public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsM
|
||||
return EMPTY_BLOCK;
|
||||
|
||||
var thread = this.CreateChatThread(this.SystemPrompt(sourceURL));
|
||||
var userRequest = this.AddUserRequest(thread, text.Text);
|
||||
var userRequest = AddUserRequest(thread, text.Text);
|
||||
await this.AddAIResponseAsync(thread, userRequest.UserPrompt, userRequest.Time);
|
||||
|
||||
var answer = thread.Blocks[^1];
|
||||
|
||||
@@ -20,6 +20,8 @@ public sealed class AssistantAuditAgent(ILogger<AssistantAuditAgent> logger, ILo
|
||||
|
||||
protected override Type Type => Type.SYSTEM;
|
||||
|
||||
protected override Tools.Components Component => Tools.Components.AGENT_ASSISTANT_PLUGIN_AUDIT;
|
||||
|
||||
public override string Id => "Assistant Plugin Security Audit";
|
||||
|
||||
protected override string JobDescription =>
|
||||
@@ -109,7 +111,7 @@ public sealed class AssistantAuditAgent(ILogger<AssistantAuditAgent> logger, ILo
|
||||
return EMPTY_BLOCK;
|
||||
|
||||
var thread = this.CreateChatThread(this.SystemPrompt(string.Empty));
|
||||
var userRequest = this.AddUserRequest(thread, text.Text);
|
||||
var userRequest = AddUserRequest(thread, text.Text);
|
||||
await this.AddAIResponseAsync(thread, userRequest.UserPrompt, userRequest.Time);
|
||||
return thread.Blocks[^1];
|
||||
}
|
||||
|
||||
@@ -61,6 +61,11 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
protected abstract string SystemPrompt { get; }
|
||||
|
||||
protected abstract Tools.Components Component { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the assistant plugin, which is only set by plugin-provided assistants.
|
||||
/// </summary>
|
||||
protected virtual string RuntimeAssistantName => string.Empty;
|
||||
|
||||
protected virtual Func<string> Result2Copy => () => this.ResultingContentBlock is null ? string.Empty : this.ResultingContentBlock.Content switch
|
||||
{
|
||||
@@ -375,8 +380,9 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = string.Format(this.TB("Assistant - {0}"), this.Title),
|
||||
Blocks = [],
|
||||
RuntimeComponent = this.Component,
|
||||
};
|
||||
|
||||
this.AssignRuntimeIdentity(this.ChatThread);
|
||||
}
|
||||
|
||||
protected Guid CreateChatThread(Guid workspaceId, string name)
|
||||
@@ -392,12 +398,26 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
ChatId = chatId,
|
||||
Name = name,
|
||||
Blocks = [],
|
||||
RuntimeComponent = this.Component,
|
||||
};
|
||||
|
||||
this.AssignRuntimeIdentity(this.ChatThread);
|
||||
return chatId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets who runs the given thread: this assistant's component and, for an assistant plugin, its name.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Both decide which tools the thread may use and what its requests to self-hosted servers name. Every
|
||||
/// way an assistant creates or sends a thread goes through here, so that none of them can forget one.
|
||||
/// </remarks>
|
||||
/// <param name="chatThread">The thread this assistant runs.</param>
|
||||
protected void AssignRuntimeIdentity(ChatThread chatThread)
|
||||
{
|
||||
chatThread.RuntimeComponent = this.Component;
|
||||
chatThread.RuntimeAssistantName = this.RuntimeAssistantName;
|
||||
}
|
||||
|
||||
private Task RefreshProviderSelectionFromConfigurationAsync()
|
||||
{
|
||||
this.ProviderSettings = this.SettingsManager.GetPreselectedProvider(this.Component, this.ProviderSettings.Id);
|
||||
@@ -519,7 +539,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
{
|
||||
this.ChatThread.Blocks.Add(this.ResultingContentBlock);
|
||||
this.ChatThread.SelectedProvider = this.ProviderSettings.Id;
|
||||
this.ChatThread.RuntimeComponent = this.Component;
|
||||
this.AssignRuntimeIdentity(this.ChatThread);
|
||||
this.ChatThread.SelectedToolIds = [..this.SelectedToolIds];
|
||||
this.ChatThread.RuntimeSelectedToolIds = this.GetRunnableToolIds(this.ChatThread.RequiredOutboundDataRestriction.Restriction);
|
||||
this.ChatThread.RuntimeToolsAreAssistantManaged = this.AssistantManagedToolIds is not null;
|
||||
@@ -604,6 +624,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
|
||||
this.ChatThread.Blocks.Add(this.ResultingContentBlock);
|
||||
this.ChatThread.SelectedProvider = this.ProviderSettings.Id;
|
||||
this.AssignRuntimeIdentity(this.ChatThread);
|
||||
|
||||
await this.CheckpointAssistantSession();
|
||||
await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
|
||||
|
||||
+1
-1
@@ -110,7 +110,6 @@ public partial class AssistantBatchProcessing
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = this.Title,
|
||||
Blocks = [],
|
||||
RuntimeComponent = this.Component,
|
||||
|
||||
// A thread of its own, which has read no mailbox yet:
|
||||
RuntimeSelectedToolIds = this.GetRunnableToolIds(OutboundDataRestriction.UNRESTRICTED),
|
||||
@@ -120,6 +119,7 @@ public partial class AssistantBatchProcessing
|
||||
RuntimeToolsAreAssistantManaged = true,
|
||||
};
|
||||
|
||||
this.AssignRuntimeIdentity(chatThread);
|
||||
var userPrompt = new ContentText
|
||||
{
|
||||
Text = BuildUserPrompt(fileName, fileContent),
|
||||
|
||||
@@ -46,6 +46,12 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
|
||||
// session and media state separate while ComponentsExtensions derives their defaults from chat.
|
||||
protected override Tools.Components Component => Tools.Components.DYNAMIC_ASSISTANT;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// Empty for an assistant somebody built for themselves, see MayShareAssistantName.
|
||||
/// </remarks>
|
||||
protected override string RuntimeAssistantName => this.sharesAssistantName ? this.assistantPlugin?.Name ?? string.Empty : string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin ID as the assistant session instance ID.
|
||||
/// </summary>
|
||||
@@ -68,6 +74,7 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
|
||||
private PluginAssistantAudit? audit;
|
||||
private string securityMessage = string.Empty;
|
||||
private bool isSecurityBlocked;
|
||||
private bool sharesAssistantName;
|
||||
private PluginAssistants? pendingChatLauncher;
|
||||
private const string ASSISTANT_QUERY_KEY = "assistantId";
|
||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||
@@ -87,6 +94,7 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
|
||||
private static readonly AssistantSessionStateKey<PluginAssistantAudit?> AUDIT_STATE_KEY = new(nameof(audit));
|
||||
private static readonly AssistantSessionStateKey<string> SECURITY_MESSAGE_STATE_KEY = new(nameof(securityMessage));
|
||||
private static readonly AssistantSessionStateKey<bool> IS_SECURITY_BLOCKED_STATE_KEY = new(nameof(isSecurityBlocked));
|
||||
private static readonly AssistantSessionStateKey<bool> SHARES_ASSISTANT_NAME_STATE_KEY = new(nameof(sharesAssistantName));
|
||||
|
||||
private bool CanReviseCurrentAssistant => this.assistantPlugin is { IsInternal: false, IsManagedByConfigServer: false } && !string.IsNullOrWhiteSpace(this.assistantPlugin.PluginPath);
|
||||
|
||||
@@ -109,6 +117,7 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
|
||||
state.Set(AUDIT_STATE_KEY, this.audit);
|
||||
state.Set(SECURITY_MESSAGE_STATE_KEY, this.securityMessage);
|
||||
state.Set(IS_SECURITY_BLOCKED_STATE_KEY, this.isSecurityBlocked);
|
||||
state.Set(SHARES_ASSISTANT_NAME_STATE_KEY, this.sharesAssistantName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -130,6 +139,7 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
|
||||
state.Restore(AUDIT_STATE_KEY, value => this.audit = value);
|
||||
state.Restore(SECURITY_MESSAGE_STATE_KEY, value => this.securityMessage = value);
|
||||
state.Restore(IS_SECURITY_BLOCKED_STATE_KEY, value => this.isSecurityBlocked = value);
|
||||
state.Restore(SHARES_ASSISTANT_NAME_STATE_KEY, value => this.sharesAssistantName = value);
|
||||
}
|
||||
|
||||
#region Implementation of AssistantBase
|
||||
@@ -177,6 +187,7 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
|
||||
this.audit = this.SettingsManager.ConfigurationData.AssistantPluginAudits.FirstOrDefault(x => x.PluginId == pluginAssistant.Id && x.PluginHash == pluginHash);
|
||||
|
||||
var securityState = PluginAssistantSecurityResolver.Resolve(this.SettingsManager, pluginAssistant);
|
||||
this.sharesAssistantName = MayShareAssistantName(pluginAssistant, securityState);
|
||||
if (!securityState.CanStartAssistant)
|
||||
{
|
||||
this.assistantPlugin = pluginAssistant;
|
||||
@@ -374,6 +385,7 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
|
||||
var securityState = PluginAssistantSecurityResolver.Resolve(this.SettingsManager, updatedPlugin);
|
||||
this.securityMessage = securityState.CanStartAssistant ? string.Empty : securityState.Description;
|
||||
this.isSecurityBlocked = !securityState.CanStartAssistant;
|
||||
this.sharesAssistantName = MayShareAssistantName(updatedPlugin, securityState);
|
||||
|
||||
this.assistantState.Clear();
|
||||
if (this.RootComponent is not null)
|
||||
@@ -382,6 +394,22 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Whether requests may name this assistant plugin, e.g., in the User-Agent for self-hosted servers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only assistants from AI Studio itself or from the organization are named: those shipped with
|
||||
/// the app, rolled out by its configuration server, or approved by it. Everybody can build an
|
||||
/// assistant of their own with the Assistant Builder, and its name may well be personal, such as
|
||||
/// an application to one particular company. That name stays on the device; the request still
|
||||
/// names the component, so the operators can count it as an assistant all the same.
|
||||
/// </remarks>
|
||||
/// <param name="plugin">The assistant plugin.</param>
|
||||
/// <param name="securityState">Its security state, which tells whether the organization approved it.</param>
|
||||
/// <returns>True when its name may be shared; otherwise, false.</returns>
|
||||
private static bool MayShareAssistantName(PluginAssistants plugin, PluginAssistantSecurityState securityState) =>
|
||||
plugin.IsInternal || plugin.IsManagedByConfigServer || securityState.IsEnterpriseApproved;
|
||||
|
||||
/// <summary>
|
||||
/// Reads the tools this plugin names for its assistant.
|
||||
/// </summary>
|
||||
|
||||
@@ -4723,6 +4723,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1898114759"]
|
||||
-- Select the language for the app.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] = "Select the language for the app."
|
||||
|
||||
-- When enabled, requests to self-hosted providers and LiteLLM name the feature that sent them, e.g., the chat, an assistant, or an agent, together with the version of AI Studio and your operating system. This lets the operators of these servers, usually your organization, see which features are used. Cloud providers never receive these details, and AI Studio never sends anything to its developers.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907749697"] = "When enabled, requests to self-hosted providers and LiteLLM name the feature that sent them, e.g., the chat, an assistant, or an agent, together with the version of AI Studio and your operating system. This lets the operators of these servers, usually your organization, see which features are used. Cloud providers never receive these details, and AI Studio never sends anything to its developers."
|
||||
|
||||
-- Your organization has disabled update checks and installations.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1909339369"] = "Your organization has disabled update checks and installations."
|
||||
|
||||
@@ -4783,6 +4786,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3138812562"]
|
||||
-- Spellchecking is enabled
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Spellchecking is enabled"
|
||||
|
||||
-- Operators of self-hosted servers learn which feature sent a request
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3294604676"] = "Operators of self-hosted servers learn which feature sent a request"
|
||||
|
||||
-- External HTTPS certificates
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T348936513"] = "External HTTPS certificates"
|
||||
|
||||
@@ -4807,6 +4813,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3652888444"]
|
||||
-- Show administration settings?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3694781396"] = "Show administration settings?"
|
||||
|
||||
-- No usage details are sent
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3702320045"] = "No usage details are sent"
|
||||
|
||||
-- Read the Enterprise IT documentation for details.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3705451321"] = "Read the Enterprise IT documentation for details."
|
||||
|
||||
@@ -4828,6 +4837,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"]
|
||||
-- Show details when suspicious content was removed?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4156872850"] = "Show details when suspicious content was removed?"
|
||||
|
||||
-- Share feature usage with operators of self-hosted servers?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4162855418"] = "Share feature usage with operators of self-hosted servers?"
|
||||
|
||||
-- Select a transcription provider
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Select a transcription provider"
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ internal sealed class StructuredLlmStageRunner(
|
||||
Name = $"Visual Briefing {stage}",
|
||||
SystemPrompt = systemPrompt,
|
||||
SelectedProvider = provider.Id,
|
||||
RuntimeComponent = Tools.Components.VISUAL_BRIEFING_ASSISTANT,
|
||||
Blocks =
|
||||
[
|
||||
CreateBlock(time, ChatRole.USER, initialPrompt),
|
||||
|
||||
@@ -162,8 +162,21 @@ public sealed record ChatThread
|
||||
/// </summary>
|
||||
public List<ContentBlock> Blocks { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The component running this thread, which decides the tools it may use and what its requests name.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// None until somebody sets it: whoever sends a thread states which component they are. A place
|
||||
/// which forgets that shows up as a request without a component, instead of silently counting as chat.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public AIStudio.Tools.Components RuntimeComponent { get; set; } = AIStudio.Tools.Components.CHAT;
|
||||
public AIStudio.Tools.Components RuntimeComponent { get; set; } = AIStudio.Tools.Components.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the assistant plugin running this thread, or empty for any other component.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string RuntimeAssistantName { get; set; } = string.Empty;
|
||||
|
||||
[JsonIgnore]
|
||||
public HashSet<string> RuntimeSelectedToolIds { get; set; } = [];
|
||||
|
||||
@@ -1148,6 +1148,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
this.Logger.LogDebug($"Start processing user input using provider '{this.Provider.InstanceName}' with model '{this.Provider.Model}'.");
|
||||
this.StateHasChanged();
|
||||
this.ChatThread!.RuntimeComponent = Tools.Components.CHAT;
|
||||
this.ChatThread.RuntimeAssistantName = string.Empty;
|
||||
this.ChatThread.SelectedToolIds = [..this.selectedToolIds];
|
||||
this.ChatThread.RuntimeSelectedToolIds = this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds, this.ChatThread.RequiredOutboundDataRestriction.Restriction);
|
||||
await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<ConfigurationOption OptionDescription="@T("Enable spellchecking?")" LabelOn="@T("Spellchecking is enabled")" LabelOff="@T("Spellchecking is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.EnableSpellchecking = updatedState)" OptionHelp="@T("When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections.")"/>
|
||||
<ConfigurationOption OptionDescription="@T("Show details when suspicious content was removed?")" LabelOn="@T("A dialog lists what was removed and explains the attack pattern")" LabelOff="@T("Only a short notification is shown")" State="@(() => this.SettingsManager.ConfigurationData.App.ShowPromptInjectionAlert)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.ShowPromptInjectionAlert = updatedState)" OptionHelp="@T("Shows a dialog listing the removed passages, together with an explanation and an external reference.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ShowPromptInjectionAlert, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSlider T="int" OptionDescription="@T("Request timeout")" Min="@ExternalHttpClientTimeout.MIN_HTTP_CLIENT_TIMEOUT_SECONDS" Max="@ExternalHttpClientTimeout.MAX_HTTP_CLIENT_TIMEOUT_SECONDS" Step="60" Unit="@T("seconds")" Value="@(() => this.SettingsManager.ConfigurationData.App.HttpClientTimeoutSeconds)" ValueUpdate="@(updatedValue => this.SettingsManager.ConfigurationData.App.HttpClientTimeoutSeconds = updatedValue)" OptionHelp="@T("How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.HttpClientTimeoutSeconds, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationOption OptionDescription="@T("Share feature usage with operators of self-hosted servers?")" LabelOn="@T("Operators of self-hosted servers learn which feature sent a request")" LabelOff="@T("No usage details are sent")" State="@(() => this.SettingsManager.ConfigurationData.App.ShareFeatureUsageWithSelfHostedServerOperators)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.ShareFeatureUsageWithSelfHostedServerOperators = updatedState)" OptionHelp="@T("When enabled, requests to self-hosted providers and LiteLLM name the feature that sent them, e.g., the chat, an assistant, or an agent, together with the version of AI Studio and your operating system. This lets the operators of these servers, usually your organization, see which features are used. Cloud providers never receive these details, and AI Studio never sends anything to its developers.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ShareFeatureUsageWithSelfHostedServerOperators, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Check for updates")" SelectedValue="@(() => this.DisplayedUpdateInterval)" Data="@ConfigurationSelectDataFactory.GetManagedUpdateIntervalData(this.DisplayedUpdateInterval)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInterval = selectedValue)" OptionHelp="@this.UpdateIntervalHelp" IsLocked="@this.IsUpdateIntervalLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Update installation method")" SelectedValue="@(() => this.DisplayedUpdateInstallation)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviourData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInstallation = selectedValue)" OptionHelp="@this.UpdateInstallationHelp" IsLocked="@this.IsUpdateInstallationLocked"/>
|
||||
<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.")"/>
|
||||
|
||||
@@ -82,6 +82,7 @@ public partial class Writer : MSGComponentBase
|
||||
do not repeat the task.
|
||||
""",
|
||||
Blocks = [],
|
||||
RuntimeComponent = Tools.Components.WRITER,
|
||||
};
|
||||
|
||||
var time = DateTimeOffset.Now;
|
||||
|
||||
@@ -938,6 +938,22 @@ CONFIG["SETTINGS"] = {}
|
||||
-- The default is 3600 (1 hour).
|
||||
-- CONFIG["SETTINGS"]["DataApp.HttpClientTimeoutSeconds"] = 3600
|
||||
|
||||
-- Configure whether requests to self-hosted servers name the feature of AI Studio which sent them.
|
||||
-- Only self-hosted providers and LiteLLM receive these details; cloud providers never do.
|
||||
-- When set to true, their requests carry a User-Agent such as
|
||||
-- "MindWorkAIStudio/26.10.1 (win-x64) Component/TRANSLATION_ASSISTANT", which lets the operators
|
||||
-- of these servers see which features are used. The default is false: AI Studio then sends no
|
||||
-- User-Agent at all. The Enterprise IT documentation describes what is sent, and what to clarify
|
||||
-- before you switch it on.
|
||||
-- Allowed values are: true, false
|
||||
-- CONFIG["SETTINGS"]["DataApp.ShareFeatureUsageWithSelfHostedServerOperators"] = true
|
||||
--
|
||||
-- Allow users to change this setting locally.
|
||||
-- Allowed values are: true, false
|
||||
-- When set to true, the configured value becomes the organization default,
|
||||
-- but users can still switch it on or off in the app settings.
|
||||
-- CONFIG["SETTINGS"]["DataApp.ShareFeatureUsageWithSelfHostedServerOperators.AllowUserOverride"] = true
|
||||
|
||||
-- Configure additional root certificates for external HTTPS requests.
|
||||
--
|
||||
-- This is intended for managed Linux/Flatpak deployments where organization-internal
|
||||
|
||||
+12
@@ -4725,6 +4725,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1898114759"]
|
||||
-- Select the language for the app.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] = "Wählen Sie die Sprache für die App aus."
|
||||
|
||||
-- When enabled, requests to self-hosted providers and LiteLLM name the feature that sent them, e.g., the chat, an assistant, or an agent, together with the version of AI Studio and your operating system. This lets the operators of these servers, usually your organization, see which features are used. Cloud providers never receive these details, and AI Studio never sends anything to its developers.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907749697"] = "Wenn diese Option aktiviert ist, nennen Anfragen an selbst gehostete Anbieter und LiteLLM die Funktion, von der sie stammen, zum Beispiel den Chat, einen Assistenten oder einen Agenten. Dazu kommen die Version von AI Studio und Ihr Betriebssystem. So sehen die Betreiber dieser Server, in der Regel Ihre Organisation, welche Funktionen genutzt werden. Cloud-Anbieter erhalten diese Angaben nie, und AI Studio sendet niemals etwas an seine Entwickler."
|
||||
|
||||
-- Your organization has disabled update checks and installations.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1909339369"] = "Ihre Organisation hat die Suche nach Updates und deren Installation deaktiviert."
|
||||
|
||||
@@ -4785,6 +4788,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3138812562"]
|
||||
-- Spellchecking is enabled
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Rechtschreibprüfung ist aktiviert"
|
||||
|
||||
-- Operators of self-hosted servers learn which feature sent a request
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3294604676"] = "Betreiber selbst gehosteter Server erfahren, von welcher Funktion eine Anfrage stammt"
|
||||
|
||||
-- External HTTPS certificates
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T348936513"] = "Externe HTTPS-Zertifikate"
|
||||
|
||||
@@ -4809,6 +4815,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3652888444"]
|
||||
-- Show administration settings?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3694781396"] = "Optionen für die Administration anzeigen?"
|
||||
|
||||
-- No usage details are sent
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3702320045"] = "Es werden keine Angaben zur Nutzung gesendet"
|
||||
|
||||
-- Read the Enterprise IT documentation for details.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3705451321"] = "Lesen Sie die Enterprise-IT-Dokumentation für die Details."
|
||||
|
||||
@@ -4830,6 +4839,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"]
|
||||
-- Show details when suspicious content was removed?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4156872850"] = "Details anzeigen, wenn verdächtige Inhalte entfernt wurden?"
|
||||
|
||||
-- Share feature usage with operators of self-hosted servers?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4162855418"] = "Mit Betreibern selbst gehosteter Server teilen, welche Funktionen Sie nutzen?"
|
||||
|
||||
-- Select a transcription provider
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Wählen Sie einen Transkriptionsanbieter aus"
|
||||
|
||||
|
||||
+12
@@ -4725,6 +4725,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1898114759"]
|
||||
-- Select the language for the app.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] = "Select the language for the app."
|
||||
|
||||
-- When enabled, requests to self-hosted providers and LiteLLM name the feature that sent them, e.g., the chat, an assistant, or an agent, together with the version of AI Studio and your operating system. This lets the operators of these servers, usually your organization, see which features are used. Cloud providers never receive these details, and AI Studio never sends anything to its developers.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907749697"] = "When enabled, requests to self-hosted providers and LiteLLM name the feature that sent them, e.g., the chat, an assistant, or an agent, together with the version of AI Studio and your operating system. This lets the operators of these servers, usually your organization, see which features are used. Cloud providers never receive these details, and AI Studio never sends anything to its developers."
|
||||
|
||||
-- Your organization has disabled update checks and installations.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1909339369"] = "Your organization has disabled update checks and installations."
|
||||
|
||||
@@ -4785,6 +4788,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3138812562"]
|
||||
-- Spellchecking is enabled
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Spellchecking is enabled"
|
||||
|
||||
-- Operators of self-hosted servers learn which feature sent a request
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3294604676"] = "Operators of self-hosted servers learn which feature sent a request"
|
||||
|
||||
-- External HTTPS certificates
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T348936513"] = "External HTTPS certificates"
|
||||
|
||||
@@ -4809,6 +4815,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3652888444"]
|
||||
-- Show administration settings?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3694781396"] = "Show administration settings?"
|
||||
|
||||
-- No usage details are sent
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3702320045"] = "No usage details are sent"
|
||||
|
||||
-- Read the Enterprise IT documentation for details.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3705451321"] = "Read the Enterprise IT documentation for details."
|
||||
|
||||
@@ -4830,6 +4839,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"]
|
||||
-- Show details when suspicious content was removed?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4156872850"] = "Show details when suspicious content was removed?"
|
||||
|
||||
-- Share feature usage with operators of self-hosted servers?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4162855418"] = "Share feature usage with operators of self-hosted servers?"
|
||||
|
||||
-- Select a transcription provider
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Select a transcription provider"
|
||||
|
||||
|
||||
@@ -37,6 +37,11 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
/// </summary>
|
||||
private readonly ILogger logger;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the requests of this provider carry the User-Agent of AI Studio, see AppUserAgent.
|
||||
/// </summary>
|
||||
private readonly bool sharesFeatureUsage;
|
||||
|
||||
protected static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = ProviderJsonOptions.OPTIONS;
|
||||
|
||||
/// <summary>
|
||||
@@ -52,6 +57,17 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
this.Provider = provider;
|
||||
this.BaseUri = baseUri;
|
||||
this.HttpClient = ExternalHttpClientTimeout.CreateHttpClient(baseUri, trustPolicy);
|
||||
|
||||
//
|
||||
// Decided once per provider, so that all requests of one provider agree. As the default header
|
||||
// of the client, the User-Agent reaches every request: transcriptions, embeddings, and model
|
||||
// lists name the app this way, while chat requests additionally name their component, see
|
||||
// StreamOpenAICompatibleChatCompletion. The settings are read the same way the client above
|
||||
// reads its timeout:
|
||||
//
|
||||
this.sharesFeatureUsage = AppUserAgent.IsAllowedFor(provider, Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>());
|
||||
if (this.sharesFeatureUsage)
|
||||
AppUserAgent.Apply(this.HttpClient);
|
||||
}
|
||||
|
||||
#region Handling of IProvider, which all providers must implement
|
||||
@@ -1304,6 +1320,18 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters("parallel_tool_calls");
|
||||
|
||||
//
|
||||
// Where the User-Agent is shared, a chat request also names the component which sent it. This
|
||||
// covers both ways to the provider below: the single request, and every round of a tool run.
|
||||
//
|
||||
Action<HttpRequestHeaders>? requestHeadersAction = this.sharesFeatureUsage
|
||||
? headers =>
|
||||
{
|
||||
headersAction?.Invoke(headers);
|
||||
AppUserAgent.ApplyComponent(headers, chatThread);
|
||||
}
|
||||
: headersAction;
|
||||
|
||||
var toolRegistry = Program.SERVICE_PROVIDER.GetService<ToolRegistry>();
|
||||
var toolExecutor = Program.SERVICE_PROVIDER.GetService<ToolExecutor>();
|
||||
var currentAssistantContent = chatThread.Blocks.LastOrDefault(x => x.Role is ChatRole.AI)?.Content as ContentText;
|
||||
@@ -1335,7 +1363,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
{
|
||||
var adapter = new ChatCompletionToolCallingAdapter<TRequest>(requestFactory, systemPrompt, apiParameters,
|
||||
runnableTools.Select(x => ProviderToolAdapters.ToChatCompletionTool(x.Definition, enforcesStrictToolSchemas)).ToList(), mayAskForSequentialToolCalls, runnableTools,
|
||||
(requestDto, requestToken) => this.StreamChatCompletionRequest(requestDto, providerName, requestPath, requestedSecret, headersAction, requestToken),
|
||||
(requestDto, requestToken) => this.StreamChatCompletionRequest(requestDto, providerName, requestPath, requestedSecret, requestHeadersAction, requestToken),
|
||||
ChatCompletionSourceReader.Read<TDelta, TAnnotation>,
|
||||
this.logger);
|
||||
|
||||
@@ -1381,7 +1409,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(Program.ENCRYPTION));
|
||||
|
||||
// Set provider-specific headers:
|
||||
headersAction?.Invoke(request.Headers);
|
||||
requestHeadersAction?.Invoke(request.Headers);
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json");
|
||||
|
||||
@@ -459,6 +459,25 @@ public static class LLMProvidersExtensions
|
||||
_ => false,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Whether the provider runs on a server that the user or their organization operates.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The address of such a server comes from the user: a self-hosted model, or a LiteLLM gateway.
|
||||
/// Only their operators may learn which feature of AI Studio sent a request, see AppUserAgent.
|
||||
/// This is deliberately not derived from IsHostnameNeeded, even though both name the same providers
|
||||
/// today: whether a dialog asks for a hostname is a question of the user interface, and it must
|
||||
/// never silently decide what a server learns about the user.
|
||||
/// </remarks>
|
||||
/// <param name="provider">The provider.</param>
|
||||
/// <returns>True when the provider runs on a self-hosted server; otherwise, false.</returns>
|
||||
public static bool RunsOnSelfHostedServer(this LLMProviders provider) => provider switch
|
||||
{
|
||||
LLMProviders.SELF_HOSTED => true,
|
||||
LLMProviders.LITE_LLM => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
public static bool IsAPIKeyNeeded(this LLMProviders provider, Host host) => provider switch
|
||||
{
|
||||
LLMProviders.OPEN_AI => true,
|
||||
|
||||
@@ -146,6 +146,15 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
|
||||
/// </summary>
|
||||
public int HttpClientTimeoutSeconds { get; set; } = ManagedConfiguration.Register(configSelection, n => n.HttpClientTimeoutSeconds, ExternalHttpClientTimeout.DEFAULT_HTTP_CLIENT_TIMEOUT_SECONDS);
|
||||
|
||||
/// <summary>
|
||||
/// Should requests to self-hosted servers, i.e., self-hosted providers and LiteLLM, name the feature that sent them?
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Whoever operates such a server learns about it, which usually is an organization. Off by default:
|
||||
/// AI Studio then sends no User-Agent at all. Cloud providers never learn about it either way.
|
||||
/// </remarks>
|
||||
public bool ShareFeatureUsageWithSelfHostedServerOperators { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShareFeatureUsageWithSelfHostedServerOperators, false);
|
||||
|
||||
/// <summary>
|
||||
/// Should external HTTP clients trust additional root certificates from a configured PEM bundle?
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
using System.Globalization;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.Metadata;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// The User-Agent which AI Studio sends to self-hosted servers, when the user allows it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It lets the operators of a self-hosted server or a gateway such as LiteLLM tell requests from
|
||||
/// AI Studio apart from other clients, e.g., in their logs, and see which feature sent them.<br/><br/>
|
||||
/// Who receives it is decided in IsAllowedFor, and nowhere else: only self-hosted servers, and only
|
||||
/// when the user or their organization switched on sharing the feature usage. Without that, AI Studio
|
||||
/// sends no User-Agent at all, the same as before this existed. Cloud providers never receive it.
|
||||
/// </remarks>
|
||||
public static class AppUserAgent
|
||||
{
|
||||
private static readonly MetaDataAttribute META_DATA = Assembly.GetExecutingAssembly().GetCustomAttribute<MetaDataAttribute>()!;
|
||||
|
||||
/// <summary>
|
||||
/// The User-Agent naming the app, its version, and the platform, e.g., <c>MindWorkAIStudio/26.9.1 (osx-arm64)</c>.
|
||||
/// </summary>
|
||||
public static readonly string BASE = CreateBase();
|
||||
|
||||
/// <summary>
|
||||
/// Whether requests to the given provider may carry the User-Agent.
|
||||
/// </summary>
|
||||
/// <param name="provider">The provider which receives the requests.</param>
|
||||
/// <param name="settingsManager">The settings, which say whether the user shares the feature usage.</param>
|
||||
/// <returns>True when the provider runs on a self-hosted server and sharing is switched on; otherwise, false.</returns>
|
||||
public static bool IsAllowedFor(LLMProviders provider, SettingsManager settingsManager) =>
|
||||
provider.RunsOnSelfHostedServer() && settingsManager.ConfigurationData.App.ShareFeatureUsageWithSelfHostedServerOperators;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the User-Agent as default header of the given HTTP client.
|
||||
/// </summary>
|
||||
/// <param name="httpClient">The HTTP client, whose requests should carry the User-Agent.</param>
|
||||
public static void Apply(HttpClient httpClient) => httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd(BASE);
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the User-Agent of a single request by one naming the component of the given chat thread.
|
||||
/// </summary>
|
||||
/// <param name="headers">The headers of the request.</param>
|
||||
/// <param name="chatThread">The chat thread, whose component sent the request.</param>
|
||||
public static void ApplyComponent(HttpRequestHeaders headers, ChatThread chatThread)
|
||||
{
|
||||
headers.Remove("User-Agent");
|
||||
headers.TryAddWithoutValidation("User-Agent", ForComponent(chatThread.RuntimeComponent, chatThread.RuntimeAssistantName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the User-Agent naming the given component, e.g., <c>MindWorkAIStudio/26.9.1 (osx-arm64) Component/TRANSLATION_ASSISTANT</c>.
|
||||
/// </summary>
|
||||
/// <param name="component">The component which sends the request.</param>
|
||||
/// <param name="assistantName">The name of the assistant plugin, if any.</param>
|
||||
/// <returns>The User-Agent.</returns>
|
||||
public static string ForComponent(Components component, string assistantName = "")
|
||||
{
|
||||
if (component is Components.NONE)
|
||||
return BASE;
|
||||
|
||||
var userAgent = $"{BASE} Component/{component}";
|
||||
var assistantToken = ToProductToken(assistantName);
|
||||
return string.IsNullOrWhiteSpace(assistantToken)
|
||||
? userAgent
|
||||
: $"{userAgent} Assistant/{assistantToken}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a free-text name into a valid product token of a User-Agent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Header values are ASCII only, and a product token must not contain separators like spaces,
|
||||
/// slashes, or parentheses. Diacritics are dropped (Ü becomes U), everything else is replaced by a dash.
|
||||
/// </remarks>
|
||||
private static string ToProductToken(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return string.Empty;
|
||||
|
||||
const int MAX_LENGTH = 64;
|
||||
var normalized = name.Trim().Replace("ß", "ss").Normalize(NormalizationForm.FormD);
|
||||
var token = new StringBuilder(normalized.Length);
|
||||
foreach (var character in normalized)
|
||||
{
|
||||
if (CharUnicodeInfo.GetUnicodeCategory(character) is UnicodeCategory.NonSpacingMark)
|
||||
continue;
|
||||
|
||||
if (char.IsAsciiLetterOrDigit(character) || character is '.' or '_' or '-')
|
||||
token.Append(character);
|
||||
else if (token.Length > 0 && token[^1] is not '-')
|
||||
token.Append('-');
|
||||
|
||||
if (token.Length >= MAX_LENGTH)
|
||||
break;
|
||||
}
|
||||
|
||||
return token.ToString().TrimEnd('-');
|
||||
}
|
||||
|
||||
private static string CreateBase()
|
||||
{
|
||||
var rid = RIDExtensions.GetCurrentRID().AsMicrosoftRid();
|
||||
return string.IsNullOrWhiteSpace(rid)
|
||||
? $"MindWorkAIStudio/{META_DATA.Version}"
|
||||
: $"MindWorkAIStudio/{META_DATA.Version} ({rid})";
|
||||
}
|
||||
}
|
||||
@@ -303,6 +303,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
// Config: timeout for external HTTP requests
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.HttpClientTimeoutSeconds, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: share the feature usage with the operators of self-hosted servers?
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShareFeatureUsageWithSelfHostedServerOperators, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: custom root certificates for external HTTP requests
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ExternalHttpCustomRootCertificatesEnabled, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ExternalHttpCustomRootCertificateBundlePath, this.Id, settingsTable, dryRun);
|
||||
|
||||
@@ -658,6 +658,9 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
|
||||
Name = threadName,
|
||||
SystemPrompt = systemPrompt,
|
||||
SelectedProvider = provider.Id,
|
||||
|
||||
// Generating and revising assistant plugins is the work of the Assistant Builder:
|
||||
RuntimeComponent = Components.META_ASSISTANT,
|
||||
Blocks =
|
||||
[
|
||||
new()
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
- Added organization-wide management for mailboxes. IT departments can prevent users from adding mailboxes, and they can rule out the less strict choices of where a chat may send data after reading mails.
|
||||
- Added the mail servers of your organization to the setup of a mailbox. IT departments can name them in a configuration, and the mailbox dialog lists them before the public mail providers, with a hint on the username and a link to their instructions.
|
||||
- Added a way for IT departments to allow mailboxes only on the mail servers of your organization. AI Studio then connects to no other server, and the AI no longer reads a mailbox somebody added elsewhere.
|
||||
- Added a setting to share which features of AI Studio you use with the operators of self-hosted AI servers, usually your organization. When you switch it on, requests to self-hosted providers and LiteLLM name the feature that sent them, such as the chat, an assistant, or an agent, together with the version of AI Studio and your operating system. Cloud providers never receive these details, and AI Studio sends nothing to its developers. The setting is off by default, and you find it in the app settings. Thanks, Dominic Neuburg (`donework`), for this contribution.
|
||||
- Added a way for IT departments to let requests to the self-hosted AI servers of your organization name the feature of AI Studio that sent them, either for everybody or as a default that users may change. This shows which features are used. The Enterprise IT documentation explains what is sent and what to clarify before you switch it on, for example, with your works council.
|
||||
- Improved the Read Web Page tool. When you have not allowed the AI to choose web addresses freely, the tool now refuses every address that does not appear in your chat or in what the tools returned, instead of only asking the AI not to read it.
|
||||
- Fixed a warning about the confidence level that stayed in the dialog for adding a local data source, even after you chose an embedding provider that meets it. The dialog now checks again whenever you change either of them, so a warning also appears when you switch to a provider that falls short.
|
||||
- Fixed two confidence levels sharing the same name in the German user interface. Moderate is called "Mäßig" again, and Medium stays "Mittel".
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace AIStudio.Tests.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Checks the User-Agent which AI Studio sends to self-hosted servers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Operators of a gateway such as LiteLLM read it in their logs to see which assistant or agent sent a
|
||||
/// request. It has to stay a valid header value whatever a plugin calls its assistant, because an
|
||||
/// invalid one would either break the request or silently vanish from the logs. Who receives it at all
|
||||
/// is a privacy question: nobody without the consent of the user, and a cloud provider never.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class AppUserAgentTests
|
||||
{
|
||||
[TestCase(LLMProviders.SELF_HOSTED)]
|
||||
[TestCase(LLMProviders.LITE_LLM)]
|
||||
public void ASelfHostedServerReceivesItOnlyWhenSharingIsOn(LLMProviders provider)
|
||||
{
|
||||
var settingsManager = CreateSettingsManager();
|
||||
Assert.That(AppUserAgent.IsAllowedFor(provider, settingsManager), Is.False, "Sharing is off by default, so not even a self-hosted server may learn anything.");
|
||||
|
||||
settingsManager.ConfigurationData.App.ShareFeatureUsageWithSelfHostedServerOperators = true;
|
||||
Assert.That(AppUserAgent.IsAllowedFor(provider, settingsManager), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NoOtherProviderEverReceivesIt()
|
||||
{
|
||||
var settingsManager = CreateSettingsManager();
|
||||
settingsManager.ConfigurationData.App.ShareFeatureUsageWithSelfHostedServerOperators = true;
|
||||
|
||||
//
|
||||
// Listed by exclusion on purpose: a provider added later falls on the safe side, and whoever
|
||||
// wants it to receive the User-Agent has to change this test and say why.
|
||||
//
|
||||
foreach (var provider in Enum.GetValues<LLMProviders>().Except([LLMProviders.SELF_HOSTED, LLMProviders.LITE_LLM]))
|
||||
Assert.That(AppUserAgent.IsAllowedFor(provider, settingsManager), Is.False, $"The provider {provider} must never learn which feature sent a request.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheBaseNamesTheApp()
|
||||
{
|
||||
Assert.That(AppUserAgent.BASE, Does.StartWith("MindWorkAIStudio/"));
|
||||
Assert.That(ProductInfoHeaderValue.TryParse(AppUserAgent.BASE.Split(' ')[0], out _), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ABuiltInComponentIsNamedByItsEnumValue()
|
||||
{
|
||||
var userAgent = AppUserAgent.ForComponent(AIStudio.Tools.Components.TRANSLATION_ASSISTANT);
|
||||
Assert.That(userAgent, Is.EqualTo($"{AppUserAgent.BASE} Component/TRANSLATION_ASSISTANT"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NoComponentKeepsTheBase()
|
||||
{
|
||||
Assert.That(AppUserAgent.ForComponent(AIStudio.Tools.Components.NONE), Is.EqualTo(AppUserAgent.BASE));
|
||||
}
|
||||
|
||||
[TestCase("Übersetzung für Straßen", "Ubersetzung-fur-Strassen")]
|
||||
[TestCase(" My (cool) Assistant/v2 ", "My-cool-Assistant-v2")]
|
||||
[TestCase("already_valid-name.1", "already_valid-name.1")]
|
||||
[TestCase("日本語", "")]
|
||||
public void APluginNameBecomesAValidProductToken(string pluginName, string expectedToken)
|
||||
{
|
||||
var userAgent = AppUserAgent.ForComponent(AIStudio.Tools.Components.DYNAMIC_ASSISTANT, pluginName);
|
||||
var expected = string.IsNullOrEmpty(expectedToken)
|
||||
? $"{AppUserAgent.BASE} Component/DYNAMIC_ASSISTANT"
|
||||
: $"{AppUserAgent.BASE} Component/DYNAMIC_ASSISTANT Assistant/{expectedToken}";
|
||||
|
||||
Assert.That(userAgent, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ARequestHeaderReplacesTheDefault()
|
||||
{
|
||||
using var request = new HttpRequestMessage();
|
||||
request.Headers.UserAgent.ParseAdd(AppUserAgent.BASE);
|
||||
|
||||
AppUserAgent.ApplyComponent(request.Headers, new ChatThread { RuntimeComponent = AIStudio.Tools.Components.EMAIL_ASSISTANT });
|
||||
|
||||
Assert.That(request.Headers.UserAgent.ToString(), Is.EqualTo($"{AppUserAgent.BASE} Component/EMAIL_ASSISTANT"));
|
||||
Assert.That(request.Headers.UserAgent.Count, Is.EqualTo(3), "The header must still parse as product, platform comment, and component.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AThreadWithoutAComponentDoesNotCountAsChat()
|
||||
{
|
||||
using var request = new HttpRequestMessage();
|
||||
AppUserAgent.ApplyComponent(request.Headers, new ChatThread());
|
||||
|
||||
Assert.That(request.Headers.UserAgent.ToString(), Is.EqualTo(AppUserAgent.BASE), "A thread whose sender forgot to name its component must not be counted as chat, nor as anything else.");
|
||||
}
|
||||
|
||||
private static SettingsManager CreateSettingsManager() => new(NullLogger<SettingsManager>.Instance, null!);
|
||||
}
|
||||
@@ -69,5 +69,8 @@ public sealed class ClassicRagStandsBackTests : ToolRegistryTestBase
|
||||
private static ChatThread ThreadSearching(DataSourceRetrievalMode preference) => new()
|
||||
{
|
||||
DataSourceOptions = new() { DisableDataSources = false, AutomaticDataSourceSelection = true, RetrievalMode = preference },
|
||||
|
||||
// A thread names no component until its sender does, and the chat does so before every message:
|
||||
RuntimeComponent = AIStudio.Tools.Components.CHAT,
|
||||
};
|
||||
}
|
||||
@@ -952,3 +952,95 @@ Two things to keep in mind when you prepare the icon:
|
||||
- **Your organization holds the rights.** Whatever icon you ship -- for a provider through
|
||||
`IconPath`, or for the configuration plugin itself through its `icon.lua` -- your organization is
|
||||
responsible for holding the rights to use it.
|
||||
|
||||
## Feature usage statistics (telemetry)
|
||||
|
||||
When your organization runs its own AI servers, for example behind a LiteLLM gateway, you may want
|
||||
to know which features of AI Studio your colleagues use: the chat, the assistants, the agents. AI
|
||||
Studio can name the feature in the `User-Agent` header of every request it sends to these servers.
|
||||
Your servers then log it like any other header; LiteLLM, for example, shows it as a tag of the
|
||||
request. This is off by default, and AI Studio then sends no `User-Agent` at all.
|
||||
|
||||
### What your servers receive
|
||||
|
||||
The `User-Agent` names the app, its version, the platform, and the feature which sent the request:
|
||||
|
||||
| Request sent by | `User-Agent` |
|
||||
|---|---|
|
||||
| The chat | `MindWorkAIStudio/26.10.1 (win-x64) Component/CHAT` |
|
||||
| The translation assistant | `MindWorkAIStudio/26.10.1 (win-x64) Component/TRANSLATION_ASSISTANT` |
|
||||
| The data source selection agent | `MindWorkAIStudio/26.10.1 (win-x64) Component/AGENT_DATA_SOURCE_SELECTION` |
|
||||
| An assistant plugin of your organization | `MindWorkAIStudio/26.10.1 (win-x64) Component/DYNAMIC_ASSISTANT Assistant/Cafe-Menu-Writer-Beta` |
|
||||
| Transcription, embeddings, and model lists | `MindWorkAIStudio/26.10.1 (win-x64)` |
|
||||
|
||||
The platform is one of `win-x64`, `win-arm64`, `linux-x64`, `linux-arm64`, `osx-x64`, and
|
||||
`osx-arm64`. The feature names are the values of the enumeration in
|
||||
`app/MindWork AI Studio/Tools/Components.cs`.
|
||||
|
||||
Assistant plugins are named only when they come with AI Studio, when your organization deployed
|
||||
them, or when it approved them, see
|
||||
[Enterprise approval for assistant plugins](#enterprise-approval-for-assistant-plugins). Users can
|
||||
build assistants for themselves and name them as they like, possibly after a person or a private
|
||||
project; such a name never leaves the device, and the request shows up as
|
||||
`Component/DYNAMIC_ASSISTANT` only. For the header, a name is reduced to ASCII letters, digits,
|
||||
dots, underscores, and dashes, and cut after 64 characters: "Café Menu Writer (Beta)" becomes
|
||||
`Cafe-Menu-Writer-Beta`.
|
||||
|
||||
The `User-Agent` names neither the user nor their device; the platform only says which operating
|
||||
system and processor architecture AI Studio runs on. The request itself carries what it always
|
||||
carries, such as the prompt and the API key; the `User-Agent` adds only which feature sent it.
|
||||
|
||||
### Who receives it
|
||||
|
||||
Only providers of the types "self-hosted" and "LiteLLM" receive it. AI Studio decides by the type
|
||||
of the provider, not by its address: a LiteLLM provider receives it wherever its server runs.
|
||||
|
||||
- **Cloud providers never receive it**, such as OpenAI, Anthropic, Google, or Mistral, whatever the
|
||||
setting says.
|
||||
- **Other services never learn which feature sent a request**, such as ERI servers, Confluence, or
|
||||
web pages.
|
||||
- **AI Studio sends no usage data to its developers**, with or without this setting.
|
||||
|
||||
### What the numbers mean
|
||||
|
||||
- **They count requests, not uses.** A single message in the chat may cause several requests:
|
||||
each round of tool calls is a request of its own, and the classic RAG process lets its agents
|
||||
select and check the data sources before the answer. Some assistants work in several steps, and
|
||||
the batch processing sends one request per file.
|
||||
- **Agents appear under their own name.** When the chat lets an agent select its data sources, the
|
||||
request of the agent shows up as `Component/AGENT_DATA_SOURCE_SELECTION`, not as part of the chat.
|
||||
- **Transcription, embeddings, and model lists name the app only.** Which kind of request it was,
|
||||
your server sees from the endpoint; which feature caused it is not sent.
|
||||
|
||||
### Switching it on
|
||||
|
||||
Switch it on for everybody, without a way to opt out:
|
||||
|
||||
```lua
|
||||
CONFIG["SETTINGS"]["DataApp.ShareFeatureUsageWithSelfHostedServerOperators"] = true
|
||||
```
|
||||
|
||||
Add `AllowUserOverride` to make it the default of your organization, which users may still change
|
||||
in the app settings:
|
||||
|
||||
```lua
|
||||
CONFIG["SETTINGS"]["DataApp.ShareFeatureUsageWithSelfHostedServerOperators"] = true
|
||||
CONFIG["SETTINGS"]["DataApp.ShareFeatureUsageWithSelfHostedServerOperators.AllowUserOverride"] = true
|
||||
```
|
||||
|
||||
Set it to `false` without `AllowUserOverride` to keep everybody from switching it on. Either way,
|
||||
users find the setting in the app settings, together with an explanation of what is sent; when you
|
||||
lock it, they see your value there. A change applies to the requests which follow; an embedding
|
||||
run which is already underway finishes as it started.
|
||||
|
||||
### Before you switch it on
|
||||
|
||||
As long as your server cannot tell the users apart, the numbers say how often each feature is
|
||||
used, and nothing about anybody in particular. This changes as soon as it can: for example, when
|
||||
every user has an API key of their own, such as a virtual key in LiteLLM, or when your logs keep
|
||||
the IP addresses of the devices. The usage data then relates to persons, and data protection law
|
||||
applies to it. In some countries, such as Germany, evaluating it may also require the consent of
|
||||
your works council, because it could be used to monitor the behavior or performance of employees.
|
||||
|
||||
Clarify this before you switch it on, evaluate the numbers per feature rather than per person, and
|
||||
tell your colleagues that their requests name the features they use.
|
||||
Reference in new issue
Block a user