mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-17 18:38:00 +00:00
Fixed the tokenizer path in exported provider configurations (#1076)
This commit is contained in:
1 parent
22065783c2
commit
d333720f3f
12 files changed
+451
-23
No files matched your search
@@ -199,7 +199,7 @@ public sealed record DataDocumentAnalysisPolicy : ConfigurationBaseObject
|
|||||||
|
|
||||||
return $$"""
|
return $$"""
|
||||||
CONFIG["DOCUMENT_ANALYSIS_POLICIES"][#CONFIG["DOCUMENT_ANALYSIS_POLICIES"]+1] = {
|
CONFIG["DOCUMENT_ANALYSIS_POLICIES"][#CONFIG["DOCUMENT_ANALYSIS_POLICIES"]+1] = {
|
||||||
["Id"] = "{{id}}",
|
["Id"] = "{{LuaTools.EscapeLuaString(id)}}",
|
||||||
["PolicyName"] = {{LuaTools.ToLuaStringLiteral(this.PolicyName.Trim())}},
|
["PolicyName"] = {{LuaTools.ToLuaStringLiteral(this.PolicyName.Trim())}},
|
||||||
["PolicyDescription"] = {{LuaTools.ToLuaStringLiteral(this.PolicyDescription.Trim())}},
|
["PolicyDescription"] = {{LuaTools.ToLuaStringLiteral(this.PolicyDescription.Trim())}},
|
||||||
|
|
||||||
@@ -219,8 +219,8 @@ public sealed record DataDocumentAnalysisPolicy : ConfigurationBaseObject
|
|||||||
|
|
||||||
-- Optional: preselect a provider or profile by ID.
|
-- Optional: preselect a provider or profile by ID.
|
||||||
-- The IDs must exist in CONFIG["LLM_PROVIDERS"] or CONFIG["PROFILES"].
|
-- The IDs must exist in CONFIG["LLM_PROVIDERS"] or CONFIG["PROFILES"].
|
||||||
["PreselectedProvider"] = "{{preselectedProvider}}",
|
["PreselectedProvider"] = "{{LuaTools.EscapeLuaString(preselectedProvider)}}",
|
||||||
["PreselectedProfile"] = "{{preselectedProfile}}",
|
["PreselectedProfile"] = "{{LuaTools.EscapeLuaString(preselectedProfile)}}",
|
||||||
|
|
||||||
-- Optional: hide the policy definition section in the UI.
|
-- Optional: hide the policy definition section in the UI.
|
||||||
-- When set to true, users will only see the document selection interface
|
-- When set to true, users will only see the document selection interface
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Text.Json.Serialization;
|
|||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Provider.HuggingFace;
|
using AIStudio.Provider.HuggingFace;
|
||||||
using AIStudio.Tools.PluginSystem;
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
using SharedTools;
|
using SharedTools;
|
||||||
|
|
||||||
@@ -257,13 +258,15 @@ public sealed record EmbeddingProvider(
|
|||||||
""";
|
""";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var tokenizerPathLine = ExportedTokenizerPath.BuildLuaField(this.TokenizerPath, TokenizerModelId.ForEmbeddingProvider(this), " ");
|
||||||
|
|
||||||
return $$"""
|
return $$"""
|
||||||
CONFIG["EMBEDDING_PROVIDERS"][#CONFIG["EMBEDDING_PROVIDERS"]+1] = {
|
CONFIG["EMBEDDING_PROVIDERS"][#CONFIG["EMBEDDING_PROVIDERS"]+1] = {
|
||||||
["Id"] = "{{Guid.NewGuid().ToString()}}",
|
["Id"] = "{{Guid.NewGuid().ToString()}}",
|
||||||
["Name"] = "{{LuaTools.EscapeLuaString(this.Name)}}",
|
["Name"] = "{{LuaTools.EscapeLuaString(this.Name)}}",
|
||||||
["UsedLLMProvider"] = "{{this.UsedLLMProvider}}",
|
["UsedLLMProvider"] = "{{this.UsedLLMProvider}}",
|
||||||
|
|
||||||
["TokenizerPath"] = "{{LuaTools.EscapeLuaString(this.TokenizerPath)}}",
|
{{tokenizerPathLine}}
|
||||||
["TokenLimit"] = {{this.EffectiveTokenLimit}},
|
["TokenLimit"] = {{this.EffectiveTokenLimit}},
|
||||||
["EmbeddingBatchSize"] = {{this.EffectiveEmbeddingBatchSize}},
|
["EmbeddingBatchSize"] = {{this.EffectiveEmbeddingBatchSize}},
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using SharedTools;
|
||||||
|
|
||||||
|
namespace AIStudio.Settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tokenizer path an exported provider carries, which a configuration plugin and an import both read.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// AI Studio keeps the tokenizer of a provider as a copy below its data directory, in a folder of the
|
||||||
|
/// provider's own, e.g., tokenizers/chat_[ID]/tokenizer.json. An absolute path to that copy is of no use
|
||||||
|
/// in an export: a configuration plugin reads the path relative to its own folder and ignores one which
|
||||||
|
/// leads out of it, and the path would give away the user name of whoever exported it.<br/><br/>
|
||||||
|
///
|
||||||
|
/// The export therefore names the copy relative to the data directory, and one string serves both
|
||||||
|
/// readers. In a configuration plugin, it is relative to the folder of the plugin, where the
|
||||||
|
/// administrator copies the file to the same place. Imported on the computer it came from, it is
|
||||||
|
/// relative to the data directory and names the copy itself.
|
||||||
|
/// </remarks>
|
||||||
|
public static class ExportedTokenizerPath
|
||||||
|
{
|
||||||
|
private const string TOKENIZERS_DIRECTORY = "tokenizers";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Turns the path of the tokenizer copy of a provider into the path an export carries.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The runtime stores the copy as tokenizers/[model ID]/[file name] below the data directory, so this
|
||||||
|
/// builds the same path from the model ID instead of asking where the data directory lies. The file
|
||||||
|
/// name is taken after the last separator of either kind, which gives the same result on every OS,
|
||||||
|
/// also for a path written on another one.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="tokenizerPath">The path of the tokenizer copy, as the provider holds it in memory.</param>
|
||||||
|
/// <param name="tokenizerModelId">The model the tokenizer belongs to, as TokenizerModelId builds it.</param>
|
||||||
|
/// <returns>The path relative to the data directory, or an empty string when the provider has no tokenizer.</returns>
|
||||||
|
public static string FromLocal(string tokenizerPath, string tokenizerModelId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(tokenizerPath))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var fileName = tokenizerPath[(tokenizerPath.LastIndexOfAny(['/', '\\']) + 1)..];
|
||||||
|
return string.IsNullOrWhiteSpace(fileName) ? string.Empty : $"{TOKENIZERS_DIRECTORY}/{tokenizerModelId}/{fileName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Turns the tokenizer path of an imported export into a path on this computer.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A relative path names a copy below the data directory, and only one inside its folder of
|
||||||
|
/// tokenizers: an export comes from somebody else, so it gets no way to name any other file of the
|
||||||
|
/// data directory. An absolute path stems from an export of an older version and is taken as it is.
|
||||||
|
/// Whether the file exists is up to the caller.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="exportedPath">The tokenizer path as the export carries it.</param>
|
||||||
|
/// <param name="dataDirectory">The data directory, or null while it is not known yet.</param>
|
||||||
|
/// <returns>The path on this computer, or an empty string when the export names no tokenizer copy below the data directory.</returns>
|
||||||
|
public static string ToLocal(string exportedPath, string? dataDirectory)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(exportedPath) || Path.IsPathFullyQualified(exportedPath))
|
||||||
|
return exportedPath;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(dataDirectory))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var path = DataDirectoryRelativePathConverter.ToAbsolute(exportedPath, dataDirectory);
|
||||||
|
return !string.IsNullOrWhiteSpace(path) && PathTools.IsInsideDirectory(Path.Join(dataDirectory, TOKENIZERS_DIRECTORY), path) ? path : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes the TokenizerPath field of an exported provider.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The file itself does not travel with the Lua code, so a field naming one brings a note on where it
|
||||||
|
/// belongs. The lines after the first carry the indentation, so the field fits wherever its first
|
||||||
|
/// line is placed.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="tokenizerPath">The path of the tokenizer copy, as the provider holds it in memory.</param>
|
||||||
|
/// <param name="tokenizerModelId">The model the tokenizer belongs to, as TokenizerModelId builds it.</param>
|
||||||
|
/// <param name="indentation">The indentation of the field within its table.</param>
|
||||||
|
/// <returns>The Lua code of the field, ending with a comma.</returns>
|
||||||
|
public static string BuildLuaField(string tokenizerPath, string tokenizerModelId, string indentation)
|
||||||
|
{
|
||||||
|
var exportedPath = FromLocal(tokenizerPath, tokenizerModelId);
|
||||||
|
var field = $"""["TokenizerPath"] = "{LuaTools.EscapeLuaString(exportedPath)}",""";
|
||||||
|
if (string.IsNullOrEmpty(exportedPath))
|
||||||
|
return field;
|
||||||
|
|
||||||
|
string[] lines =
|
||||||
|
[
|
||||||
|
"-- This provider uses a tokenizer of its own. Ship the file with this plugin:",
|
||||||
|
"-- AI Studio keeps it in its data directory, below the path named here. Copy",
|
||||||
|
"-- it to the same place in the folder of this plugin, or change the path to",
|
||||||
|
"-- where you put it. Without the file, AI Studio uses its default tokenizer.",
|
||||||
|
field,
|
||||||
|
];
|
||||||
|
|
||||||
|
return string.Join($"{Environment.NewLine}{indentation}", lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using System.Text.Json.Serialization;
|
|||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Provider.HuggingFace;
|
using AIStudio.Provider.HuggingFace;
|
||||||
using AIStudio.Tools.PluginSystem;
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
using SharedTools;
|
using SharedTools;
|
||||||
|
|
||||||
@@ -279,14 +280,15 @@ public sealed record Provider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var capabilityOverridesLine = this.CapabilityOverrides?.ExportAsLuaTable(" ") ?? string.Empty;
|
var capabilityOverridesLine = this.CapabilityOverrides?.ExportAsLuaTable(" ") ?? string.Empty;
|
||||||
|
var tokenizerPathLine = ExportedTokenizerPath.BuildLuaField(this.TokenizerPath, TokenizerModelId.ForProvider(this), " ");
|
||||||
|
|
||||||
return $$"""
|
return $$"""
|
||||||
CONFIG["LLM_PROVIDERS"][#CONFIG["LLM_PROVIDERS"]+1] = {
|
CONFIG["LLM_PROVIDERS"][#CONFIG["LLM_PROVIDERS"]+1] = {
|
||||||
["Id"] = "{{Guid.NewGuid().ToString()}}",
|
["Id"] = "{{Guid.NewGuid().ToString()}}",
|
||||||
["InstanceName"] = "{{LuaTools.EscapeLuaString(this.InstanceName)}}",
|
["InstanceName"] = "{{LuaTools.EscapeLuaString(this.InstanceName)}}",
|
||||||
["UsedLLMProvider"] = "{{this.UsedLLMProvider}}",
|
["UsedLLMProvider"] = "{{this.UsedLLMProvider}}",
|
||||||
|
|
||||||
["TokenizerPath"] = "{{LuaTools.EscapeLuaString(this.TokenizerPath)}}",
|
{{tokenizerPathLine}}
|
||||||
|
|
||||||
["Host"] = "{{this.Host}}",
|
["Host"] = "{{this.Host}}",
|
||||||
["Hostname"] = "{{LuaTools.EscapeLuaString(this.Hostname)}}",
|
["Hostname"] = "{{LuaTools.EscapeLuaString(this.Hostname)}}",
|
||||||
|
|||||||
@@ -213,11 +213,20 @@ public static class ConfigurationSnippetImport
|
|||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// An export names the tokenizer relative to the data directory, where the copy of the provider
|
||||||
|
/// lies on the computer it came from. Imported there, the path leads back to that copy; anywhere
|
||||||
|
/// else, the copy is missing.
|
||||||
|
/// </remarks>
|
||||||
private static string KeepLocalTokenizer(string tokenizerPath, List<string> hints)
|
private static string KeepLocalTokenizer(string tokenizerPath, List<string> hints)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(tokenizerPath) || IsLocalFile(tokenizerPath))
|
if (string.IsNullOrWhiteSpace(tokenizerPath))
|
||||||
return tokenizerPath;
|
return tokenizerPath;
|
||||||
|
|
||||||
|
var localPath = ExportedTokenizerPath.ToLocal(tokenizerPath, SettingsManager.DataDirectory);
|
||||||
|
if (IsLocalFile(localPath))
|
||||||
|
return localPath;
|
||||||
|
|
||||||
hints.Add(string.Format(TB("The tokenizer '{0}' is not on this computer, so the provider is imported without it."), tokenizerPath));
|
hints.Add(string.Format(TB("The tokenizer '{0}' is not on this computer, so the provider is imported without it."), tokenizerPath));
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
- Fixed the security notice about removed instructions (prompt injection) appearing only minutes later while AI Studio updated a data source in the background. The notice also mixed your own attachments with files of the data source. Now each notice appears as soon as AI Studio has read the content, for example, the attachments of your message, and it names only that content.
|
- Fixed the security notice about removed instructions (prompt injection) appearing only minutes later while AI Studio updated a data source in the background. The notice also mixed your own attachments with files of the data source. Now each notice appears as soon as AI Studio has read the content, for example, the attachments of your message, and it names only that content.
|
||||||
- Fixed the security notice about removed instructions (prompt injection) appearing several times for one answer when AI Studio checks which passages of your data sources fit your question: once for every passage it checked, and once more for all of them together. You now get one notice per answer, and a file counts once, however many of its passages were affected.
|
- Fixed the security notice about removed instructions (prompt injection) appearing several times for one answer when AI Studio checks which passages of your data sources fit your question: once for every passage it checked, and once more for all of them together. You now get one notice per answer, and a file counts once, however many of its passages were affected.
|
||||||
- Fixed the example configuration for IT departments, which spelled llama.cpp in a way AI Studio does not accept. AI Studio ignored a self-hosted provider set up with that spelling. The example now also names only the self-hosted servers which can create embeddings or transcribe audio.
|
- Fixed the example configuration for IT departments, which spelled llama.cpp in a way AI Studio does not accept. AI Studio ignored a self-hosted provider set up with that spelling. The example now also names only the self-hosted servers which can create embeddings or transcribe audio.
|
||||||
- Fixed exporting an LLM provider or an embedding provider with a tokenizer on Windows. The path of the tokenizer broke the exported configuration, so a configuration plugin containing it could not be loaded.
|
- Fixed exporting an LLM provider or an embedding provider with a tokenizer. On Windows, the export broke the configuration plugin, and on every system, the plugin ignored the tokenizer because the export named a folder on your computer. The export now names the place in your configuration plugin where the tokenizer file belongs.
|
||||||
- Fixed building AI Studio from source on Windows, where the checks that run before every build failed because two of them expected the line breaks and file paths of macOS and Linux. Thanks, Paul Koudelka (`PaulKoudelka`), for this contribution.
|
- Fixed building AI Studio from source on Windows, where the checks that run before every build failed because two of them expected the line breaks and file paths of macOS and Linux. Thanks, Paul Koudelka (`PaulKoudelka`), for this contribution.
|
||||||
- Fixed a problem on Windows where AI Studio could fail to read the configuration of your organization when the name of an environment variable on your computer contained umlauts or other special characters.
|
- Fixed a problem on Windows where AI Studio could fail to read the configuration of your organization when the name of an environment variable on your computer contained umlauts or other special characters.
|
||||||
- Updated the code contributions on the supporters page, which now thank everyone who has contributed code to AI Studio so far.
|
- Updated the code contributions on the supporters page, which now thank everyone who has contributed code to AI Studio so far.
|
||||||
|
|||||||
@@ -18,13 +18,22 @@ public static class LuaTools
|
|||||||
public static string ToLuaStringLiteral(string? value, bool forceLongString = false, int longStringLengthThreshold = 80)
|
public static string ToLuaStringLiteral(string? value, bool forceLongString = false, int longStringLengthThreshold = 80)
|
||||||
{
|
{
|
||||||
value ??= string.Empty;
|
value ??= string.Empty;
|
||||||
if (!forceLongString &&
|
|
||||||
value.Length <= longStringLengthThreshold &&
|
//
|
||||||
!value.Contains('\n') &&
|
// Lua reads every kind of line break in a long string as \n. A carriage return therefore
|
||||||
!value.Contains('\r'))
|
// survives only in a quoted string, where it is escaped, even when a long string was asked for:
|
||||||
|
//
|
||||||
|
if (value.Contains('\r') || (!forceLongString &&
|
||||||
|
value.Length <= longStringLengthThreshold &&
|
||||||
|
!value.Contains('\n')))
|
||||||
return $"\"{EscapeLuaString(value)}\"";
|
return $"\"{EscapeLuaString(value)}\"";
|
||||||
|
|
||||||
return $"{CreateLongStringOpeningDelimiter(value)}{value}{CreateLongStringClosingDelimiter(value)}";
|
//
|
||||||
|
// Lua also drops a line break right after the opening bracket, so a text which starts with
|
||||||
|
// one gets another one in front:
|
||||||
|
//
|
||||||
|
var skippedLineBreak = value.StartsWith('\n') ? "\n" : string.Empty;
|
||||||
|
return $"{CreateLongStringOpeningDelimiter(value)}{skippedLineBreak}{value}{CreateLongStringClosingDelimiter(value)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string CreateLongStringOpeningDelimiter(string value)
|
private static string CreateLongStringOpeningDelimiter(string value)
|
||||||
@@ -41,8 +50,13 @@ public static class LuaTools
|
|||||||
|
|
||||||
private static string CreateLongStringEquals(string value)
|
private static string CreateLongStringEquals(string value)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// The closing bracket must neither occur in the text nor be completed by its end. A text
|
||||||
|
// which ends with "]===" would otherwise form "]===]" with the first character of the
|
||||||
|
// closing bracket, and Lua ends the string right there:
|
||||||
|
//
|
||||||
var equalsCount = 3;
|
var equalsCount = 3;
|
||||||
while (value.Contains($"]{new string('=', equalsCount)}]"))
|
while (value.Contains($"]{new string('=', equalsCount)}]") || value.EndsWith($"]{new string('=', equalsCount)}", StringComparison.Ordinal))
|
||||||
equalsCount++;
|
equalsCount++;
|
||||||
|
|
||||||
return new string('=', equalsCount);
|
return new string('=', equalsCount);
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
using AIStudio.Provider;
|
||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
using AIStudio.Tools.Rust;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
using Lua;
|
||||||
|
using Lua.Standard;
|
||||||
|
|
||||||
|
namespace AIStudio.Tests.Settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks the tokenizer path which an exported provider carries, as a configuration plugin reads it back.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// In memory, a provider points to its tokenizer copy below the data directory by an absolute path.
|
||||||
|
/// Exported as it is, that path named the user name of the administrator, broke the Lua code on Windows,
|
||||||
|
/// and was ignored by every configuration plugin, which only reads tokenizers inside its own folder.
|
||||||
|
/// The export names the copy relative to the data directory instead, which is also where the
|
||||||
|
/// administrator finds the file to copy into the plugin.
|
||||||
|
/// </remarks>
|
||||||
|
[TestFixture]
|
||||||
|
public sealed class ProviderLuaExportTests
|
||||||
|
{
|
||||||
|
private const string PROVIDER_ID = "5f3c2a1e-7b4d-4e8f-9a6c-1d2e3f4a5b6c";
|
||||||
|
|
||||||
|
private static readonly Guid PLUGIN_ID = new("33333333-3333-3333-3333-333333333333");
|
||||||
|
|
||||||
|
private string testDirectory = string.Empty;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void CreateTestDirectory() => this.testDirectory = Directory.CreateTempSubdirectory("provider-lua-export-").FullName;
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void RemoveTestDirectory()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(this.testDirectory, true);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
// A temporary directory we could not remove says nothing about the code under test.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(PluginConfigurationObjectType.LLM_PROVIDER)]
|
||||||
|
[TestCase(PluginConfigurationObjectType.EMBEDDING_PROVIDER)]
|
||||||
|
public async Task AWindowsPathLeavesNeitherTheUserNameNorTheDriveBehind(PluginConfigurationObjectType type)
|
||||||
|
{
|
||||||
|
var modelId = ModelIdOf(type);
|
||||||
|
var (luaCode, readBack) = await ExportAndReadBackAsync(type, $@"C:\Users\Jane Doe\AppData\Roaming\com.github.mindwork-ai.ai-studio\data\tokenizers\{modelId}\tokenizer.json");
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(readBack, Is.EqualTo($"tokenizers/{modelId}/tokenizer.json"), "A configuration plugin reads the path relative to its own folder.");
|
||||||
|
Assert.That(luaCode, Does.Not.Contain("Jane"));
|
||||||
|
Assert.That(luaCode, Does.Not.Contain("C:"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(PluginConfigurationObjectType.LLM_PROVIDER)]
|
||||||
|
[TestCase(PluginConfigurationObjectType.EMBEDDING_PROVIDER)]
|
||||||
|
public async Task AQuoteInTheFileNameSurvivesTheRoundTrip(PluginConfigurationObjectType type)
|
||||||
|
{
|
||||||
|
var modelId = ModelIdOf(type);
|
||||||
|
var (_, readBack) = await ExportAndReadBackAsync(type, $"""/Users/jane/Library/Application Support/com.github.mindwork-ai.ai-studio/data/tokenizers/{modelId}/my "best" tokenizer.json""");
|
||||||
|
|
||||||
|
Assert.That(readBack, Is.EqualTo($"""tokenizers/{modelId}/my "best" tokenizer.json"""));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(PluginConfigurationObjectType.LLM_PROVIDER)]
|
||||||
|
[TestCase(PluginConfigurationObjectType.EMBEDDING_PROVIDER)]
|
||||||
|
public async Task AProviderWithoutATokenizerExportsAnEmptyPathAndNoNote(PluginConfigurationObjectType type)
|
||||||
|
{
|
||||||
|
var (luaCode, readBack) = await ExportAndReadBackAsync(type, string.Empty);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(readBack, Is.Empty);
|
||||||
|
Assert.That(luaCode, Does.Contain("""["TokenizerPath"] = "",""").And.Not.Contain("tokenizer of its own"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(PluginConfigurationObjectType.LLM_PROVIDER)]
|
||||||
|
[TestCase(PluginConfigurationObjectType.EMBEDDING_PROVIDER)]
|
||||||
|
public async Task AConfigurationPluginFindsTheTokenizerWhereTheExportSaysItBelongs(PluginConfigurationObjectType type)
|
||||||
|
{
|
||||||
|
var modelId = ModelIdOf(type);
|
||||||
|
var dataDirectory = Path.Combine(this.testDirectory, "data");
|
||||||
|
var pluginDirectory = Path.Combine(this.testDirectory, "plugin");
|
||||||
|
var copyInTheDataDirectory = CreateTokenizer(Path.Combine(dataDirectory, "tokenizers", modelId, "tokenizer.json"));
|
||||||
|
|
||||||
|
var (_, exportedPath) = await ExportAndReadBackAsync(type, copyInTheDataDirectory);
|
||||||
|
var fileInThePlugin = CreateTokenizer(Path.Combine(pluginDirectory, exportedPath));
|
||||||
|
|
||||||
|
var storage = new AcceptingTokenizerStorage();
|
||||||
|
var synced = await PluginConfigurationObject.SyncTokenizerAsync(storage, exportedPath, string.Empty, pluginDirectory, modelId, "the exported provider");
|
||||||
|
var syncedWithTheAbsolutePath = await PluginConfigurationObject.SyncTokenizerAsync(storage, copyInTheDataDirectory, string.Empty, pluginDirectory, modelId, "the exported provider");
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(synced.Path, Is.EqualTo(fileInThePlugin), "The administrator copies the file to the same place in the plugin, where the plugin looks for it.");
|
||||||
|
Assert.That(syncedWithTheAbsolutePath, Is.EqualTo(StoredTokenizer.NONE), "What the export wrote before lies outside of the plugin, so the plugin ignored it.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ModelIdOf(PluginConfigurationObjectType type) => type switch
|
||||||
|
{
|
||||||
|
PluginConfigurationObjectType.LLM_PROVIDER => TokenizerModelId.ForProviderId(PROVIDER_ID),
|
||||||
|
PluginConfigurationObjectType.EMBEDDING_PROVIDER => TokenizerModelId.ForEmbeddingProviderId(PROVIDER_ID),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(type), type, "This type has no tokenizer."),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exports a provider with the given tokenizer path and reads the export back, as a configuration plugin does.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The exported Lua code and the tokenizer path the plugin reads from it.</returns>
|
||||||
|
private static async Task<(string LuaCode, string TokenizerPath)> ExportAndReadBackAsync(PluginConfigurationObjectType type, string tokenizerPath)
|
||||||
|
{
|
||||||
|
var luaCode = type switch
|
||||||
|
{
|
||||||
|
PluginConfigurationObjectType.LLM_PROVIDER => new AIStudio.Settings.Provider(1, PROVIDER_ID, "Chat provider", LLMProviders.OPEN_AI, new("gpt-6", "GPT-6"), TokenizerPath: tokenizerPath).ExportAsConfigurationSection(),
|
||||||
|
PluginConfigurationObjectType.EMBEDDING_PROVIDER => new EmbeddingProvider(1, PROVIDER_ID, "Embedding provider", LLMProviders.OPEN_AI, new("text-embedding-3-small", "text-embedding-3-small"), TokenizerPath: tokenizerPath).ExportAsConfigurationSection(),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(type), type, "This type has no tokenizer."),
|
||||||
|
};
|
||||||
|
|
||||||
|
var section = PluginConfigurationObjectDescriptors.Get(type).LuaTableName;
|
||||||
|
var state = LuaState.Create();
|
||||||
|
state.OpenBasicLibrary();
|
||||||
|
state.OpenTableLibrary();
|
||||||
|
|
||||||
|
await state.DoStringAsync($$"""
|
||||||
|
CONFIG = {}
|
||||||
|
CONFIG["{{section}}"] = {}
|
||||||
|
{{luaCode}}
|
||||||
|
""");
|
||||||
|
|
||||||
|
if (!state.Environment["CONFIG"].TryRead<LuaTable>(out var configTable) ||
|
||||||
|
!configTable[section].TryRead<LuaTable>(out var providersTable) ||
|
||||||
|
!providersTable[1].TryRead<LuaTable>(out var providerTable))
|
||||||
|
throw new InvalidOperationException("The export did not produce a provider table.");
|
||||||
|
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
PluginConfigurationObjectType.LLM_PROVIDER when AIStudio.Settings.Provider.TryParseProviderTable(1, providerTable, PLUGIN_ID, string.Empty, dryRun: true, out var provider) => (luaCode, ((AIStudio.Settings.Provider)provider).TokenizerPath),
|
||||||
|
PluginConfigurationObjectType.EMBEDDING_PROVIDER when EmbeddingProvider.TryParseEmbeddingProviderTable(1, providerTable, PLUGIN_ID, string.Empty, dryRun: true, out var provider) => (luaCode, ((EmbeddingProvider)provider).TokenizerPath),
|
||||||
|
_ => throw new InvalidOperationException("The exported provider could not be read."),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateTokenizer(string path)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
|
File.WriteAllText(path, """{ "version": "1.0", "model": { "type": "WordPiece" } }""");
|
||||||
|
return Path.GetFullPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stands in for the runtime: every existing file passes as a tokenizer, and storing one leaves it where it is.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class AcceptingTokenizerStorage : ITokenizerStorage
|
||||||
|
{
|
||||||
|
public Task<TokenizerResponse> ValidateTokenizer(string filePath) => Task.FromResult(new TokenizerResponse(File.Exists(filePath), 0, string.Empty));
|
||||||
|
|
||||||
|
public Task<TokenizerResponse> StoreTokenizer(string modelId, string filePath) => Task.FromResult(new TokenizerResponse(true, 0, string.Empty, filePath));
|
||||||
|
|
||||||
|
public Task<TokenizerResponse> DeleteTokenizer(string modelId) => Task.FromResult(new TokenizerResponse(true, 0, string.Empty));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
|
using Lua;
|
||||||
|
|
||||||
|
using SharedTools;
|
||||||
|
|
||||||
|
namespace AIStudio.Tests.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks that a text written as a Lua string literal reads back as the very same text.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Profiles, chat templates, and document analysis policies are exported through these literals. Since
|
||||||
|
/// an export can be imported, it is read back on other computers as well: by Lua in a configuration
|
||||||
|
/// plugin, and by the snippet parser in an import. Long strings keep long texts readable, but Lua reads
|
||||||
|
/// them by rules of its own: it drops a line break right after the opening bracket, turns every kind of
|
||||||
|
/// line break into \n, and ends the string at the first closing bracket of its level. Each case is
|
||||||
|
/// therefore read both ways and compared with the original.
|
||||||
|
/// </remarks>
|
||||||
|
[TestFixture]
|
||||||
|
public sealed class LuaToolsTests
|
||||||
|
{
|
||||||
|
private static readonly string LONG_TEXT = string.Concat(Enumerable.Repeat("A text longer than what a quoted string takes. ", 3));
|
||||||
|
|
||||||
|
private static IEnumerable<TestCaseData> Texts()
|
||||||
|
{
|
||||||
|
foreach (var forceLongString in new[] { false, true })
|
||||||
|
{
|
||||||
|
var suffix = forceLongString ? ", forced long string" : string.Empty;
|
||||||
|
yield return new TestCaseData(string.Empty, forceLongString).SetArgDisplayNames($"An empty text{suffix}");
|
||||||
|
yield return new TestCaseData("Short, with \"quotes\" and C:\\Users\\me", forceLongString).SetArgDisplayNames($"Quotes and backslashes{suffix}");
|
||||||
|
yield return new TestCaseData(LONG_TEXT, forceLongString).SetArgDisplayNames($"A long text{suffix}");
|
||||||
|
yield return new TestCaseData("First line\nSecond line", forceLongString).SetArgDisplayNames($"A line break in between{suffix}");
|
||||||
|
yield return new TestCaseData("\nStarts with a line break", forceLongString).SetArgDisplayNames($"A line break at the start{suffix}");
|
||||||
|
yield return new TestCaseData("\n\nStarts with an empty line", forceLongString).SetArgDisplayNames($"An empty line at the start{suffix}");
|
||||||
|
yield return new TestCaseData("Windows line breaks\r\nin between\r\n", forceLongString).SetArgDisplayNames($"Windows line breaks{suffix}");
|
||||||
|
yield return new TestCaseData("An old Mac line break\rin between", forceLongString).SetArgDisplayNames($"A carriage return alone{suffix}");
|
||||||
|
yield return new TestCaseData("Holds ]] and ]===] in between", forceLongString).SetArgDisplayNames($"Closing brackets in between{suffix}");
|
||||||
|
yield return new TestCaseData("Ends like a closing bracket ]===", forceLongString).SetArgDisplayNames($"The start of a closing bracket at the end{suffix}");
|
||||||
|
yield return new TestCaseData("Holds ]===] and ends like the next level ]====", forceLongString).SetArgDisplayNames($"The start of the next level at the end{suffix}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(Texts))]
|
||||||
|
public async Task LuaReadsTheLiteralAsTheOriginalText(string text, bool forceLongString)
|
||||||
|
{
|
||||||
|
var literal = LuaTools.ToLuaStringLiteral(text, forceLongString);
|
||||||
|
var state = LuaState.Create();
|
||||||
|
await state.DoStringAsync($"value = {literal}");
|
||||||
|
|
||||||
|
Assert.That(state.Environment["value"].Read<string>(), Is.EqualTo(text), literal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(Texts))]
|
||||||
|
public void AnImportReadsTheLiteralAsTheOriginalText(string text, bool forceLongString)
|
||||||
|
{
|
||||||
|
var snippet = $$"""CONFIG["PROFILES"][#CONFIG["PROFILES"]+1] = { ["Text"] = {{LuaTools.ToLuaStringLiteral(text, forceLongString)}} }""";
|
||||||
|
|
||||||
|
Assert.That(ConfigurationSnippetParser.TryParse(snippet, PluginConfigurationObjectType.PROFILE, out var table, out var issue), Is.True, issue);
|
||||||
|
Assert.That(table["Text"].Read<string>(), Is.EqualTo(text), snippet);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,12 @@ using AIStudio.Settings.DataModel;
|
|||||||
using AIStudio.Tests.Tools.ToolCalling;
|
using AIStudio.Tests.Tools.ToolCalling;
|
||||||
using AIStudio.Tools;
|
using AIStudio.Tools;
|
||||||
using AIStudio.Tools.PluginSystem;
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
|
using SharedTools;
|
||||||
|
|
||||||
using AuthMethod = AIStudio.Tools.ERIClient.DataModel.AuthMethod;
|
using AuthMethod = AIStudio.Tools.ERIClient.DataModel.AuthMethod;
|
||||||
using Host = AIStudio.Provider.SelfHosted.Host;
|
using Host = AIStudio.Provider.SelfHosted.Host;
|
||||||
|
|
||||||
@@ -42,18 +45,26 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
|||||||
];
|
];
|
||||||
|
|
||||||
private string temporaryDirectory = string.Empty;
|
private string temporaryDirectory = string.Empty;
|
||||||
|
private string dataDirectory = string.Empty;
|
||||||
|
private string? previousDataDirectory;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void PrepareEncryptionAndFiles()
|
public void PrepareEncryptionAndFiles()
|
||||||
{
|
{
|
||||||
PluginFactory.InitializeEnterpriseEncryption(EnterpriseEncryption.GenerateSecret());
|
PluginFactory.InitializeEnterpriseEncryption(EnterpriseEncryption.GenerateSecret());
|
||||||
this.temporaryDirectory = Directory.CreateTempSubdirectory("snippet-import-").FullName;
|
this.temporaryDirectory = Directory.CreateTempSubdirectory("snippet-import-").FullName;
|
||||||
|
|
||||||
|
// The data directory is static state of the whole application, too. An exported tokenizer is found below it:
|
||||||
|
this.previousDataDirectory = SettingsManager.DataDirectory;
|
||||||
|
this.dataDirectory = Path.Combine(this.temporaryDirectory, "data");
|
||||||
|
SettingsManager.DataDirectory = this.dataDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
[TearDown]
|
[TearDown]
|
||||||
public void RemoveEncryptionAndFiles()
|
public void RemoveEncryptionAndFiles()
|
||||||
{
|
{
|
||||||
RemoveEncryptionSecret();
|
RemoveEncryptionSecret();
|
||||||
|
SettingsManager.DataDirectory = this.previousDataDirectory;
|
||||||
Directory.Delete(this.temporaryDirectory, recursive: true);
|
Directory.Delete(this.temporaryDirectory, recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,12 +299,14 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
|||||||
[TestCase(PluginConfigurationObjectType.EMBEDDING_PROVIDER)]
|
[TestCase(PluginConfigurationObjectType.EMBEDDING_PROVIDER)]
|
||||||
public void ATokenizerOnThisComputerIsKept(PluginConfigurationObjectType type)
|
public void ATokenizerOnThisComputerIsKept(PluginConfigurationObjectType type)
|
||||||
{
|
{
|
||||||
var tokenizer = this.CreateFile("tokenizer.json");
|
// The export keeps only the file name of the tokenizer and names the folder of the provider's copy itself:
|
||||||
var result = Import(ExportProvider(type, null, tokenizer).Snippet, type);
|
var (original, snippet) = ExportProvider(type, null, Path.Combine(this.temporaryDirectory, "tokenizer.json"));
|
||||||
|
var copy = this.CreateTokenizerCopy(type, original.Id);
|
||||||
|
var result = Import(snippet, type);
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
Assert.That(TokenizerPathOf(result.ConfigurationObject), Is.EqualTo(tokenizer));
|
Assert.That(TokenizerPathOf(result.ConfigurationObject), Is.EqualTo(copy), "Imported on the computer it came from, the export leads back to the copy of the provider.");
|
||||||
Assert.That(result.Hints, Is.Empty);
|
Assert.That(result.Hints, Is.Empty);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -302,13 +315,41 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
|||||||
[TestCase(PluginConfigurationObjectType.EMBEDDING_PROVIDER)]
|
[TestCase(PluginConfigurationObjectType.EMBEDDING_PROVIDER)]
|
||||||
public void ATokenizerWhichIsNotOnThisComputerIsLeftOut(PluginConfigurationObjectType type)
|
public void ATokenizerWhichIsNotOnThisComputerIsLeftOut(PluginConfigurationObjectType type)
|
||||||
{
|
{
|
||||||
var tokenizer = Path.Combine(this.temporaryDirectory, "missing-tokenizer.json");
|
var result = Import(ExportProvider(type, null, Path.Combine(this.temporaryDirectory, "missing-tokenizer.json")).Snippet, type);
|
||||||
var result = Import(ExportProvider(type, null, tokenizer).Snippet, type);
|
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
Assert.That(TokenizerPathOf(result.ConfigurationObject), Is.Empty, "Saving the provider would fail on a tokenizer which cannot be copied.");
|
Assert.That(TokenizerPathOf(result.ConfigurationObject), Is.Empty, "Saving the provider would fail on a tokenizer which cannot be copied.");
|
||||||
Assert.That(result.Hints, Has.Exactly(1).Contains(tokenizer));
|
Assert.That(result.Hints, Has.Exactly(1).Contains("missing-tokenizer.json"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(PluginConfigurationObjectType.LLM_PROVIDER)]
|
||||||
|
[TestCase(PluginConfigurationObjectType.EMBEDDING_PROVIDER)]
|
||||||
|
public void TheAbsoluteTokenizerPathOfAnOlderExportIsKept(PluginConfigurationObjectType type)
|
||||||
|
{
|
||||||
|
var tokenizer = this.CreateFile("tokenizer.json");
|
||||||
|
var result = Import(ExportProviderWithTokenizerPath(type, tokenizer), type);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(TokenizerPathOf(result.ConfigurationObject), Is.EqualTo(tokenizer), "Versions before v26.10.1 exported the absolute path of the tokenizer.");
|
||||||
|
Assert.That(result.Hints, Is.Empty);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(PluginConfigurationObjectType.LLM_PROVIDER)]
|
||||||
|
[TestCase(PluginConfigurationObjectType.EMBEDDING_PROVIDER)]
|
||||||
|
public void AnExportCannotNameAnotherFileOfTheDataDirectoryAsItsTokenizer(PluginConfigurationObjectType type)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(this.dataDirectory);
|
||||||
|
File.WriteAllText(Path.Combine(this.dataDirectory, "settings.json"), "{}");
|
||||||
|
var result = Import(ExportProviderWithTokenizerPath(type, "tokenizers/../settings.json"), type);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(TokenizerPathOf(result.ConfigurationObject), Is.Empty, "Only the folder of the tokenizers holds what an export may name.");
|
||||||
|
Assert.That(result.Hints, Has.Count.EqualTo(1));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,6 +497,31 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the tokenizer copy of a provider where the runtime stores it, below the data directory.
|
||||||
|
/// </summary>
|
||||||
|
private string CreateTokenizerCopy(PluginConfigurationObjectType type, string providerId)
|
||||||
|
{
|
||||||
|
var modelId = type is PluginConfigurationObjectType.LLM_PROVIDER ? TokenizerModelId.ForProviderId(providerId) : TokenizerModelId.ForEmbeddingProviderId(providerId);
|
||||||
|
var path = Path.Combine(this.dataDirectory, "tokenizers", modelId, "tokenizer.json");
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
|
File.WriteAllText(path, "{}");
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What an export of a provider carries when its tokenizer path was written otherwise, e.g., by
|
||||||
|
/// an older version or by hand.
|
||||||
|
/// </summary>
|
||||||
|
private static string ExportProviderWithTokenizerPath(PluginConfigurationObjectType type, string tokenizerPath)
|
||||||
|
{
|
||||||
|
const string EMPTY_TOKENIZER_PATH = """["TokenizerPath"] = "",""";
|
||||||
|
var snippet = ExportProvider(type, null).Snippet;
|
||||||
|
Assert.That(snippet, Does.Contain(EMPTY_TOKENIZER_PATH));
|
||||||
|
|
||||||
|
return snippet.Replace(EMPTY_TOKENIZER_PATH, $"""["TokenizerPath"] = "{LuaTools.EscapeLuaString(tokenizerPath)}",""");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// What the app exports for one item of each type, along with the item.
|
/// What the app exports for one item of each type, along with the item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -35,9 +35,11 @@ public sealed class ConfigurationSnippetParserTests
|
|||||||
private const string QUOTED_TEXT = "He said \"hi\" to C:\\Users\\me, and 'bye'.";
|
private const string QUOTED_TEXT = "He said \"hi\" to C:\\Users\\me, and 'bye'.";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Every kind of line break, an indentation, and the closing brackets of long strings.
|
/// A line break at the start, an empty line, an indentation, and the closing brackets of long
|
||||||
|
/// strings, inside and at the end. Carriage returns are left to the output rules of the policy:
|
||||||
|
/// a text with one is exported as a quoted string, so it would never reach a long string here.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const string MULTILINE_TEXT = "First line\r\nSecond line\rThird line\n\tIndented, with ]] and ]==] and ]===] inside.";
|
private const string MULTILINE_TEXT = "\nFirst line\nSecond line\n\n\tIndented, with ]] and ]==] and ]===] inside, and ending like one: ]===";
|
||||||
|
|
||||||
private const string WINDOWS_PATH = @"C:\Users\me\AppData\Roaming\MindWork AI Studio\tokenizer.json";
|
private const string WINDOWS_PATH = @"C:\Users\me\AppData\Roaming\MindWork AI Studio\tokenizer.json";
|
||||||
|
|
||||||
|
|||||||
@@ -619,6 +619,8 @@ The button copies the fragment to your clipboard. Paste it into your [configurat
|
|||||||
|
|
||||||
Some exports ask a question first: a provider with an API key offers to include it encrypted (see [Encrypted API Keys](#encrypted-api-keys)), an ERI data source does the same for its token or its credentials, a chat template with file attachments asks whether to keep their paths or copy them into your plugin, and a tool opens a dialog for the areas and the kind of management you want (see [Exporting tool configurations](#exporting-tool-configurations)).
|
Some exports ask a question first: a provider with an API key offers to include it encrypted (see [Encrypted API Keys](#encrypted-api-keys)), an ERI data source does the same for its token or its credentials, a chat template with file attachments asks whether to keep their paths or copy them into your plugin, and a tool opens a dialog for the areas and the kind of management you want (see [Exporting tool configurations](#exporting-tool-configurations)).
|
||||||
|
|
||||||
|
**A tokenizer travels as a path, not as a file.** When an LLM or embedding provider uses a tokenizer of its own, the fragment names the copy AI Studio keeps of it, relative to the [data directory](#the-data-and-configuration-directories), e.g., `tokenizers/chat_<ID>/tokenizer.json` for an LLM provider and `tokenizers/embedding_<ID>/tokenizer.json` for an embedding provider. A configuration plugin reads this path relative to its own folder. Copy the file from your data directory to the same place in your plugin, or change the path to wherever you put it. A comment in the fragment says the same. The ID in the folder name is the one the provider has on your computer; it does not have to match anything else. Without the file, AI Studio counts tokens with its default tokenizer.
|
||||||
|
|
||||||
Handing a whole plugin to a colleague is a different thing: that is the **Share** function on the plugins page, which writes a `.mwplugin` archive and is governed by its own organization setting rather than by the administration settings.
|
Handing a whole plugin to a colleague is a different thing: that is the **Share** function on the plugins page, which writes a `.mwplugin` archive and is governed by its own organization setting rather than by the administration settings.
|
||||||
|
|
||||||
### Importing an exported item
|
### Importing an exported item
|
||||||
@@ -628,7 +630,7 @@ Not everything worth sharing calls for a configuration plugin. Say somebody buil
|
|||||||
- **Data only.** AI Studio reads the fragment with the same rules as a configuration plugin, but it never runs it. A fragment which contains anything other than values, such as a function call, a variable, or an expression, is refused.
|
- **Data only.** AI Studio reads the fragment with the same rules as a configuration plugin, but it never runs it. A fragment which contains anything other than values, such as a function call, a variable, or an expression, is refused.
|
||||||
- **An item of the user's own.** The imported item gets an ID of its own and belongs to no configuration plugin, even when the fragment came from one. Importing the same fragment twice gives two items.
|
- **An item of the user's own.** The imported item gets an ID of its own and belongs to no configuration plugin, even when the fragment came from one. Importing the same fragment twice gives two items.
|
||||||
- **Reviewed before it is saved.** Providers, profiles, chat templates, and data sources open in their usual form, filled in, so the user checks everything before saving. A document analysis policy is added right away and selected, and the user edits it in the assistant like any other.
|
- **Reviewed before it is saved.** Providers, profiles, chat templates, and data sources open in their usual form, filled in, so the user checks everything before saving. A document analysis policy is added right away and selected, and the user edits it in the assistant like any other.
|
||||||
- **Hints first.** Before that, the dialog lists what the user should know: a preselected provider, profile, data source, or tool which does not exist on this computer, a tokenizer which is not on this computer and is therefore left out, an API key which cannot be decrypted, and every local file which a chat template would attach to each of its chats. Attachments which are not on this computer are relinked or removed in the form.
|
- **Hints first.** Before that, the dialog lists what the user should know: a preselected provider, profile, data source, or tool which does not exist on this computer, a tokenizer which is not on this computer and is therefore left out, an API key which cannot be decrypted, and every local file which a chat template would attach to each of its chats. Attachments which are not on this computer are relinked or removed in the form. An exported provider names the copy of its tokenizer on the computer it came from, so the tokenizer comes along only when the fragment is imported there.
|
||||||
- **Secrets.** An API key, token, or password which the export included in encrypted form is decrypted only where the encryption secret of your organization is configured, see [Encrypted API Keys](#encrypted-api-keys). Without it, a provider is imported without its key, and an ERI data source with a token or password cannot be imported at all. Data sources which sign in with Kerberos cannot be imported either.
|
- **Secrets.** An API key, token, or password which the export included in encrypted form is decrypted only where the encryption secret of your organization is configured, see [Encrypted API Keys](#encrypted-api-keys). Without it, a provider is imported without its key, and an ERI data source with a token or password cannot be imported at all. Data sources which sign in with Kerberos cannot be imported either.
|
||||||
- **No connection on its own.** Importing contacts no server. The user loads the models of a provider, or tests the connection of a data source, in the form.
|
- **No connection on its own.** Importing contacts no server. The user loads the models of a provider, or tests the connection of a data source, in the form.
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user