History message
csharp
using System;
using System.Collections.Generic;
using System.Linq;
// Note: Requires NuGet package: Microsoft.ML.Tokenizers
using Microsoft.ML.Tokenizers;
namespace MultiProviderChat
{
// --- 1. UNIFIED DATA MODELS ---
public enum MessageRole { System, User, Assistant }
public class ChatMessage
{
public MessageRole Role { get; set; }
public string Content { get; set; }
}
// --- 2. PROVIDER INTERFACE ---
public interface IProviderAdapter
{
object MapToProvider(IEnumerable<ChatMessage> history);
int CountHistoryTokens(IEnumerable<ChatMessage> history);
}
// --- 3. TOKEN UTILITY ---
public static class TokenUtility
{
// OpenAI models primarily use 'cl100k_base' or 'o200k_base'.
private static readonly Tokenizer _tokenizer = TiktokenTokenizer.CreateForModel("gpt-4o");
public static int CountTokens(string text) =>
string.IsNullOrEmpty(text) ? 0 : _tokenizer.CountTokens(text);
}
// --- 4. CONCRETE ADAPTERS ---
// OpenAI & Ollama: Both use the standard { role, content } format.
public class OpenAIAdapter : IProviderAdapter
{
public object MapToProvider(IEnumerable<ChatMessage> history) =>
history.Select(m => new { role = m.Role.ToString().ToLower(), content = m.Content }).ToList();
public int CountHistoryTokens(IEnumerable<ChatMessage> history)
{
int tokens = 3; // Base overhead
foreach (var msg in history)
{
tokens += 4 + TokenUtility.CountTokens(msg.Content);
}
return tokens;
}
}
// Google Gemini: Uses "contents" with "role" (user/model) and "parts".
// Note: 'Assistant' is mapped to 'model'.
public class GeminiAdapter : IProviderAdapter
{
public object MapToProvider(IEnumerable<ChatMessage> history)
{
return history
.Where(m => m.Role != MessageRole.System) // System instructions are separate in Gemini
.Select(m => new
{
role = m.Role == MessageRole.User ? "user" : "model",
parts = new[] { new { text = m.Content } }
}).ToList();
}
public string ExtractSystemInstruction(IEnumerable<ChatMessage> history) =>
history.FirstOrDefault(m => m.Role == MessageRole.System)?.Content ?? "";
public int CountHistoryTokens(IEnumerable<ChatMessage> history) =>
history.Sum(m => TokenUtility.CountTokens(m.Content)) + (history.Count() * 2);
}
// Anthropic: Uses 'user' and 'assistant'. System prompts are separate.
public class AnthropicAdapter : IProviderAdapter
{
public object MapToProvider(IEnumerable<ChatMessage> history) =>
history.Where(m => m.Role != MessageRole.System)
.Select(m => new { role = m.Role == MessageRole.User ? "user" : "assistant", content = m.Content })
.ToList();
public int CountHistoryTokens(IEnumerable<ChatMessage> history) =>
history.Sum(m => TokenUtility.CountTokens(m.Content)) + (history.Count() * 3);
}
// --- 5. THE FACTORY ---
public static class ProviderFactory
{
public static IProviderAdapter GetAdapter(string providerName) =>
providerName?.ToLower() switch
{
"openai" or "ollama" => new OpenAIAdapter(),
"gemini" => new GeminiAdapter(),
"anthropic" => new AnthropicAdapter(),
_ => throw new NotSupportedException($"Provider '{providerName}' is not supported.")
};
}
// --- 6. EXECUTION EXAMPLE ---
public class Program
{
public static void Main()
{
var history = new List<ChatMessage>
{
new ChatMessage { Role = MessageRole.System, Content = "You are a helpful assistant." },
new ChatMessage { Role = MessageRole.User, Content = "Hello Gemini!" },
new ChatMessage { Role = MessageRole.Assistant, Content = "Hi! I am the Gemini model." }
};
// Process for Gemini
var geminiAdapter = (GeminiAdapter)ProviderFactory.GetAdapter("gemini");
var geminiPayload = geminiAdapter.MapToProvider(history);
var systemText = geminiAdapter.ExtractSystemInstruction(history);
Console.WriteLine($"Gemini System Instruction: {systemText}");
Console.WriteLine($"Gemini History Count (User/Model turns): {((dynamic)geminiPayload).Count}");
Console.WriteLine($"Estimated Tokens: {geminiAdapter.CountHistoryTokens(history)}");
}
}
}
; // Requires NuGet: Microsoft.Data.Sqlite
public class ChatRepository
{
private readonly string _connectionString;
public ChatRepository(string folderName)
{
// Ensure folder exists
Directory.CreateDirectory(folderName);
string dbPath = Path.Combine(folderName, "chat_history.db");
_connectionString = $"Data Source={dbPath}";
InitializeDatabase();
}
private void InitializeDatabase()
{
using var connection = new SqliteConnection(_connectionString);
connection.Open();
var command = connection.CreateCommand();
command.CommandText = @"
CREATE TABLE IF NOT EXISTS Messages (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
ConversationId TEXT,
Role TEXT,
Content TEXT,
Timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);";
command.ExecuteNonQuery();
}
public void SaveMessage(string conversationId, ChatMessage message)
{
using var connection = new SqliteConnection(_connectionString);
connection.Open();
var command = connection.CreateCommand();
command.CommandText = @"
INSERT INTO Messages (ConversationId, Role, Content)
VALUES ($convId, $role, $content);";
command.Parameters.AddWithValue("$convId", conversationId);
command.Parameters.AddWithValue("$role", message.Role.ToString());
command.Parameters.AddWithValue("$content", message.Content);
command.ExecuteNonQuery();
}
public List<ChatMessage> GetHistory(string conversationId)
{
var history = new List<ChatMessage>();
using var connection = new SqliteConnection(_connectionString);
connection.Open();
var command = connection.CreateCommand();
command.CommandText = "SELECT Role, Content FROM Messages WHERE ConversationId = $convId ORDER BY Timestamp ASC";
command.Parameters.AddWithValue("$convId", conversationId);
using var reader = command.ExecuteReader();
while (reader.Read())
{
history.Add(new ChatMessage
{
Role = Enum.Parse<MessageRole>(reader.GetString(0)),
Content = reader.GetString(1)
});
}
return history;
}
}
csharppublic static void Main()
{
var repo = new ChatRepository("MyAppData/ChatLogs");
string sessionId = "session_001";
// 1. Save a new message
var userMsg = new ChatMessage { Role = MessageRole.User, Content = "Hello!" };
repo.SaveMessage(sessionId, userMsg);
// 2. Read history back
var history = repo.GetHistory(sessionId);
// 3. Convert for a provider
var adapter = ProviderFactory.GetAdapter("openai");
var payload = adapter.MapToProvider(history);
}
Comments
Post a Comment