Refactor message handling to use TextMessage and introduce IMessage interfaces

This commit is contained in:
Thorsten Sommer 2025-12-28 13:37:07 +01:00
parent ef3d58cbee
commit b61e83596a
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
30 changed files with 96 additions and 49 deletions

View File

@ -1,4 +1,6 @@
namespace AIStudio.Chat;
using AIStudio.Provider;
namespace AIStudio.Chat;
public static class ListContentBlockExtensions
{
@ -7,9 +9,8 @@ public static class ListContentBlockExtensions
/// </summary>
/// <param name="blocks">The list of content blocks to process.</param>
/// <param name="transformer">A function that transforms each content block into a message result asynchronously.</param>
/// <typeparam name="TResult">The type of the result produced by the transformation function.</typeparam>
/// <returns>An asynchronous task that resolves to a list of transformed results.</returns>
public static async Task<IList<TResult>> BuildMessages<TResult>(this List<ContentBlock> blocks, Func<ContentBlock, Task<TResult>> transformer)
public static async Task<IList<IMessageBase>> BuildMessages(this List<ContentBlock> blocks, Func<ContentBlock, Task<IMessageBase>> transformer)
{
var messages = blocks
.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text))

View File

@ -30,7 +30,7 @@ public sealed class ProviderAlibabaCloud() : BaseProvider("https://dashscope-int
yield break;
// Prepare the system prompt:
var systemPrompt = new Message
var systemPrompt = new TextMessage
{
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
@ -40,7 +40,7 @@ public sealed class ProviderAlibabaCloud() : BaseProvider("https://dashscope-int
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessages(async n => new Message
var messages = await chatThread.Blocks.BuildMessages(async n => new TextMessage
{
Role = n.Role switch
{

View File

@ -1,5 +1,4 @@
using System.Text.Json.Serialization;
using AIStudio.Provider.OpenAI;
namespace AIStudio.Provider.Anthropic;
@ -13,7 +12,7 @@ namespace AIStudio.Provider.Anthropic;
/// <param name="System">The system prompt for the chat completion.</param>
public readonly record struct ChatRequest(
string Model,
IList<Message> Messages,
IList<IMessageBase> Messages,
int MaxTokens,
bool Stream,
string System

View File

@ -31,7 +31,7 @@ public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.co
var apiParameters = this.ParseAdditionalApiParameters("system");
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessages(async n => new Message
var messages = await chatThread.Blocks.BuildMessages(async n => new TextMessage
{
Role = n.Role switch
{

View File

@ -30,7 +30,7 @@ public sealed class ProviderDeepSeek() : BaseProvider("https://api.deepseek.com/
yield break;
// Prepare the system prompt:
var systemPrompt = new Message
var systemPrompt = new TextMessage
{
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
@ -40,7 +40,7 @@ public sealed class ProviderDeepSeek() : BaseProvider("https://api.deepseek.com/
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessages(async n => new Message
var messages = await chatThread.Blocks.BuildMessages(async n => new TextMessage
{
Role = n.Role switch
{

View File

@ -10,7 +10,7 @@ namespace AIStudio.Provider.Fireworks;
/// <param name="Stream">Whether to stream the chat completion.</param>
public readonly record struct ChatRequest(
string Model,
IList<Message> Messages,
IList<IMessageBase> Messages,
bool Stream
)
{

View File

@ -30,7 +30,7 @@ public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/infere
yield break;
// Prepare the system prompt:
var systemPrompt = new Message
var systemPrompt = new TextMessage
{
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
@ -40,7 +40,7 @@ public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/infere
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessages(async n => new Message
var messages = await chatThread.Blocks.BuildMessages(async n => new TextMessage
{
Role = n.Role switch
{

View File

@ -5,4 +5,9 @@ namespace AIStudio.Provider.Fireworks;
/// </summary>
/// <param name="Content">The text content of the message.</param>
/// <param name="Role">The role of the message.</param>
public readonly record struct Message(string Content, string Role);
public record TextMessage(string Content, string Role) : IMessage<string>
{
public TextMessage() : this(string.Empty, string.Empty)
{
}
}

View File

@ -30,7 +30,7 @@ public sealed class ProviderGWDG() : BaseProvider("https://chat-ai.academiccloud
yield break;
// Prepare the system prompt:
var systemPrompt = new Message
var systemPrompt = new TextMessage
{
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
@ -40,7 +40,7 @@ public sealed class ProviderGWDG() : BaseProvider("https://chat-ai.academiccloud
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessages(async n => new Message
var messages = await chatThread.Blocks.BuildMessages(async n => new TextMessage
{
Role = n.Role switch
{

View File

@ -1,5 +1,4 @@
using System.Text.Json.Serialization;
using AIStudio.Provider.OpenAI;
namespace AIStudio.Provider.Google;
@ -11,7 +10,7 @@ namespace AIStudio.Provider.Google;
/// <param name="Stream">Whether to stream the chat completion.</param>
public readonly record struct ChatRequest(
string Model,
IList<Message> Messages,
IList<IMessageBase> Messages,
bool Stream
)
{

View File

@ -30,7 +30,7 @@ public class ProviderGoogle() : BaseProvider("https://generativelanguage.googlea
yield break;
// Prepare the system prompt:
var systemPrompt = new Message
var systemPrompt = new TextMessage
{
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
@ -40,7 +40,7 @@ public class ProviderGoogle() : BaseProvider("https://generativelanguage.googlea
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessages(async n => new Message
var messages = await chatThread.Blocks.BuildMessages(async n => new TextMessage
{
Role = n.Role switch
{

View File

@ -1,5 +1,4 @@
using System.Text.Json.Serialization;
using AIStudio.Provider.OpenAI;
namespace AIStudio.Provider.Groq;
@ -12,7 +11,7 @@ namespace AIStudio.Provider.Groq;
/// <param name="Seed">The seed for the chat completion.</param>
public readonly record struct ChatRequest(
string Model,
IList<Message> Messages,
IList<IMessageBase> Messages,
bool Stream,
int Seed
)

View File

@ -30,7 +30,7 @@ public class ProviderGroq() : BaseProvider("https://api.groq.com/openai/v1/", LO
yield break;
// Prepare the system prompt:
var systemPrompt = new Message
var systemPrompt = new TextMessage
{
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
@ -40,7 +40,7 @@ public class ProviderGroq() : BaseProvider("https://api.groq.com/openai/v1/", LO
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessages(async n => new Message
var messages = await chatThread.Blocks.BuildMessages(async n => new TextMessage
{
Role = n.Role switch
{

View File

@ -30,7 +30,7 @@ public sealed class ProviderHelmholtz() : BaseProvider("https://api.helmholtz-bl
yield break;
// Prepare the system prompt:
var systemPrompt = new Message
var systemPrompt = new TextMessage
{
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
@ -40,7 +40,7 @@ public sealed class ProviderHelmholtz() : BaseProvider("https://api.helmholtz-bl
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessages(async n => new Message
var messages = await chatThread.Blocks.BuildMessages(async n => new TextMessage
{
Role = n.Role switch
{

View File

@ -35,7 +35,7 @@ public sealed class ProviderHuggingFace : BaseProvider
yield break;
// Prepare the system prompt:
var systemPrompt = new Message
var systemPrompt = new TextMessage
{
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
@ -45,7 +45,7 @@ public sealed class ProviderHuggingFace : BaseProvider
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages:
var message = await chatThread.Blocks.BuildMessages(async n => new Message
var message = await chatThread.Blocks.BuildMessages(async n => new TextMessage
{
Role = n.Role switch
{

View File

@ -0,0 +1,15 @@
namespace AIStudio.Provider;
/// <summary>
/// Standard interface for messages exchanged with AI models.
/// </summary>
/// <typeparam name="T">The type of the message content.</typeparam>
public interface IMessage<T> : IMessageBase
{
/// <summary>
/// Gets the main content of the message exchanged with the AI model.
/// The content encapsulates the core information or data being transmitted,
/// and its type can vary based on the specific implementation or use case.
/// </summary>
public T Content { get; init; }
}

View File

@ -0,0 +1,14 @@
namespace AIStudio.Provider;
/// <summary>
/// The none-generic base interface for messages exchanged with AI models.
/// </summary>
public interface IMessageBase
{
/// <summary>
/// Gets the role of the entity sending or receiving the message.
/// This property typically identifies whether the entity is acting
/// as a user, assistant, or system in the context of the interaction.
/// </summary>
public string Role { get; init; }
}

View File

@ -12,7 +12,7 @@ namespace AIStudio.Provider.Mistral;
/// <param name="SafePrompt">Whether to inject a safety prompt before all conversations.</param>
public readonly record struct ChatRequest(
string Model,
IList<RegularMessage> Messages,
IList<IMessageBase> Messages,
bool Stream,
int RandomSeed,
bool SafePrompt = false

View File

@ -28,7 +28,7 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/
yield break;
// Prepare the system prompt:
var systemPrompt = new RegularMessage
var systemPrompt = new TextMessage
{
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
@ -38,7 +38,7 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessages(async n => new RegularMessage
var messages = await chatThread.Blocks.BuildMessages(async n => new TextMessage
{
Role = n.Role switch
{

View File

@ -1,8 +1,13 @@
namespace AIStudio.Provider.Mistral;
/// <summary>
/// Regulat chat message model.
/// Text chat message model.
/// </summary>
/// <param name="Content">The text content of the message.</param>
/// <param name="Role">The role of the message.</param>
public readonly record struct RegularMessage(string Content, string Role);
public record TextMessage(string Content, string Role) : IMessage<string>
{
public TextMessage() : this(string.Empty, string.Empty)
{
}
}

View File

@ -10,7 +10,7 @@ namespace AIStudio.Provider.OpenAI;
/// <param name="Stream">Whether to stream the chat completion.</param>
public record ChatCompletionAPIRequest(
string Model,
IList<Message> Messages,
IList<IMessageBase> Messages,
bool Stream
)
{

View File

@ -70,7 +70,7 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"
LOGGER.LogInformation("Using the system prompt role '{SystemPromptRole}' and the '{RequestPath}' API for model '{ChatModelId}'.", systemPromptRole, requestPath, chatModel.Id);
// Prepare the system prompt:
var systemPrompt = new Message
var systemPrompt = new TextMessage
{
Role = systemPromptRole,
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
@ -90,7 +90,7 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"
var apiParameters = this.ParseAdditionalApiParameters("input", "store", "tools");
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessages(async n => new Message
var messages = await chatThread.Blocks.BuildMessages(async n => new TextMessage
{
Role = n.Role switch
{
@ -137,7 +137,7 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Input = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message
Input = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new TextMessage
{
Role = n.Role switch
{

View File

@ -12,7 +12,7 @@ namespace AIStudio.Provider.OpenAI;
/// <param name="Tools">The tools to use for the request.</param>
public record ResponsesAPIRequest(
string Model,
IList<Message> Input,
IList<TextMessage> Input,
bool Stream,
bool Store,
IList<Tool> Tools)

View File

@ -5,4 +5,9 @@ namespace AIStudio.Provider.OpenAI;
/// </summary>
/// <param name="Content">The text content of the message.</param>
/// <param name="Role">The role of the message.</param>
public readonly record struct Message(string Content, string Role);
public record TextMessage(string Content, string Role) : IMessage<string>
{
public TextMessage() : this(string.Empty, string.Empty)
{
}
}

View File

@ -33,7 +33,7 @@ public sealed class ProviderOpenRouter() : BaseProvider("https://openrouter.ai/a
yield break;
// Prepare the system prompt:
var systemPrompt = new Message
var systemPrompt = new TextMessage
{
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
@ -43,7 +43,7 @@ public sealed class ProviderOpenRouter() : BaseProvider("https://openrouter.ai/a
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessages(async n => new Message
var messages = await chatThread.Blocks.BuildMessages(async n => new TextMessage
{
Role = n.Role switch
{

View File

@ -39,7 +39,7 @@ public sealed class ProviderPerplexity() : BaseProvider("https://api.perplexity.
yield break;
// Prepare the system prompt:
var systemPrompt = new Message
var systemPrompt = new TextMessage
{
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
@ -49,7 +49,7 @@ public sealed class ProviderPerplexity() : BaseProvider("https://api.perplexity.
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessages(async n => new Message()
var messages = await chatThread.Blocks.BuildMessages(async n => new TextMessage()
{
Role = n.Role switch
{

View File

@ -10,7 +10,7 @@ namespace AIStudio.Provider.SelfHosted;
/// <param name="Stream">Whether to stream the chat completion.</param>
public readonly record struct ChatRequest(
string Model,
IList<Message> Messages,
IList<IMessageBase> Messages,
bool Stream
)
{

View File

@ -26,7 +26,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, isTrying: true);
// Prepare the system prompt:
var systemPrompt = new Message
var systemPrompt = new TextMessage
{
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
@ -36,7 +36,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessages(async n => new Message
var messages = await chatThread.Blocks.BuildMessages(async n => new TextMessage
{
Role = n.Role switch
{

View File

@ -5,4 +5,9 @@ namespace AIStudio.Provider.SelfHosted;
/// </summary>
/// <param name="Content">The text content of the message.</param>
/// <param name="Role">The role of the message.</param>
public readonly record struct Message(string Content, string Role);
public record TextMessage(string Content, string Role) : IMessage<string>
{
public TextMessage() : this(string.Empty, string.Empty)
{
}
}

View File

@ -30,7 +30,7 @@ public sealed class ProviderX() : BaseProvider("https://api.x.ai/v1/", LOGGER)
yield break;
// Prepare the system prompt:
var systemPrompt = new Message
var systemPrompt = new TextMessage
{
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
@ -40,7 +40,7 @@ public sealed class ProviderX() : BaseProvider("https://api.x.ai/v1/", LOGGER)
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessages(async n => new Message()
var messages = await chatThread.Blocks.BuildMessages(async n => new TextMessage
{
Role = n.Role switch
{