mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-10 15:13:49 +00:00
Added direct-chat launchers for assistant plugins and a dialog to reconfigure them (#935)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
f03c2d1c88
commit
a4c35fc2f2
44 files changed
+2615
-219
No files matched your search
+3
@@ -0,0 +1,3 @@
|
||||
namespace AIStudio.Tools.PluginSystem.Assistants;
|
||||
|
||||
public sealed record AssistantChatLaunchConfiguration(string WorkspaceName, Guid? ProviderId, Guid? ProfileId, Guid? ChatTemplateId, IReadOnlyList<Guid>? DataSourceIds);
|
||||
@@ -7,66 +7,88 @@ public class AssistantComponentFactory
|
||||
{
|
||||
private static readonly ILogger<AssistantComponentFactory> LOGGER = Program.LOGGER_FACTORY.CreateLogger<AssistantComponentFactory>();
|
||||
|
||||
public static IAssistantComponent CreateComponent(
|
||||
AssistantComponentType type,
|
||||
Dictionary<string, object> props,
|
||||
List<IAssistantComponent> children)
|
||||
public static IAssistantComponent CreateComponent(AssistantComponentType type, Dictionary<string, object> props, List<IAssistantComponent> children)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case AssistantComponentType.FORM:
|
||||
return new AssistantForm { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.TEXT_AREA:
|
||||
return new AssistantTextArea { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.BUTTON:
|
||||
return new AssistantButton { Props = props, Children = children};
|
||||
|
||||
case AssistantComponentType.BUTTON_GROUP:
|
||||
return new AssistantButtonGroup { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.DROPDOWN:
|
||||
return new AssistantDropdown { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.PROVIDER_SELECTION:
|
||||
return new AssistantProviderSelection { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.PROFILE_SELECTION:
|
||||
return new AssistantProfileSelection { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.SWITCH:
|
||||
return new AssistantSwitch { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.HEADING:
|
||||
return new AssistantHeading { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.TEXT:
|
||||
return new AssistantText { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.LIST:
|
||||
return new AssistantList { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.WEB_CONTENT_READER:
|
||||
return new AssistantWebContentReader { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.FILE_CONTENT_READER:
|
||||
return new AssistantFileContentReader { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.FILE_ATTACHMENTS:
|
||||
return new AssistantFileAttachment { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.IMAGE:
|
||||
return new AssistantImage { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.COLOR_PICKER:
|
||||
return new AssistantColorPicker { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.DATE_PICKER:
|
||||
return new AssistantDatePicker { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.DATE_RANGE_PICKER:
|
||||
return new AssistantDateRangePicker { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.TIME_PICKER:
|
||||
return new AssistantTimePicker { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.LAYOUT_ITEM:
|
||||
return new AssistantItem { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.LAYOUT_GRID:
|
||||
return new AssistantGrid { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.LAYOUT_PAPER:
|
||||
return new AssistantPaper { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.LAYOUT_STACK:
|
||||
return new AssistantStack { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.LAYOUT_ACCORDION:
|
||||
return new AssistantAccordion { Props = props, Children = children };
|
||||
|
||||
case AssistantComponentType.LAYOUT_ACCORDION_SECTION:
|
||||
return new AssistantAccordionSection { Props = props, Children = children };
|
||||
|
||||
default:
|
||||
LOGGER.LogError($"Unknown assistant component type!\n{type} is not a supported assistant component type");
|
||||
throw new Exception($"Unknown assistant component type: {type}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace AIStudio.Tools.PluginSystem.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// Everything a user may change about an installed direct chat launcher.
|
||||
/// </summary>
|
||||
/// <param name="PluginName">The plugin name, shown on the plugins page.</param>
|
||||
/// <param name="Title">The assistant title, shown on the tile.</param>
|
||||
/// <param name="Description">The description, used for both the plugin and the assistant.</param>
|
||||
/// <param name="Launch">The workspace and the chat settings the tile starts its chat with.</param>
|
||||
public sealed record DirectChatLauncherDefinition(string PluginName, string Title, string Description, AssistantChatLaunchConfiguration Launch);
|
||||
@@ -0,0 +1,213 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// Writes the complete plugin.lua of a direct chat launcher from its metadata and the settings a
|
||||
/// user chose.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A launcher needs no LLM to be changed: it has no system prompt, no UI, and no prompt builder.
|
||||
/// The plugin loader stops reading those fields as soon as a launch behavior is present, so a
|
||||
/// launcher is fully described by its top-level metadata plus a flat ASSISTANT table. That makes a
|
||||
/// canonical rewrite lossless in behavior, which is what this writer produces.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It is not lossless in text: comments, formatting, and anything the file carries beyond that
|
||||
/// shape are gone afterward. Callers must therefore check both CanRewrite and IsCanonicalSource
|
||||
/// before offering the mechanical editing path, and fall back to the code editor or the AI revision
|
||||
/// otherwise.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class DirectChatLauncherLuaWriter
|
||||
{
|
||||
private const string PLUGIN_FILE_NAME = "plugin.lua";
|
||||
|
||||
//
|
||||
// The plugin loader rejects empty authors, categories, and target groups. A plugin that is
|
||||
// running should have all of them, but a defective one must not turn into a file that cannot be
|
||||
// loaded back, hence these fallbacks. They mirror what the Assistant Builder generates.
|
||||
//
|
||||
private const string FALLBACK_AUTHOR = "MindWork AI - Assistant Builder";
|
||||
private const string FALLBACK_SUPPORT_CONTACT = "mailto:info@mindwork.ai";
|
||||
private const string FALLBACK_SOURCE_URL = "https://github.com/MindWorkAI/AI-Studio";
|
||||
private const string FALLBACK_CATEGORY = nameof(PluginCategory.CORE);
|
||||
private const string FALLBACK_TARGET_GROUP = nameof(PluginTargetGroup.EVERYONE);
|
||||
|
||||
//
|
||||
// An inline icon or a companion file would be dropped by a canonical rewrite, and neither is
|
||||
// recoverable from the loaded plugin: the icon is kept as a data URL, and companion files are
|
||||
// pulled in by Lua itself.
|
||||
//
|
||||
private static readonly Regex NON_CANONICAL_CONTENT = new(@"\bICON_SVG\b|\brequire\s*\(", RegexOptions.CultureInvariant);
|
||||
|
||||
/// <summary>
|
||||
/// Whether this plugin is a locally managed launcher whose settings a user may edit at all.
|
||||
/// This check reads no files, so it is safe to call while rendering.
|
||||
/// </summary>
|
||||
public static bool CanRewrite(PluginAssistants plugin) =>
|
||||
plugin is { StartsChatDirectly: true, IsInternal: false, IsManagedByConfigServer: false } &&
|
||||
!string.IsNullOrWhiteSpace(plugin.PluginPath);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the current plugin.lua holds nothing a canonical rewrite would throw away.
|
||||
/// </summary>
|
||||
/// <param name="currentLua">The current plugin.lua content.</param>
|
||||
public static bool IsCanonicalSource(string currentLua) => !string.IsNullOrWhiteSpace(currentLua) && !NON_CANONICAL_CONTENT.IsMatch(currentLua);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the plugin directory holds a single plugin.lua and no companion Lua files.
|
||||
/// This one touches the file system, so keep it out of render paths.
|
||||
/// </summary>
|
||||
public static bool HasCompanionLuaFiles(PluginAssistants plugin) =>
|
||||
plugin.ReadAllLuaFiles().Keys.Any(relativePath => !string.Equals(relativePath, PLUGIN_FILE_NAME, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>
|
||||
/// Writes the complete plugin.lua for the given launcher.
|
||||
/// </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)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.AppendLine("--[[");
|
||||
builder.AppendLine(" This direct chat launcher is maintained by AI Studio: its settings dialog rewrites this");
|
||||
builder.AppendLine(" file as a whole. Editing it by hand works, but the next change made through the dialog");
|
||||
builder.AppendLine(" replaces everything below, including comments and formatting.");
|
||||
builder.AppendLine("]]");
|
||||
builder.AppendLine();
|
||||
|
||||
builder.AppendLine("-- The ID for this plugin:");
|
||||
builder.AppendLine($"ID = \"{plugin.Id}\"");
|
||||
builder.AppendLine();
|
||||
|
||||
builder.AppendLine("-- The name of the plugin:");
|
||||
builder.AppendLine($"NAME = \"{Escape(definition.PluginName)}\"");
|
||||
builder.AppendLine();
|
||||
|
||||
builder.AppendLine("-- The description of the plugin:");
|
||||
builder.AppendLine($"DESCRIPTION = \"{Escape(definition.Description)}\"");
|
||||
builder.AppendLine();
|
||||
|
||||
builder.AppendLine("-- The version of the plugin:");
|
||||
builder.AppendLine($"VERSION = \"{plugin.Version}\"");
|
||||
builder.AppendLine();
|
||||
|
||||
builder.AppendLine("-- The type of the plugin:");
|
||||
builder.AppendLine($"TYPE = \"{nameof(PluginType.ASSISTANT)}\"");
|
||||
builder.AppendLine();
|
||||
|
||||
builder.AppendLine("-- The authors of the plugin:");
|
||||
builder.AppendLine($"AUTHORS = {WriteStringList(plugin.Authors, FALLBACK_AUTHOR)}");
|
||||
builder.AppendLine();
|
||||
|
||||
builder.AppendLine("-- The support contact for the plugin:");
|
||||
builder.AppendLine($"SUPPORT_CONTACT = \"{Escape(ValueOrFallback(plugin.SupportContact, FALLBACK_SUPPORT_CONTACT))}\"");
|
||||
builder.AppendLine();
|
||||
|
||||
builder.AppendLine("-- The source URL for the plugin:");
|
||||
builder.AppendLine($"SOURCE_URL = \"{Escape(ValueOrFallback(plugin.SourceURL, FALLBACK_SOURCE_URL))}\"");
|
||||
builder.AppendLine();
|
||||
|
||||
builder.AppendLine("-- The categories for the plugin:");
|
||||
builder.AppendLine($"CATEGORIES = {WriteEnumList(plugin.Categories, FALLBACK_CATEGORY)}");
|
||||
builder.AppendLine();
|
||||
|
||||
builder.AppendLine("-- The target groups for the plugin:");
|
||||
builder.AppendLine($"TARGET_GROUPS = {WriteEnumList(plugin.TargetGroups, FALLBACK_TARGET_GROUP)}");
|
||||
builder.AppendLine();
|
||||
|
||||
builder.AppendLine("-- The flag for whether the plugin is maintained:");
|
||||
builder.AppendLine($"IS_MAINTAINED = {WriteBoolean(plugin.IsMaintained)}");
|
||||
builder.AppendLine();
|
||||
|
||||
builder.AppendLine("-- When the plugin is deprecated, this message will be shown to users:");
|
||||
builder.AppendLine($"DEPRECATION_MESSAGE = \"{Escape(plugin.DeprecationMessage)}\"");
|
||||
builder.AppendLine();
|
||||
|
||||
builder.AppendLine("-- Enterprise-managed assistants cannot be revised with AI. Keep false for locally managed plugins:");
|
||||
builder.AppendLine("DEPLOYED_USING_CONFIG_SERVER = false");
|
||||
builder.AppendLine();
|
||||
|
||||
//
|
||||
// This metadata marks assistants the Builder created and must not appear on manually
|
||||
// authored plugins, so it is carried over rather than always written:
|
||||
//
|
||||
if (plugin.IsAssistantBuilderGenerated)
|
||||
{
|
||||
builder.AppendLine("-- This assistant was created by the AI Studio Assistant Builder:");
|
||||
builder.AppendLine("AI_STUDIO_ASSISTANT_BUILDER = {Generated = true, SchemaVersion = 1}");
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
builder.AppendLine("-- The tile opens a chat directly, hence it needs no system prompt, no submit text, and no UI:");
|
||||
builder.AppendLine("ASSISTANT = {");
|
||||
builder.AppendLine($" [\"Title\"] = \"{Escape(definition.Title)}\",");
|
||||
builder.AppendLine($" [\"Description\"] = \"{Escape(definition.Description)}\",");
|
||||
builder.AppendLine($" [\"LaunchBehavior\"] = \"{nameof(AssistantPluginLaunchBehavior.OPEN_WORKSPACE_CHAT_BY_NAME)}\",");
|
||||
builder.AppendLine($" [\"WorkspaceName\"] = \"{Escape(definition.Launch.WorkspaceName.Trim())}\",");
|
||||
|
||||
//
|
||||
// Omitted IDs mean "use the chat defaults", while an empty GUID explicitly selects no
|
||||
// profile or no chat template. An empty provider GUID has no such meaning and is invalid:
|
||||
//
|
||||
if (definition.Launch.ProviderId is { } providerId && providerId != Guid.Empty)
|
||||
builder.AppendLine($" [\"ProviderId\"] = \"{providerId}\",");
|
||||
|
||||
if (definition.Launch.ProfileId is { } profileId)
|
||||
builder.AppendLine($" [\"ProfileId\"] = \"{profileId}\",");
|
||||
|
||||
if (definition.Launch.ChatTemplateId is { } chatTemplateId)
|
||||
builder.AppendLine($" [\"ChatTemplateId\"] = \"{chatTemplateId}\",");
|
||||
|
||||
if (definition.Launch.DataSourceIds is { Count: > 0 } dataSourceIds)
|
||||
{
|
||||
builder.AppendLine(" [\"DataSourceIds\"] = {");
|
||||
foreach (var dataSourceId in dataSourceIds)
|
||||
builder.AppendLine($" \"{dataSourceId}\",");
|
||||
|
||||
builder.AppendLine(" },");
|
||||
}
|
||||
|
||||
builder.Append('}');
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string WriteStringList(IReadOnlyList<string> values, string fallback)
|
||||
{
|
||||
var usableValues = values.Where(value => !string.IsNullOrWhiteSpace(value)).Select(value => value.Trim()).ToArray();
|
||||
if (usableValues.Length == 0)
|
||||
usableValues = [fallback];
|
||||
|
||||
return $"{{{string.Join(", ", usableValues.Select(value => $"\"{Escape(value)}\""))}}}";
|
||||
}
|
||||
|
||||
private static string WriteEnumList<T>(IReadOnlyList<T> values, string fallback) where T : struct, Enum
|
||||
{
|
||||
var names = values.Select(value => Enum.GetName(value) ?? string.Empty).Where(name => !string.IsNullOrWhiteSpace(name)).ToArray();
|
||||
if (names.Length == 0)
|
||||
names = [fallback];
|
||||
|
||||
return $"{{{string.Join(", ", names.Select(name => $"\"{name}\""))}}}";
|
||||
}
|
||||
|
||||
private static string WriteBoolean(bool value) => value ? "true" : "false";
|
||||
|
||||
private static string ValueOrFallback(string value, string fallback) => string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
|
||||
|
||||
//
|
||||
// Titles, descriptions, and workspace names are free text. Lua has no raw newlines inside
|
||||
// quoted strings, so everything that would break out of one is escaped. The backslash must come
|
||||
// first, otherwise the escapes added afterwards would be escaped again:
|
||||
//
|
||||
private static string Escape(string value) => value
|
||||
.Replace("\\", "\\\\", StringComparison.Ordinal)
|
||||
.Replace("\"", "\\\"", StringComparison.Ordinal)
|
||||
.Replace("\r", "\\r", StringComparison.Ordinal)
|
||||
.Replace("\n", "\\n", StringComparison.Ordinal)
|
||||
.Replace("\t", "\\t", StringComparison.Ordinal);
|
||||
}
|
||||
@@ -40,8 +40,8 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
|
||||
public bool HasDeploymentManagementMetadata { get; private set; }
|
||||
public bool IsManagedByConfigServer { get; private set; }
|
||||
public AssistantPluginLaunchBehavior LaunchBehavior { get; private set; }
|
||||
public string LaunchWorkspaceName { get; private set; } = string.Empty;
|
||||
public bool StartsChatDirectly => this.LaunchBehavior is AssistantPluginLaunchBehavior.OPEN_WORKSPACE_CHAT_BY_NAME;
|
||||
public AssistantChatLaunchConfiguration? ChatLaunchConfiguration { get; private set; }
|
||||
public bool StartsChatDirectly => this.ChatLaunchConfiguration is not null;
|
||||
public const int TEXT_AREA_MAX_VALUE = 524288;
|
||||
|
||||
private LuaFunction? buildPromptFunction;
|
||||
@@ -65,13 +65,20 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
|
||||
private bool TryProcessAssistant(out string message)
|
||||
{
|
||||
message = string.Empty;
|
||||
this.RootComponent = null;
|
||||
this.AssistantTitle = string.Empty;
|
||||
this.AssistantDescription = string.Empty;
|
||||
this.RawSystemPrompt = string.Empty;
|
||||
this.SystemPrompt = string.Empty;
|
||||
this.SubmitText = string.Empty;
|
||||
this.AllowProfiles = true;
|
||||
this.HasEmbeddedProfileSelection = false;
|
||||
this.IsAssistantBuilderGenerated = false;
|
||||
this.HasDeploymentManagementMetadata = false;
|
||||
this.IsManagedByConfigServer = false;
|
||||
this.buildPromptFunction = null;
|
||||
this.LaunchBehavior = AssistantPluginLaunchBehavior.NONE;
|
||||
this.LaunchWorkspaceName = string.Empty;
|
||||
this.ChatLaunchConfiguration = null;
|
||||
|
||||
this.RegisterLuaHelpers();
|
||||
this.TryReadAssistantBuilderMetadata();
|
||||
@@ -97,6 +104,18 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
|
||||
message = TB("The provided ASSISTANT lua table does not contain a valid description.");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.AssistantTitle = assistantTitle;
|
||||
this.AssistantDescription = assistantDescription;
|
||||
|
||||
if (!this.TryReadLaunchConfiguration(assistantTable, out var launchConfigIssue))
|
||||
{
|
||||
message = launchConfigIssue;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.StartsChatDirectly)
|
||||
return true;
|
||||
|
||||
if (!assistantTable.TryGetValue("SystemPrompt", out var assistantSystemPromptValue) ||
|
||||
!assistantSystemPromptValue.TryRead<string>(out var assistantSystemPrompt))
|
||||
@@ -129,19 +148,11 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
|
||||
|
||||
var rawSystemPrompt = assistantSystemPrompt.Trim();
|
||||
|
||||
this.AssistantTitle = assistantTitle;
|
||||
this.AssistantDescription = assistantDescription;
|
||||
this.RawSystemPrompt = rawSystemPrompt;
|
||||
this.SystemPrompt = BuildSecureSystemPrompt(rawSystemPrompt);
|
||||
this.SubmitText = assistantSubmitText;
|
||||
this.AllowProfiles = assistantAllowProfiles;
|
||||
|
||||
if (!this.TryReadLaunchConfiguration(assistantTable, out var launchConfigIssue))
|
||||
{
|
||||
message = launchConfigIssue;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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))
|
||||
{
|
||||
@@ -212,7 +223,13 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
|
||||
return false;
|
||||
}
|
||||
|
||||
this.LaunchWorkspaceName = workspaceName;
|
||||
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))
|
||||
return false;
|
||||
|
||||
this.ChatLaunchConfiguration = new(workspaceName, providerId, profileId, chatTemplateId, dataSourceIds);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -222,6 +239,58 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadOptionalGuid(LuaTable assistantTable, string fieldName, bool allowEmpty, out Guid? id, out string message)
|
||||
{
|
||||
id = null;
|
||||
message = string.Empty;
|
||||
|
||||
if (!assistantTable.TryGetValue(fieldName, out var idValue))
|
||||
return true;
|
||||
|
||||
if (!idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var parsedId) || (!allowEmpty && parsedId == Guid.Empty))
|
||||
{
|
||||
message = string.Format(TB("The ASSISTANT table contains an invalid {0}. Expected a {1}GUID."), fieldName, allowEmpty ? string.Empty : "non-empty ");
|
||||
return false;
|
||||
}
|
||||
|
||||
id = parsedId;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadOptionalDataSourceIds(LuaTable assistantTable, out IReadOnlyList<Guid>? dataSourceIds, out string message)
|
||||
{
|
||||
dataSourceIds = null;
|
||||
message = string.Empty;
|
||||
|
||||
if (!assistantTable.TryGetValue("DataSourceIds", out var dataSourceIdsValue))
|
||||
return true;
|
||||
|
||||
if (!dataSourceIdsValue.TryRead<LuaTable>(out var dataSourceIdsTable) || dataSourceIdsTable.ArrayLength == 0)
|
||||
{
|
||||
message = TB("The ASSISTANT table contains invalid DataSourceIds. Expected a non-empty list of unique, non-empty GUIDs.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var parsedIds = new List<Guid>(dataSourceIdsTable.ArrayLength);
|
||||
var uniqueIds = new HashSet<Guid>();
|
||||
for (var index = 1; index <= dataSourceIdsTable.ArrayLength; index++)
|
||||
{
|
||||
if (!dataSourceIdsTable[index].TryRead<string>(out var idText) ||
|
||||
!Guid.TryParse(idText, out var parsedId) ||
|
||||
parsedId == Guid.Empty ||
|
||||
!uniqueIds.Add(parsedId))
|
||||
{
|
||||
message = TB("The ASSISTANT table contains invalid DataSourceIds. Expected a non-empty list of unique, non-empty GUIDs.");
|
||||
return false;
|
||||
}
|
||||
|
||||
parsedIds.Add(parsedId);
|
||||
}
|
||||
|
||||
dataSourceIds = parsedIds.ToImmutableArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<string?> TryBuildPromptAsync(LuaTable input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this.buildPromptFunction is null)
|
||||
|
||||
Reference in new issue
Block a user