mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-12 20:33:48 +00:00
Added tool calling support (#731)
Co-authored-by: krut_ni <nils.kruthoff@dlr.de> Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
266 files changed
+11186
-740
No files matched your search
+2
-1
@@ -1,3 +1,4 @@
|
||||
namespace AIStudio.Tools.PluginSystem.Assistants;
|
||||
|
||||
public sealed record AssistantChatLaunchConfiguration(string WorkspaceName, Guid? ProviderId, Guid? ProfileId, Guid? ChatTemplateId, IReadOnlyList<Guid>? DataSourceIds);
|
||||
/// <param name="ToolIds">The tools preselected for the chat, or null when the launcher names none.</param>
|
||||
public sealed record AssistantChatLaunchConfiguration(string WorkspaceName, Guid? ProviderId, Guid? ProfileId, Guid? ChatTemplateId, IReadOnlyList<Guid>? DataSourceIds, IReadOnlyList<string>? ToolIds);
|
||||
@@ -10,9 +10,9 @@ public sealed class AssistantPluginAuditService(AssistantAuditAgent auditAgent)
|
||||
/// <summary>
|
||||
/// Runs an assistant plugin audit, optionally falling back to the supplied provider when no audit provider is configured.
|
||||
/// </summary>
|
||||
public async Task<PluginAssistantAudit> RunAuditAsync(PluginAssistants plugin, CancellationToken token = default, Settings.Provider? fallbackProvider = null)
|
||||
public async Task<PluginAssistantAudit> RunAuditAsync(PluginAssistants plugin, Settings.Provider? fallbackProvider = null, CancellationToken token = default)
|
||||
{
|
||||
var result = await auditAgent.AuditAsync(plugin, token, fallbackProvider);
|
||||
var result = await auditAgent.AuditAsync(plugin, fallbackProvider, token);
|
||||
var provider = auditAgent.ProviderSettings;
|
||||
var promptPreview = await plugin.BuildAuditPromptPreviewAsync(token);
|
||||
|
||||
|
||||
+28
-2
@@ -1,6 +1,8 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem.Assistants;
|
||||
|
||||
/// <summary>
|
||||
@@ -65,12 +67,27 @@ public static class DirectChatLauncherLuaWriter
|
||||
plugin.ReadAllLuaFiles().Keys.Any(relativePath => !string.Equals(relativePath, PLUGIN_FILE_NAME, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>
|
||||
/// Writes the complete plugin.lua for the given launcher.
|
||||
/// Writes the complete plugin.lua for an installed launcher whose settings changed.
|
||||
/// </summary>
|
||||
/// <param name="plugin">The installed launcher whose metadata is carried over.</param>
|
||||
/// <param name="definition">The name, title, description, and chat settings the user chose.</param>
|
||||
/// <returns>The plugin.lua content, ready to be validated and written.</returns>
|
||||
public static string Write(PluginAssistants plugin, DirectChatLauncherDefinition definition)
|
||||
public static string Write(PluginAssistants plugin, DirectChatLauncherDefinition definition) =>
|
||||
Write(DirectChatLauncherPluginMetadata.FromPlugin(plugin), definition);
|
||||
|
||||
/// <summary>
|
||||
/// Writes the complete plugin.lua for a launcher.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A launcher is fully described by its metadata plus a flat ASSISTANT table, so this is the
|
||||
/// whole file rather than a starting point. The Assistant Builder uses that: for a launcher it
|
||||
/// asks a model for the texts only and writes the file itself, because there is nothing left
|
||||
/// for a model to decide.
|
||||
/// </remarks>
|
||||
/// <param name="plugin">The metadata of the launcher, either carried over or newly chosen.</param>
|
||||
/// <param name="definition">The name, title, description, and chat settings the user chose.</param>
|
||||
/// <returns>The plugin.lua content, ready to be validated and written.</returns>
|
||||
public static string Write(DirectChatLauncherPluginMetadata plugin, DirectChatLauncherDefinition definition)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
@@ -173,6 +190,15 @@ public static class DirectChatLauncherLuaWriter
|
||||
builder.AppendLine(" },");
|
||||
}
|
||||
|
||||
if (definition.Launch.ToolIds is { Count: > 0 } toolIds)
|
||||
{
|
||||
builder.AppendLine(" [\"ToolIds\"] = {");
|
||||
foreach (var toolId in toolIds)
|
||||
builder.AppendLine($" {LuaTools.ToLuaStringLiteral(toolId)},");
|
||||
|
||||
builder.AppendLine(" },");
|
||||
}
|
||||
|
||||
builder.Append('}');
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
namespace AIStudio.Tools.PluginSystem.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// The plugin metadata a direct chat launcher carries beyond its chat settings.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An installed launcher keeps these in its plugin.lua, and editing one carries them over. A
|
||||
/// launcher the Assistant Builder is about to create has no file yet, so its metadata comes from
|
||||
/// the Builder's defaults instead. Both paths end in the same writer, which is where the two meet.
|
||||
/// </remarks>
|
||||
/// <param name="Id">The plugin ID, which stays with the plugin for its whole life.</param>
|
||||
/// <param name="Version">The plugin version, as it appears in the Lua file.</param>
|
||||
/// <param name="Authors">The authors of the plugin.</param>
|
||||
/// <param name="SupportContact">Where users turn with questions about this plugin.</param>
|
||||
/// <param name="SourceURL">Where the plugin comes from.</param>
|
||||
/// <param name="Categories">The categories this plugin belongs to.</param>
|
||||
/// <param name="TargetGroups">The target groups this plugin is meant for.</param>
|
||||
/// <param name="IsMaintained">Whether the plugin is still maintained.</param>
|
||||
/// <param name="DeprecationMessage">What users are told when the plugin is deprecated.</param>
|
||||
/// <param name="IsAssistantBuilderGenerated">Whether the Assistant Builder created this plugin.</param>
|
||||
public sealed record DirectChatLauncherPluginMetadata(Guid Id, string Version, IReadOnlyList<string> Authors, string SupportContact, string SourceURL,
|
||||
IReadOnlyList<PluginCategory> Categories, IReadOnlyList<PluginTargetGroup> TargetGroups, bool IsMaintained, string DeprecationMessage, bool IsAssistantBuilderGenerated)
|
||||
{
|
||||
/// <summary>
|
||||
/// Takes the metadata of an installed launcher for the case where one is edited.
|
||||
/// </summary>
|
||||
public static DirectChatLauncherPluginMetadata FromPlugin(PluginAssistants plugin) => new(plugin.Id, plugin.Version.ToString(), plugin.Authors,
|
||||
plugin.SupportContact, plugin.SourceURL, plugin.Categories, plugin.TargetGroups, plugin.IsMaintained, plugin.DeprecationMessage, plugin.IsAssistantBuilderGenerated);
|
||||
}
|
||||
@@ -34,6 +34,18 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
|
||||
public string SystemPrompt { get; private set; } = string.Empty;
|
||||
public string SubmitText { get; private set; } = string.Empty;
|
||||
public bool AllowProfiles { get; private set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// The tools this assistant runs with, when its plugin names any.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null means the plugin says nothing about tools, and the user picks them as in any other
|
||||
/// assistant. A list takes that choice away: the assistant then runs with exactly these tools,
|
||||
/// which is what an author who tested their assistant with them wants. It is a wish, not a
|
||||
/// permission — a tool switched off in the settings, or one the selected provider is not
|
||||
/// trusted enough to receive, stays out of reach either way.
|
||||
/// </remarks>
|
||||
public IReadOnlyList<string>? AssistantToolIds { get; private set; }
|
||||
public bool HasEmbeddedProfileSelection { get; private set; }
|
||||
public bool HasCustomPromptBuilder => this.buildPromptFunction is not null;
|
||||
public bool IsAssistantBuilderGenerated { get; private set; }
|
||||
@@ -72,6 +84,7 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
|
||||
this.SystemPrompt = string.Empty;
|
||||
this.SubmitText = string.Empty;
|
||||
this.AllowProfiles = true;
|
||||
this.AssistantToolIds = null;
|
||||
this.HasEmbeddedProfileSelection = false;
|
||||
this.IsAssistantBuilderGenerated = false;
|
||||
this.HasDeploymentManagementMetadata = false;
|
||||
@@ -138,6 +151,9 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryReadOptionalToolIds(assistantTable, out var assistantToolIds, out message))
|
||||
return false;
|
||||
|
||||
if (assistantTable.TryGetValue("BuildPrompt", out var buildPromptValue))
|
||||
{
|
||||
if (buildPromptValue.TryRead<LuaFunction>(out var buildPrompt))
|
||||
@@ -152,6 +168,7 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
|
||||
this.SystemPrompt = BuildSecureSystemPrompt(rawSystemPrompt);
|
||||
this.SubmitText = assistantSubmitText;
|
||||
this.AllowProfiles = assistantAllowProfiles;
|
||||
this.AssistantToolIds = assistantToolIds;
|
||||
|
||||
// Ensure that the UI table exists nested in the ASSISTANT table and is a valid Lua table:
|
||||
if (!assistantTable.TryGetValue("UI", out var uiVal) || !uiVal.TryRead<LuaTable>(out var uiTable))
|
||||
@@ -226,10 +243,11 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
|
||||
if (!TryReadOptionalGuid(assistantTable, "ProviderId", false, out var providerId, out message) ||
|
||||
!TryReadOptionalGuid(assistantTable, "ProfileId", true, out var profileId, out message) ||
|
||||
!TryReadOptionalGuid(assistantTable, "ChatTemplateId", true, out var chatTemplateId, out message) ||
|
||||
!TryReadOptionalDataSourceIds(assistantTable, out var dataSourceIds, out message))
|
||||
!TryReadOptionalDataSourceIds(assistantTable, out var dataSourceIds, out message) ||
|
||||
!TryReadOptionalToolIds(assistantTable, out var toolIds, out message))
|
||||
return false;
|
||||
|
||||
this.ChatLaunchConfiguration = new(workspaceName, providerId, profileId, chatTemplateId, dataSourceIds);
|
||||
this.ChatLaunchConfiguration = new(workspaceName, providerId, profileId, chatTemplateId, dataSourceIds, toolIds);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -291,6 +309,48 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the tools an assistant names: the ones a launcher preselects for its chat, or the ones
|
||||
/// the assistant itself runs with.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Unlike the data sources, these are plain tool IDs rather than GUIDs, and an ID unknown to
|
||||
/// this installation is not an error: a plugin may name a tool that arrives with another plugin
|
||||
/// which is not installed yet. Whoever runs the tools drops what they cannot offer.
|
||||
/// </remarks>
|
||||
private static bool TryReadOptionalToolIds(LuaTable assistantTable, out IReadOnlyList<string>? toolIds, out string message)
|
||||
{
|
||||
toolIds = null;
|
||||
message = string.Empty;
|
||||
|
||||
if (!assistantTable.TryGetValue("ToolIds", out var toolIdsValue))
|
||||
return true;
|
||||
|
||||
if (!toolIdsValue.TryRead<LuaTable>(out var toolIdsTable) || toolIdsTable.ArrayLength == 0)
|
||||
{
|
||||
message = TB("The ASSISTANT table contains invalid ToolIds. Expected a non-empty list of unique, non-empty tool IDs.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var parsedIds = new List<string>(toolIdsTable.ArrayLength);
|
||||
var uniqueIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
for (var index = 1; index <= toolIdsTable.ArrayLength; index++)
|
||||
{
|
||||
if (!toolIdsTable[index].TryRead<string>(out var toolId) ||
|
||||
string.IsNullOrWhiteSpace(toolId) ||
|
||||
!uniqueIds.Add(toolId.Trim()))
|
||||
{
|
||||
message = TB("The ASSISTANT table contains invalid ToolIds. Expected a non-empty list of unique, non-empty tool IDs.");
|
||||
return false;
|
||||
}
|
||||
|
||||
parsedIds.Add(toolId.Trim());
|
||||
}
|
||||
|
||||
toolIds = parsedIds.ToImmutableArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<string?> TryBuildPromptAsync(LuaTable input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this.buildPromptFunction is null)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Globalization;
|
||||
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Services;
|
||||
@@ -205,6 +207,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryValidateMinimumProviderConfidenceConfiguration(settingsTable, out message))
|
||||
return false;
|
||||
|
||||
this.DeclaredSettingsCount = CountDeclaredSettings(settingsTable);
|
||||
|
||||
// Config: check for updates, and if so, how often?
|
||||
@@ -258,6 +263,21 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
// Config: global voice recording shortcut
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShortcutVoiceRecording, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: global tool availability
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.EnableTools, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.DisabledToolIds, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: minimum provider confidence per tool
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.MinimumProviderConfidenceByToolId, this.Id, settingsTable, dryRun);
|
||||
|
||||
//
|
||||
// Config: settings of the individual tools, keyed by tool and field. Two tables rather
|
||||
// than a property per setting, so that tools an administrator's AI Studio does not know
|
||||
// at compile time — the ones plugin authors define — can be configured just the same.
|
||||
//
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.LockedToolSettings, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.DefaultToolSettings, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: timeout for external HTTP requests
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.HttpClientTimeoutSeconds, this.Id, settingsTable, dryRun);
|
||||
|
||||
@@ -371,6 +391,37 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryValidateMinimumProviderConfidenceConfiguration(LuaTable settingsTable, out string message)
|
||||
{
|
||||
const string SETTING_NAME = "DataTools.MinimumProviderConfidenceByToolId";
|
||||
message = string.Empty;
|
||||
if (!settingsTable.TryGetValue(SETTING_NAME, out var configuredValue))
|
||||
return true;
|
||||
|
||||
if (configuredValue.Type is not LuaValueType.Table || !configuredValue.TryRead<LuaTable>(out var configuredTable))
|
||||
{
|
||||
message = $"The setting '{SETTING_NAME}' must be a table of tool IDs and confidence levels.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var previousKey = LuaValue.Nil;
|
||||
while (configuredTable.TryGetNext(previousKey, out var pair))
|
||||
{
|
||||
previousKey = pair.Key;
|
||||
if (!pair.Key.TryRead<string>(out var toolId) || string.IsNullOrWhiteSpace(toolId) ||
|
||||
!pair.Value.TryRead<string>(out var configuredLevel) ||
|
||||
!Enum.TryParse<ConfidenceLevel>(configuredLevel, true, out var confidenceLevel) ||
|
||||
!Enum.IsDefined(confidenceLevel) ||
|
||||
confidenceLevel is ConfidenceLevel.UNKNOWN)
|
||||
{
|
||||
message = $"The setting '{SETTING_NAME}' contains an invalid tool ID or confidence level. Allowed confidence levels are NONE, UNTRUSTED, VERY_LOW, LOW, MODERATE, MEDIUM, and HIGH.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void TryProcessEnterpriseApprovedAssistantPlugins(LuaTable settingsTable, bool dryRun)
|
||||
{
|
||||
if (!ManagedConfiguration.TryGet(x => x.AssistantPluginAudit, x => x.EnterpriseApprovedPlugins, out ConfigMeta<DataAssistantPluginAudit, IList<DataAssistantPluginEnterpriseApproval>> configMeta))
|
||||
|
||||
@@ -83,7 +83,7 @@ public static partial class PluginFactory
|
||||
}
|
||||
|
||||
var pluginPath = Path.GetDirectoryName(pluginMainFile)!;
|
||||
var plugin = await Load(pluginPath, code, cancellationToken);
|
||||
var plugin = await Load(pluginPath, code, cancellationToken: cancellationToken);
|
||||
|
||||
switch (plugin)
|
||||
{
|
||||
@@ -356,13 +356,13 @@ public static partial class PluginFactory
|
||||
|
||||
/// <param name="pluginPath">The directory the plugin is located in, or null when the code has no directory yet.</param>
|
||||
/// <param name="code">The Lua code of the plugin's main file.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for running the Lua code.</param>
|
||||
/// <param name="allowedBaseDirectory">
|
||||
/// The directory the plugin path must be nested in. Without it, the installed plugins directory
|
||||
/// is used. Validating a plugin before its installation needs this, because the plugin lives in
|
||||
/// a staging directory at that point and could not load any of its own Lua modules otherwise.
|
||||
/// The directory the plugin path must be nested in. Without it, the installed plugins directory
|
||||
/// is used. Validating a plugin before its installation needs this, because the plugin lives in
|
||||
/// a staging directory at that point and could not load any of its own Lua modules otherwise.
|
||||
/// </param>
|
||||
public static async Task<PluginBase> Load(string? pluginPath, string code, CancellationToken cancellationToken = default, string? allowedBaseDirectory = null)
|
||||
/// <param name="cancellationToken">Cancellation token for running the Lua code.</param>
|
||||
public static async Task<PluginBase> Load(string? pluginPath, string code, string? allowedBaseDirectory = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if(ForbiddenPlugins.Check(code) is { IsForbidden: true } forbiddenState)
|
||||
return new NoPlugin($"This plugin is forbidden: {forbiddenState.Message}");
|
||||
|
||||
@@ -179,7 +179,7 @@ public static partial class PluginFactory
|
||||
}
|
||||
|
||||
var code = await File.ReadAllTextAsync(pluginMainFile, Encoding.UTF8, cancellationToken);
|
||||
var plugin = await Load(meta.LocalPath, code, cancellationToken);
|
||||
var plugin = await Load(meta.LocalPath, code, cancellationToken: cancellationToken);
|
||||
plugin.PluginPath = meta.LocalPath;
|
||||
if (plugin is NoPlugin noPlugin)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user