mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-10 23:47:21 +00:00
228 lines
9.4 KiB
C#
228 lines
9.4 KiB
C#
using System.Text.Json;
|
|
|
|
using AIStudio.Assistants;
|
|
using AIStudio.Chat;
|
|
using AIStudio.Settings;
|
|
using AIStudio.Tools;
|
|
using AIStudio.Tools.Services;
|
|
using AIStudio.Tools.ToolCallingSystem;
|
|
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
namespace AIStudio.Tests.Chat;
|
|
|
|
/// <summary>
|
|
/// Checks that the system prompt reports the parts it is made of.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The dialog which shows the system prompt names its parts from what building it decided. The text
|
|
/// itself is what the request sends and the token count measures, so recording the parts must never
|
|
/// change it: each part stands in the text exactly as recorded, in the order recorded.
|
|
/// </remarks>
|
|
[TestFixture]
|
|
public sealed class ChatThreadSystemPromptTests
|
|
{
|
|
private const string BASE_PROMPT = "You are a helpful assistant.";
|
|
private const string TEMPLATE_PROMPT = "You review contracts and point out risks.";
|
|
private const string RETRIEVED_DATA = "The following useful information will help you in processing the user prompt: the travel policy allows trains only.";
|
|
|
|
private static readonly string TEMPLATE_ID = Guid.NewGuid().ToString();
|
|
private static readonly string PROFILE_ID = Guid.NewGuid().ToString();
|
|
|
|
[Test]
|
|
public void TheBasePromptStandsAloneWithoutAnythingElse()
|
|
{
|
|
using var rustService = new RustService("1", "unused");
|
|
var settings = Settings(rustService, allowProfileUsage: true);
|
|
var prepared = Thread().BuildSystemPrompt(settings);
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(prepared.Text, Is.EqualTo(BASE_PROMPT));
|
|
Assert.That(prepared.Parts.Select(part => part.Kind), Is.EqualTo(new[] { SystemPromptPartKind.BASE_PROMPT }));
|
|
Assert.That(prepared.SwitchedOffProfile, Is.EqualTo(Profile.NO_PROFILE));
|
|
Assert.That(prepared.Parts[0].Origin, Is.Null, "Only a chat which continues the conversation of an assistant names one.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void TheBasePromptNamesTheAssistantWhichChoseIt()
|
|
{
|
|
using var rustService = new RustService("1", "unused");
|
|
var settings = Settings(rustService, allowProfileUsage: true);
|
|
var origin = new ChatSystemPromptOrigin { Source = ChatSystemPromptSource.CUSTOM, AssistantName = "Translation" };
|
|
var thread = Thread();
|
|
thread.SystemPromptOrigin = origin;
|
|
|
|
var prepared = thread.BuildSystemPrompt(settings);
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(prepared.Parts.Single().Kind, Is.EqualTo(SystemPromptPartKind.BASE_PROMPT));
|
|
Assert.That(prepared.Parts.Single().Origin, Is.EqualTo(origin));
|
|
Assert.That(prepared.Text, Is.EqualTo(BASE_PROMPT), "Naming the assistant must not change the prompt.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void AStoredChatKeepsWhichAssistantChoseItsPrompt()
|
|
{
|
|
var thread = Thread();
|
|
thread.SystemPromptOrigin = new ChatSystemPromptOrigin { Source = ChatSystemPromptSource.CUSTOM, AssistantName = "Translation" };
|
|
|
|
var json = JsonSerializer.Serialize(thread, WorkspaceBehaviour.JSON_OPTIONS);
|
|
var stored = JsonSerializer.Deserialize<ChatThread>(json, WorkspaceBehaviour.JSON_OPTIONS)!;
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(json, Does.Contain("\"CUSTOM\""), "The source is stored by its name, which assistant plugins use as well.");
|
|
Assert.That(stored.SystemPromptOrigin, Is.EqualTo(thread.SystemPromptOrigin));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void EveryPartStandsInTheTextInItsOrder()
|
|
{
|
|
using var rustService = new RustService("1", "unused");
|
|
var settings = Settings(rustService, allowProfileUsage: true);
|
|
var thread = Thread();
|
|
thread.IncludeDateTime = true;
|
|
thread.SelectedChatTemplate = TEMPLATE_ID;
|
|
thread.SelectedProfile = PROFILE_ID;
|
|
thread.AugmentedData = RETRIEVED_DATA;
|
|
|
|
var tool = Tool("calculate", "Use `calculate` for every arithmetic.");
|
|
var prepared = thread.BuildSystemPrompt(settings, [tool]);
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(prepared.Parts.Select(part => part.Kind), Is.EqualTo(new[]
|
|
{
|
|
SystemPromptPartKind.DATE_TIME,
|
|
SystemPromptPartKind.CHAT_TEMPLATE,
|
|
SystemPromptPartKind.RETRIEVED_DATA,
|
|
SystemPromptPartKind.PROFILE,
|
|
SystemPromptPartKind.TOOL_INSTRUCTIONS,
|
|
}));
|
|
|
|
var position = 0;
|
|
foreach (var part in prepared.Parts)
|
|
{
|
|
var found = prepared.Text.IndexOf(part.Text, position, StringComparison.Ordinal);
|
|
Assert.That(found, Is.GreaterThanOrEqualTo(position), $"The part {part.Kind} must stand in the text after the parts before it.");
|
|
position = found + part.Text.Length;
|
|
}
|
|
|
|
Assert.That(position, Is.EqualTo(prepared.Text.Length), "Nothing may follow the last part.");
|
|
Assert.That(prepared.Parts[1].SourceName, Is.EqualTo("Legal review"));
|
|
Assert.That(prepared.Parts[1].Text, Is.EqualTo(TEMPLATE_PROMPT));
|
|
Assert.That(prepared.Parts[2].Text, Is.EqualTo(RETRIEVED_DATA));
|
|
Assert.That(prepared.Parts[3].SourceName, Is.EqualTo("Developer"));
|
|
Assert.That(prepared.Parts[4].Tools, Is.EqualTo(new[] { tool }));
|
|
Assert.That(prepared.BasePrompt, Is.EqualTo(TEMPLATE_PROMPT));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void AChatTemplateWhichForbidsProfilesSwitchesTheSelectedProfileOff()
|
|
{
|
|
using var rustService = new RustService("1", "unused");
|
|
var settings = Settings(rustService, allowProfileUsage: false);
|
|
var thread = Thread();
|
|
thread.SelectedChatTemplate = TEMPLATE_ID;
|
|
thread.SelectedProfile = PROFILE_ID;
|
|
|
|
var prepared = thread.BuildSystemPrompt(settings);
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(prepared.Text, Is.EqualTo(TEMPLATE_PROMPT));
|
|
Assert.That(prepared.Parts.Select(part => part.Kind), Is.EqualTo(new[] { SystemPromptPartKind.CHAT_TEMPLATE }));
|
|
Assert.That(prepared.SwitchedOffProfile.Name, Is.EqualTo("Developer"));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void AChatTemplateWhichForbidsProfilesSwitchesNothingOffWithoutAProfile()
|
|
{
|
|
using var rustService = new RustService("1", "unused");
|
|
var settings = Settings(rustService, allowProfileUsage: false);
|
|
var thread = Thread();
|
|
thread.SelectedChatTemplate = TEMPLATE_ID;
|
|
thread.SelectedProfile = Profile.NO_PROFILE.Id;
|
|
|
|
var prepared = thread.BuildSystemPrompt(settings);
|
|
Assert.That(prepared.SwitchedOffProfile, Is.EqualTo(Profile.NO_PROFILE), "Nothing was selected, so the chat template switched nothing off.");
|
|
}
|
|
|
|
[Test]
|
|
public void AChatTemplateWhichNoLongerExistsLeavesTheBasePrompt()
|
|
{
|
|
using var rustService = new RustService("1", "unused");
|
|
var settings = Settings(rustService, allowProfileUsage: true);
|
|
var thread = Thread();
|
|
thread.SelectedChatTemplate = Guid.NewGuid().ToString();
|
|
|
|
var prepared = thread.BuildSystemPrompt(settings);
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(prepared.Text, Is.EqualTo(BASE_PROMPT));
|
|
Assert.That(prepared.Parts.Select(part => part.Kind), Is.EqualTo(new[] { SystemPromptPartKind.BASE_PROMPT }));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void AToolWithoutInstructionsAddsNoPart()
|
|
{
|
|
using var rustService = new RustService("1", "unused");
|
|
var settings = Settings(rustService, allowProfileUsage: true);
|
|
var withInstructions = Tool("calculate", "Use `calculate` for every arithmetic.");
|
|
var withoutInstructions = Tool("silent_tool", " ");
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
var silent = Thread().BuildSystemPrompt(settings, [withoutInstructions]);
|
|
Assert.That(silent.Text, Is.EqualTo(BASE_PROMPT));
|
|
Assert.That(silent.Parts.Select(part => part.Kind), Is.EqualTo(new[] { SystemPromptPartKind.BASE_PROMPT }));
|
|
|
|
var mixed = Thread().BuildSystemPrompt(settings, [withoutInstructions, withInstructions]);
|
|
Assert.That(mixed.Parts[^1].Tools, Is.EqualTo(new[] { withInstructions }), "Only a tool with instructions is a source of that part.");
|
|
Assert.That(mixed.Text, Does.Not.Contain("`silent_tool`"));
|
|
}
|
|
}
|
|
|
|
private static ChatThread Thread() => new() { SystemPrompt = BASE_PROMPT };
|
|
|
|
private static ToolDefinition Tool(string name, string instructions) => new()
|
|
{
|
|
Id = name,
|
|
ImplementationKey = name,
|
|
SystemPromptInstructions = instructions,
|
|
Function = new() { Name = name },
|
|
};
|
|
|
|
private static SettingsManager Settings(RustService rustService, bool allowProfileUsage)
|
|
{
|
|
var settings = new SettingsManager(NullLogger<SettingsManager>.Instance, rustService);
|
|
settings.ConfigurationData.ChatTemplates.Add(new ChatTemplate
|
|
{
|
|
Num = 1,
|
|
Id = TEMPLATE_ID,
|
|
Name = "Legal review",
|
|
SystemPrompt = TEMPLATE_PROMPT,
|
|
AllowProfileUsage = allowProfileUsage,
|
|
});
|
|
|
|
settings.ConfigurationData.Profiles.Add(new Profile
|
|
{
|
|
Num = 1,
|
|
Id = PROFILE_ID,
|
|
Name = "Developer",
|
|
NeedToKnow = "I write C# for a living.",
|
|
Actions = "Answer briefly.",
|
|
});
|
|
|
|
return settings;
|
|
}
|
|
} |