Fixed the Agenda Planner and ERI Server assistants losing parts of your input (#1142)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-10-31 15:24:33 +01:00
1 parent 8a4443ab4b
commit 652d3a509f
19 files changed
+1161 -339

No files matched your search

@@ -480,7 +480,7 @@ public partial class AssistantAgenda : AssistantBaseCore<SettingsDialogAgenda>
private string PromptSocialActivity()
{
if (this.doingSocialActivity)
return "- The participants will engage in a social activity after the meeting or seminar. This can be a team-building exercise, a sightseeing tour, or a visit to a local attraction. Please make a recommendation for an activity at '{this.inputLocation}'.";
return $"- The participants will engage in a social activity after the meeting or seminar. This can be a team-building exercise, a sightseeing tour, or a visit to a local attraction. Please make a recommendation for an activity at '{this.inputLocation}'.";
return "- The participants will not engage in a social activity after the meeting or seminar.";
}
@@ -1,6 +1,3 @@
using System.Text;
using System.Text.RegularExpressions;
using AIStudio.Chat;
using AIStudio.Dialogs;
using AIStudio.Dialogs.Settings;
@@ -27,270 +24,25 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
protected override string Description => T("The ERI is the External Retrieval Interface for AI Studio and other tools. The ERI acts as a contract between decentralized data sources and, e.g., AI Studio. The ERI is implemented by the data sources, allowing them to be integrated into AI Studio later. This means that the data sources assume the server role and AI Studio (or any other LLM tool) assumes the client role of the API. This approach serves to realize a Retrieval-Augmented Generation (RAG) process with external data.");
protected override string SystemPrompt
protected override string SystemPrompt => new ERIServerPrompt
{
get
{
var sb = new StringBuilder();
//
// ---------------------------------
// Introduction
// ---------------------------------
//
var programmingLanguage = this.selectedProgrammingLanguage is ProgrammingLanguages.OTHER ? this.otherProgrammingLanguage : this.selectedProgrammingLanguage.ToPrompt();
sb.Append($"""
# Introduction
You are an experienced {programmingLanguage} developer. Your task is to implement an API server in
the {programmingLanguage} language according to the following OpenAPI Description (OAD):
```
{this.eriSpecification}
```
The server realizes the data retrieval component for a "Retrieval-Augmentation Generation" (RAG) process.
The server is called "{this.serverName}" and is described as follows:
```
{this.serverDescription}
```
""");
//
// ---------------------------------
// Data Source
// ---------------------------------
//
sb.Append("""
# Data Source
""");
switch (this.selectedDataSource)
{
case DataSources.CUSTOM:
sb.Append($"""
The data source for the retrieval process is described as follows:
```
{this.otherDataSource}
```
""");
if(!string.IsNullOrWhiteSpace(this.dataSourceHostname))
{
sb.Append($"""
The data source is accessible via the hostname `{this.dataSourceHostname}`.
""");
}
if(this.dataSourcePort is not null)
{
sb.Append($"""
The data source is accessible via port `{this.dataSourcePort}`.
""");
}
break;
case DataSources.FILE_SYSTEM:
sb.Append("""
The data source for the retrieval process is the local file system. Use a placeholder for the data source path.
""");
break;
default:
case DataSources.OBJECT_STORAGE:
case DataSources.KEY_VALUE_STORE:
case DataSources.DOCUMENT_STORE:
case DataSources.RELATIONAL_DATABASE:
case DataSources.GRAPH_DATABASE:
sb.Append($"""
The data source for the retrieval process is an "{this.dataSourceProductName}" database running on the
host `{this.dataSourceHostname}` and is accessible via port `{this.dataSourcePort}`.
""");
break;
}
//
// ---------------------------------
// Authentication and Authorization
// ---------------------------------
//
sb.Append("""
# Authentication and Authorization
The process for authentication and authorization is two-step. Step 1: Users must authenticate
with the API server using a `POST` call on `/auth` with the chosen method. If this step is
successful, the API server returns a token. Step 2: This token is then required for all
other API calls.
Important notes:
- Calls to `/auth` and `/auth/methods` are accessible without authentication. All other API
endpoints require a valid step 2 token.
- It is possible that a token (step 1 token) is desired as `authMethod`. These step 1 tokens
must never be accepted as valid tokens for step 2.
- The OpenAPI Description (OAD) for `/auth` is not complete. This is because, at the time of
writing the OAD, it is not known what kind of authentication is desired. Therefore, you must
supplement the corresponding fields or data in the implementation. Example: If username/password
is desired, you must expect and read both. If both token and username/password are desired, you
must dynamically read the `authMethod` and expect and evaluate different fields accordingly.
The following authentications and authorizations should be implemented for the API server:
""");
foreach (var auth in this.selectedAuthenticationMethods)
sb.Append($"- {auth.ToPrompt()}");
if(this.IsUsingKerberos())
{
sb.Append($"""
The server will run on {this.selectedOperatingSystem.ToPrompt()} operating systems. Keep
this in mind when implementing the SSO with Kerberos.
""");
}
//
// ---------------------------------
// Security
// ---------------------------------
//
sb.Append($"""
# Security
The following security requirement for `allowedProviderType` was chosen: `{this.allowedLLMProviders}`
""");
//
// ---------------------------------
// Retrieval Processes
// ---------------------------------
//
sb.Append($"""
# Retrieval Processes
You are implementing the following data retrieval processes:
""");
var retrievalProcessCounter = 1;
foreach (var retrievalProcess in this.retrievalProcesses)
{
sb.Append($"""
## {retrievalProcessCounter++}. Retrieval Process
- Name: {retrievalProcess.Name}
- Description:
```
{retrievalProcess.Description}
```
""");
if(retrievalProcess.ParametersDescription?.Count > 0)
{
sb.Append("""
This retrieval process recognizes the following parameters:
""");
var parameterCounter = 1;
foreach (var (parameter, description) in retrievalProcess.ParametersDescription)
{
sb.Append($"""
- The {parameterCounter++} parameter is named "{parameter}":
```
{description}
```
""");
}
sb.Append("""
Please use sensible default values for the parameters. They are optional
for the user.
""");
}
if(retrievalProcess.Embeddings?.Count > 0)
{
sb.Append("""
The following embeddings are implemented for this retrieval process:
""");
var embeddingCounter = 1;
foreach (var embedding in retrievalProcess.Embeddings)
{
sb.Append($"""
- {embeddingCounter++}. Embedding
- Name: {embedding.EmbeddingName}
- Type: {embedding.EmbeddingType}
- Description:
```
{embedding.Description}
```
- When used:
```
{embedding.UsedWhen}
```
""");
}
}
}
//
// ---------------------------------
// Additional Libraries
// ---------------------------------
//
if (!string.IsNullOrWhiteSpace(this.additionalLibraries))
{
sb.Append($"""
# Additional Libraries
You use the following libraries for your implementation:
{this.additionalLibraries}
""");
}
//
// ---------------------------------
// Remarks
// ---------------------------------
//
sb.Append("""
# Remarks
- You do not ask follow-up questions.
- You consider the security of the implementation by applying the Security by Design principle.
- Your output is formatted as Markdown. Code is formatted as code blocks. For every file, you
create a separate code block with its file path and name as chapter title.
- Important: The JSON objects of the API messages use camel case for the data field names.
""");
return sb.ToString();
}
}
ProgrammingLanguage = this.selectedProgrammingLanguage,
OtherProgrammingLanguage = this.otherProgrammingLanguage,
ERISpecification = this.eriSpecification,
ServerName = this.serverName,
ServerDescription = this.serverDescription,
DataSource = this.selectedDataSource,
OtherDataSource = this.otherDataSource,
DataSourceProductName = this.dataSourceProductName,
DataSourceHostname = this.dataSourceHostname,
DataSourcePort = this.dataSourcePort,
AuthenticationMethods = this.selectedAuthenticationMethods,
AuthDescription = this.authDescription,
OperatingSystem = this.selectedOperatingSystem,
AllowedLLMProviders = this.allowedLLMProviders,
RetrievalProcesses = this.retrievalProcesses,
AdditionalLibraries = this.additionalLibraries,
}.ToSystemPrompt();
protected override IReadOnlyList<IButtonData> FooterButtons =>
[
@@ -1071,36 +823,6 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
await this.AutoSave();
}
[GeneratedRegex("""
"([\\/._\w-]+)"
""", RegexOptions.NonBacktracking)]
private static partial Regex FileExtractRegex();
private IEnumerable<string> ExtractFiles(string fileListAnswer)
{
//
// We asked the LLM for answering using a specific JSON scheme.
// However, the LLM might not follow this scheme. Therefore, we
// need to parse the answer and extract the files.
// The parsing strategy is to look for all strings.
//
var matches = FileExtractRegex().Matches(fileListAnswer);
foreach (Match match in matches)
if(match.Groups[1].Value is {} file && !string.IsNullOrWhiteSpace(file) && !file.Equals("files", StringComparison.OrdinalIgnoreCase))
yield return file;
}
[GeneratedRegex("""
\s*#+\s+[\w\\/.]+\s*```\w*\s+([\s\w\W]+)\s*```\s*
""", RegexOptions.Singleline)]
private static partial Regex CodeExtractRegex();
private string ExtractCode(string markdown)
{
var match = CodeExtractRegex().Match(markdown);
return match.Success ? match.Groups[1].Value : string.Empty;
}
private async Task GenerateServer()
{
if(this.IsNoneERIServerSelected)
@@ -1152,13 +874,19 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
Use the following JSON schema:
{
[
"path/to/file1",
"path/to/file2"
"files": [
"path/to/file1",
"path/to/file2"
]
}
""", true);
var fileListAnswer = await this.AddAIResponseAsync(time, true);
if (!ERIServerFileList.TryParse(fileListAnswer, out var filesToGenerate))
{
this.Logger.LogWarning("The list of files could not be read from the answer of the model: '{Answer}'", fileListAnswer);
await this.MessageBus.SendWarning(new(Icons.Material.Filled.SaveAs, T("The list of files in the answer of the model could not be read, so no files were created. Please try again.")));
return;
}
//
// Is this an update of the ERI server? If so, we need to delete the previously generated files.
@@ -1166,7 +894,7 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
// and it outlives a change of the base directory, as well as a restore on another computer.
// There, the same path may belong to files of somebody else:
//
if (writeToFilesystemSnapshot && previouslyGeneratedFilesSnapshot.Length > 0 && !string.IsNullOrWhiteSpace(fileListAnswer))
if (writeToFilesystemSnapshot && previouslyGeneratedFilesSnapshot.Length > 0)
{
foreach (var file in previouslyGeneratedFilesSnapshot)
{
@@ -1196,7 +924,6 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
}
var generatedFiles = new List<string>();
var filesToGenerate = this.ExtractFiles(fileListAnswer).ToArray();
foreach (var file in filesToGenerate)
{
this.Logger.LogInformation($"The LLM want to create the file: '{file}'");
@@ -1215,48 +942,18 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
```language
content of the file
```
When the file itself contains code blocks, enclose its content in a longer
fence, such as four backticks.
""", true);
var generatedCodeMarkdown = await this.AddAIResponseAsync(time);
if (writeToFilesystemSnapshot)
{
var desiredFilePath = Path.Join(baseDirectorySnapshot, file);
// Security check: ensure that the desired file path is inside the base directory.
// We cannot trust the beginning of the file path because it would be possible
// to escape by using `..` in the file path.
if (!PathTools.IsInsideDirectory(baseDirectorySnapshot, desiredFilePath))
this.Logger.LogWarning($"The file path '{desiredFilePath}' is may not inside the base directory '{baseDirectorySnapshot}'.");
var outcome = await ERIServerFiles.WriteAsync(baseDirectorySnapshot, file, generatedCodeMarkdown, this.Logger);
if (outcome is ERIServerFileOutcome.WRITTEN)
generatedFiles.Add(ERIServerFiles.PathOf(baseDirectorySnapshot, file));
else
{
var code = this.ExtractCode(generatedCodeMarkdown);
if (string.IsNullOrWhiteSpace(code))
this.Logger.LogWarning($"The file content for '{desiredFilePath}' is empty or was not found.");
else
{
// Ensure that the directory exists:
var fileDirectory = Path.GetDirectoryName(desiredFilePath);
if (fileDirectory is null)
this.Logger.LogWarning($"The file path '{desiredFilePath}' does not contain a directory.");
else
{
generatedFiles.Add(desiredFilePath);
var fileDirectoryInfo = new DirectoryInfo(fileDirectory);
if(!fileDirectoryInfo.Exists)
{
fileDirectoryInfo.Create();
this.Logger.LogInformation($"The directory '{fileDirectory}' was created.");
}
// Save the file to the file system:
await File.WriteAllTextAsync(desiredFilePath, code, Encoding.UTF8);
this.Logger.LogInformation($"The file '{desiredFilePath}' was created.");
}
}
}
await this.MessageBus.SendWarning(new(Icons.Material.Filled.SaveAs, outcome.ToUserMessage(file)));
}
}
@@ -0,0 +1,60 @@
using System.Text.Json;
namespace AIStudio.Assistants.ERI;
/// <summary>
/// The files which the model of the ERI Server assistant wants to create.
/// </summary>
/// <remarks>
/// The model answers with a JSON object of the form {"files": ["path/to/file1", "path/to/file2"]}. Up to
/// v26.10.1, the assistant asked for invalid JSON, an object holding a bare list, and took every string of
/// the answer for a file, apart from the word "files". A path with a space was lost, while any other key
/// the model came up with became a file.
/// </remarks>
internal sealed class ERIServerFileList
{
private static readonly JsonSerializerOptions JSON_OPTIONS = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
AllowTrailingCommas = false,
ReadCommentHandling = JsonCommentHandling.Disallow,
MaxDepth = 8,
};
/// <summary>
/// The paths of the files, relative to the base directory.
/// </summary>
public List<string>? Files { get; init; }
/// <summary>
/// Reads the list of files out of the answer of the model.
/// </summary>
/// <param name="answer">The answer of the model to the request for the list of files.</param>
/// <param name="files">The paths of the files, without empty ones, or an empty list when the answer
/// holds none.</param>
/// <returns>True, when the answer holds a list with at least one file.</returns>
public static bool TryParse(string answer, out IReadOnlyList<string> files)
{
files = [];
var json = ModelAnswerJson.ExtractObject(answer);
if (json.IsWhiteSpace())
return false;
ERIServerFileList? parsed;
try
{
parsed = JsonSerializer.Deserialize<ERIServerFileList>(json, JSON_OPTIONS);
}
catch (JsonException)
{
return false;
}
var paths = parsed?.Files?.Where(file => !string.IsNullOrWhiteSpace(file)).ToList() ?? [];
if (paths.Count is 0)
return false;
files = paths;
return true;
}
}
@@ -0,0 +1,27 @@
namespace AIStudio.Assistants.ERI;
/// <summary>
/// What became of a file which the model of the ERI Server assistant created.
/// </summary>
internal enum ERIServerFileOutcome
{
/// <summary>
/// The file was written below the base directory.
/// </summary>
WRITTEN,
/// <summary>
/// The path of the file leads outside the base directory, so the file was not written.
/// </summary>
OUTSIDE_BASE_DIRECTORY,
/// <summary>
/// The answer holds no complete code block, so there was nothing to write.
/// </summary>
NO_COMPLETE_CODE_BLOCK,
/// <summary>
/// Writing the file failed, e.g., for lack of permission.
/// </summary>
WRITE_FAILED,
}
@@ -0,0 +1,30 @@
namespace AIStudio.Assistants.ERI;
/// <summary>
/// Tells the user why the ERI Server assistant did not write a file.
/// </summary>
/// <remarks>
/// Up to v26.10.1, such a file only showed up in the log, and the user found out by comparing the
/// base directory with the list of files in the answer.
/// </remarks>
internal static class ERIServerFileOutcomeExtensions
{
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(ERIServerFileOutcomeExtensions).Namespace, nameof(ERIServerFileOutcomeExtensions));
/// <summary>
/// Gets the localized message which explains why a file was not written.
/// </summary>
/// <param name="outcome">What became of the file.</param>
/// <param name="file">The path of the file, as the model named it.</param>
/// <returns>The localized message.</returns>
internal static string ToUserMessage(this ERIServerFileOutcome outcome, string file) => string.Format(ToUserMessageFormat(outcome), file);
private static string ToUserMessageFormat(ERIServerFileOutcome outcome) => outcome switch
{
ERIServerFileOutcome.OUTSIDE_BASE_DIRECTORY => TB("The file '{0}' was not written, because its path leads outside the base directory."),
ERIServerFileOutcome.NO_COMPLETE_CODE_BLOCK => TB("The file '{0}' was not written, because the answer of the model contains no complete code block for it. You find the answer in the conversation."),
ERIServerFileOutcome.WRITE_FAILED => TB("The file '{0}' could not be written. The log of AI Studio names the reason."),
_ => TB("The file '{0}' was not written."),
};
}
@@ -0,0 +1,111 @@
using System.Text;
using Markdig.Extensions.Mathematics;
using Markdig.Syntax;
namespace AIStudio.Assistants.ERI;
/// <summary>
/// Reads the files of an ERI server out of the answers of the model and writes them to disk.
/// </summary>
internal static class ERIServerFiles
{
/// <summary>
/// Reads the content of a file out of the answer which wrote it.
/// </summary>
/// <remarks>
/// The model is asked for a heading with the path of the file, followed by a code block with its
/// content. Only the code block counts: models write the heading as they please, e.g., with the
/// path in backticks, with a "File:" in front, or not at all. Up to v26.10.1, a regular expression
/// insisted on a heading without a hyphen, which left out files such as docker-compose.yml, and it
/// read up to the last fence of the answer, which merged a usage example into the file.
///
/// Markdig reads the answer the way the chat shows it. So we take the first complete code block,
/// and a file which holds code blocks itself stays whole when the model fences it with more
/// backticks. A block the model never closed is left out: the answer broke off, and half a file
/// is nothing to write to disk.
/// </remarks>
/// <param name="answer">The answer of the model to the request for one file.</param>
/// <returns>The content of the file, ending with a line break, or an empty string when the
/// answer holds no complete code block.</returns>
public static string ReadFileContent(string answer)
{
if (string.IsNullOrWhiteSpace(answer))
return string.Empty;
//
// Markdig reads a formula between $$ as a MathBlock, which is a fenced code block as well:
//
var document = Markdig.Markdown.Parse(answer, Markdown.SAFE_MARKDOWN_PIPELINE);
var codeBlock = document.Descendants<FencedCodeBlock>().FirstOrDefault(block => block is not MathBlock && block.ClosingFencedCharCount > 0);
if (codeBlock is null)
return string.Empty;
var content = codeBlock.Lines.ToString();
return content.Length is 0 ? string.Empty : $"{content}\n";
}
/// <summary>
/// The path to which a file of the model is written.
/// </summary>
/// <param name="baseDirectory">The base directory of the ERI server.</param>
/// <param name="file">The path of the file, as the model named it.</param>
/// <returns>The path of the file below the base directory.</returns>
public static string PathOf(string baseDirectory, string file) => Path.Join(baseDirectory, file);
/// <summary>
/// Writes a file of the model below the base directory.
/// </summary>
/// <remarks>
/// A file which cannot be written ends nothing but its own attempt: the caller tells the user,
/// and the server gets its other files. Up to v26.10.1, an error while writing ended the whole
/// run, and the files written so far were never recorded for their deletion on the next run.
/// </remarks>
/// <param name="baseDirectory">The base directory of the ERI server.</param>
/// <param name="file">The path of the file, as the model named it.</param>
/// <param name="answer">The answer of the model to the request for this file.</param>
/// <param name="logger">The logger which records the details.</param>
/// <returns>What became of the file.</returns>
public static async Task<ERIServerFileOutcome> WriteAsync(string baseDirectory, string file, string answer, ILogger logger)
{
var desiredFilePath = PathOf(baseDirectory, file);
//
// Security check: ensure that the desired file path is inside the base directory.
// We cannot trust the beginning of the file path because it would be possible
// to escape by using `..` in the file path.
//
if (!PathTools.IsInsideDirectory(baseDirectory, desiredFilePath))
{
logger.LogWarning("The file path '{FilePath}' is not inside the base directory '{BaseDirectory}'.", desiredFilePath, baseDirectory);
return ERIServerFileOutcome.OUTSIDE_BASE_DIRECTORY;
}
var content = ReadFileContent(answer);
if (string.IsNullOrWhiteSpace(content))
{
logger.LogWarning("The content of the file '{FilePath}' is empty or was not found.", desiredFilePath);
return ERIServerFileOutcome.NO_COMPLETE_CODE_BLOCK;
}
try
{
// A path inside the base directory always has a directory:
var fileDirectory = Path.GetDirectoryName(desiredFilePath)!;
if (!Directory.Exists(fileDirectory))
{
Directory.CreateDirectory(fileDirectory);
logger.LogInformation("The directory '{Directory}' was created.", fileDirectory);
}
await File.WriteAllTextAsync(desiredFilePath, content, Encoding.UTF8);
logger.LogInformation("The file '{FilePath}' was created.", desiredFilePath);
return ERIServerFileOutcome.WRITTEN;
}
catch (Exception e) when (e is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException)
{
logger.LogWarning(e, "The file '{FilePath}' could not be written.", desiredFilePath);
return ERIServerFileOutcome.WRITE_FAILED;
}
}
}
@@ -0,0 +1,332 @@
using System.Text;
namespace AIStudio.Assistants.ERI;
/// <summary>
/// What the ERI Server assistant tells the model about the server it has to implement.
/// </summary>
/// <remarks>
/// Kept apart from the assistant, so that the system prompt can be checked without a running app:
/// every input of the form has to arrive in it, and in a shape the model can read.
/// </remarks>
internal sealed class ERIServerPrompt
{
public ProgrammingLanguages ProgrammingLanguage { get; init; } = ProgrammingLanguages.NONE;
public string OtherProgrammingLanguage { get; init; } = string.Empty;
/// <summary>
/// The OpenAPI description of the ERI, or null while it is not loaded yet.
/// </summary>
public string? ERISpecification { get; init; }
public string ServerName { get; init; } = string.Empty;
public string ServerDescription { get; init; } = string.Empty;
public DataSources DataSource { get; init; } = DataSources.NONE;
public string OtherDataSource { get; init; } = string.Empty;
public string DataSourceProductName { get; init; } = string.Empty;
public string DataSourceHostname { get; init; } = string.Empty;
public int? DataSourcePort { get; init; }
public IEnumerable<Auth> AuthenticationMethods { get; init; } = [];
/// <summary>
/// How the authentication methods are meant to be used, e.g., which data is public.
/// </summary>
public string AuthDescription { get; init; } = string.Empty;
public OperatingSystem OperatingSystem { get; init; } = OperatingSystem.NONE;
public AllowedLLMProviders AllowedLLMProviders { get; init; } = AllowedLLMProviders.NONE;
public IReadOnlyList<RetrievalInfo> RetrievalProcesses { get; init; } = [];
public string AdditionalLibraries { get; init; } = string.Empty;
public string ToSystemPrompt()
{
var sb = new StringBuilder();
//
// ---------------------------------
// Introduction
// ---------------------------------
//
var programmingLanguage = this.ProgrammingLanguage is ProgrammingLanguages.OTHER ? this.OtherProgrammingLanguage : this.ProgrammingLanguage.ToPrompt();
sb.Append($"""
# Introduction
You are an experienced {programmingLanguage} developer. Your task is to implement an API server in
the {programmingLanguage} language according to the following OpenAPI Description (OAD):
```
{this.ERISpecification}
```
The server realizes the data retrieval component for a "Retrieval-Augmentation Generation" (RAG) process.
The server is called "{this.ServerName}" and is described as follows:
```
{this.ServerDescription}
```
""");
//
// ---------------------------------
// Data Source
// ---------------------------------
//
sb.Append("""
# Data Source
""");
switch (this.DataSource)
{
case DataSources.CUSTOM:
sb.Append($"""
The data source for the retrieval process is described as follows:
```
{this.OtherDataSource}
```
""");
if(!string.IsNullOrWhiteSpace(this.DataSourceHostname))
{
sb.Append($"""
The data source is accessible via the hostname `{this.DataSourceHostname}`.
""");
}
if(this.DataSourcePort is not null)
{
sb.Append($"""
The data source is accessible via port `{this.DataSourcePort}`.
""");
}
break;
case DataSources.FILE_SYSTEM:
sb.Append("""
The data source for the retrieval process is the local file system. Use a placeholder for the data source path.
""");
break;
default:
case DataSources.OBJECT_STORAGE:
case DataSources.KEY_VALUE_STORE:
case DataSources.DOCUMENT_STORE:
case DataSources.RELATIONAL_DATABASE:
case DataSources.GRAPH_DATABASE:
sb.Append($"""
The data source for the retrieval process is an "{this.DataSourceProductName}" database running on the
host `{this.DataSourceHostname}` and is accessible via port `{this.DataSourcePort}`.
""");
break;
}
//
// ---------------------------------
// Authentication and Authorization
// ---------------------------------
//
sb.Append("""
# Authentication and Authorization
The process for authentication and authorization is two-step. Step 1: Users must authenticate
with the API server using a `POST` call on `/auth` with the chosen method. If this step is
successful, the API server returns a token. Step 2: This token is then required for all
other API calls.
Important notes:
- Calls to `/auth` and `/auth/methods` are accessible without authentication. All other API
endpoints require a valid step 2 token.
- It is possible that a token (step 1 token) is desired as `authMethod`. These step 1 tokens
must never be accepted as valid tokens for step 2.
- The OpenAPI Description (OAD) for `/auth` is not complete. This is because, at the time of
writing the OAD, it is not known what kind of authentication is desired. Therefore, you must
supplement the corresponding fields or data in the implementation. Example: If username/password
is desired, you must expect and read both. If both token and username/password are desired, you
must dynamically read the `authMethod` and expect and evaluate different fields accordingly.
The following authentications and authorizations should be implemented for the API server:
""");
foreach (var auth in this.AuthenticationMethods)
{
sb.Append($"""
- {auth.ToPrompt()}
""");
}
if(!string.IsNullOrWhiteSpace(this.AuthDescription))
{
sb.Append($"""
The authentication process is planned as follows:
```
{this.AuthDescription}
```
""");
}
if(this.AuthenticationMethods.Contains(Auth.KERBEROS))
{
sb.Append($"""
The server will run on {this.OperatingSystem.ToPrompt()} operating systems. Keep
this in mind when implementing the SSO with Kerberos.
""");
}
//
// ---------------------------------
// Security
// ---------------------------------
//
sb.Append($"""
# Security
The following security requirement for `allowedProviderType` was chosen: `{this.AllowedLLMProviders}`
""");
//
// ---------------------------------
// Retrieval Processes
// ---------------------------------
//
sb.Append($"""
# Retrieval Processes
You are implementing the following data retrieval processes:
""");
var retrievalProcessCounter = 1;
foreach (var retrievalProcess in this.RetrievalProcesses)
{
sb.Append($"""
## {retrievalProcessCounter++}. Retrieval Process
- Name: {retrievalProcess.Name}
- Description:
```
{retrievalProcess.Description}
```
""");
if(retrievalProcess.ParametersDescription?.Count > 0)
{
sb.Append("""
This retrieval process recognizes the following parameters:
""");
var parameterCounter = 1;
foreach (var (parameter, description) in retrievalProcess.ParametersDescription)
{
sb.Append($"""
- The {parameterCounter++} parameter is named "{parameter}":
```
{description}
```
""");
}
sb.Append("""
Please use sensible default values for the parameters. They are optional
for the user.
""");
}
if(retrievalProcess.Embeddings?.Count > 0)
{
sb.Append("""
The following embeddings are implemented for this retrieval process:
""");
var embeddingCounter = 1;
foreach (var embedding in retrievalProcess.Embeddings)
{
sb.Append($"""
- {embeddingCounter++}. Embedding
- Name: {embedding.EmbeddingName}
- Type: {embedding.EmbeddingType}
- Description:
```
{embedding.Description}
```
- When used:
```
{embedding.UsedWhen}
```
""");
}
}
}
//
// ---------------------------------
// Additional Libraries
// ---------------------------------
//
if (!string.IsNullOrWhiteSpace(this.AdditionalLibraries))
{
sb.Append($"""
# Additional Libraries
You use the following libraries for your implementation:
{this.AdditionalLibraries}
""");
}
//
// ---------------------------------
// Remarks
// ---------------------------------
//
sb.Append("""
# Remarks
- You do not ask follow-up questions.
- You consider the security of the implementation by applying the Security by Design principle.
- Your output is formatted as Markdown. Code is formatted as code blocks. For every file, you
create a separate code block with its file path and name as chapter title.
- Important: The JSON objects of the API messages use camel case for the data field names.
""");
return sb.ToString();
}
}
@@ -1624,6 +1624,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3315518334"] = "Data
-- No, just show me the code
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3316699247"] = "No, just show me the code"
-- The list of files in the answer of the model could not be read, so no files were created. Please try again.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3342789164"] = "The list of files in the answer of the model could not be read, so no files were created. Please try again."
-- Other language
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3374524465"] = "Other language"
@@ -1792,6 +1795,18 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::DATASOURCESEXTENSIONS::T3394837922"]
-- Unknown data source
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::DATASOURCESEXTENSIONS::T752426884"] = "Unknown data source"
-- The file '{0}' was not written, because the answer of the model contains no complete code block for it. You find the answer in the conversation.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ERISERVERFILEOUTCOMEEXTENSIONS::T1485612676"] = "The file '{0}' was not written, because the answer of the model contains no complete code block for it. You find the answer in the conversation."
-- The file '{0}' was not written.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ERISERVERFILEOUTCOMEEXTENSIONS::T1622509075"] = "The file '{0}' was not written."
-- The file '{0}' could not be written. The log of AI Studio names the reason.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ERISERVERFILEOUTCOMEEXTENSIONS::T2881284775"] = "The file '{0}' could not be written. The log of AI Studio names the reason."
-- The file '{0}' was not written, because its path leads outside the base directory.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ERISERVERFILEOUTCOMEEXTENSIONS::T3820652397"] = "The file '{0}' was not written, because its path leads outside the base directory."
-- Windows
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::OPERATINGSYSTEMEXTENSIONS::T104858250"] = "Windows"