using System; using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Sqlite; namespace AISApp { // JSON DTOs for API public sealed class ChatMessage { [JsonPropertyName("role")] public string Role { get; set; } = ""; [JsonPropertyName("content")] public string Content { get; set; } = ""; } public sealed class ChatRequest { [JsonPropertyName("model")] public string Model { get; set; } = ""; [JsonPropertyName("messages")] public List Messages { get; set; } = new(); [JsonPropertyName("temperature")] public double Temperature { get; set; } = 0.0; } public sealed class ChatChoice { [JsonPropertyName("message")] public ChatMessage Message { get; set; } = new(); } public sealed class ChatResponse { [JsonPropertyName("choices")] public List Choices { get; set; } = new(); } public sealed class AIS { // -------- Static / constants -------- private static readonly HttpClient Http = new HttpClient(); private static readonly JsonSerializerOptions JsonOpts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, WriteIndented = false }; public static readonly Dictionary PredefinedModels = new(StringComparer.OrdinalIgnoreCase) { ["dobby"] = "sentientagi/dobby-mini-unhinged-plus-llama-3.1-8b", ["dolphin"] = "cognitivecomputations/dolphin-mixtral-8x22b", ["dolphin_free"] = "cognitivecomputations/dolphin3.0-mistral-24b:free", ["gemma"] = "google/gemma-3-12b-it", ["gpt-4o-mini"] = "openai/gpt-4o-mini", ["gpt-4.1-nano"] = "openai/gpt-4.1-nano", ["qwen"] = "qwen/qwen3-30b-a3b", ["unslop"] = "thedrummer/unslopnemo-12b", ["euryale"] = "sao10k/l3.3-euryale-70b", ["wizard"] = "microsoft/wizardlm-2-8x22b", ["deepseek"] = "deepseek/deepseek-chat-v3-0324" }; // -------- Instance state (mirrors Python) -------- public string Name { get; private set; } public double Temperature { get; private set; } public double Backoff { get; private set; } public int MaxRetries { get; private set; } public bool SafeMode { get; private set; } public string SystemMessage { get; private set; } public string Model { get; private set; } public string BaseUrl { get; private set; } public string Rel { get; private set; } public string ApiKey { get; private set; } public string DbName { get; private set; } public bool Loaded { get; private set; } private readonly List _messages = new(); private readonly SqliteConnection _conn; private Dictionary Headers => new() { ["Authorization"] = $"Bearer {ApiKey}", ["Content-Type"] = "application/json" }; // -------- ctor -------- public AIS( string? systemMessage = null, string model = "gemma", int maxRetries = 3, double backoff = 1, string? name = null, double temperature = 0.0, string dbName = ":memory:", bool safeMode = true, string baseUrl = "openrouter.ai", string rel = "/api", string? apiKey = null ) { Loaded = dbName != ":memory:" && File.Exists(dbName); DbName = dbName; // Open connection and create schema _conn = OpenConnection(dbName); InitDb(); // If existing DB, hydrate settings first if (Loaded) { temperature = GetSetting("temperature", temperature); name = GetSetting("name", name ?? ""); backoff = GetSetting("backoff", backoff); maxRetries = GetSetting("max_retries", maxRetries); safeMode = GetSetting("safe_mode", safeMode); systemMessage = GetSetting("system_message", systemMessage ?? ""); model = GetSetting("model", model); } if (string.IsNullOrEmpty(systemMessage)) { systemMessage = "You respond like a calculator; only the result is the answer. Respond only with the result. No explanations or extra words."; } SystemMessage = systemMessage; Name = !string.IsNullOrEmpty(name) ? $"ais_{name}" : $"ais_{Guid.NewGuid().ToString("N").Substring(0, 8)}"; Backoff = backoff; SafeMode = safeMode; MaxRetries = maxRetries; BaseUrl = baseUrl; Rel = rel; ApiKey = apiKey ?? Environment.GetEnvironmentVariable("OPENROUTER_API_KEY") ?? "sk-or-REPLACE_ME"; // keep explicit default like Python had Temperature = temperature; Model = PredefinedModels.TryGetValue(model, out var mapped) ? mapped : model; if (MessagesExist()) { _messages = LoadMessages(); } else { _messages.Add(new ChatMessage { Role = "system", Content = SystemMessage }); SaveMessage("system", SystemMessage); } Console.WriteLine($"[INFO] Initialized AIS name={Name} model={Model} db={DbName}"); } // -------- DB helpers -------- private static SqliteConnection OpenConnection(string dbName) { var cs = dbName == ":memory:" ? "Data Source=:memory:" : $"Data Source={dbName}"; var conn = new SqliteConnection(cs); conn.Open(); return conn; } private void InitDb() { using var cmd1 = _conn.CreateCommand(); cmd1.CommandText = @" CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, role TEXT NOT NULL, content TEXT NOT NULL )"; cmd1.ExecuteNonQuery(); using var cmd2 = _conn.CreateCommand(); cmd2.CommandText = @" CREATE TABLE IF NOT EXISTS settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, json_value TEXT NOT NULL )"; cmd2.ExecuteNonQuery(); } private bool MessagesExist() { using var cmd = _conn.CreateCommand(); cmd.CommandText = "SELECT COUNT(*) FROM messages"; var count = Convert.ToInt32(cmd.ExecuteScalar()); return count > 0; } private List LoadMessages() { var list = new List(); using var cmd = _conn.CreateCommand(); cmd.CommandText = "SELECT role, content FROM messages ORDER BY id ASC"; using var rdr = cmd.ExecuteReader(); while (rdr.Read()) { list.Add(new ChatMessage { Role = rdr.GetString(0), Content = rdr.GetString(1) }); } return list; } private void SaveMessage(string role, string content) { using var cmd = _conn.CreateCommand(); cmd.CommandText = "INSERT INTO messages (role, content) VALUES ($r, $c)"; cmd.Parameters.AddWithValue("$r", role); cmd.Parameters.AddWithValue("$c", content); cmd.ExecuteNonQuery(); } // Generic typed setting helpers private T GetSetting(string name, T defaultValue) { using var cmd = _conn.CreateCommand(); cmd.CommandText = "SELECT json_value FROM settings WHERE name = $n LIMIT 1"; cmd.Parameters.AddWithValue("$n", name); var obj = cmd.ExecuteScalar(); if (obj is string s) { try { var node = JsonNode.Parse(s); if (node is null) return defaultValue!; return node.Deserialize(JsonOpts)!; } catch { return defaultValue!; } } return defaultValue!; } private void SetSetting(string name, object? value) { var json = JsonSerializer.Serialize(value, JsonOpts); using (var upd = _conn.CreateCommand()) { upd.CommandText = "UPDATE settings SET json_value = $v WHERE name = $n"; upd.Parameters.AddWithValue("$v", json); upd.Parameters.AddWithValue("$n", name); var rows = upd.ExecuteNonQuery(); if (rows == 0) { using var ins = _conn.CreateCommand(); ins.CommandText = "INSERT INTO settings (name, json_value) VALUES ($n, $v)"; ins.Parameters.AddWithValue("$n", name); ins.Parameters.AddWithValue("$v", json); ins.ExecuteNonQuery(); } } // Reflectively update instance if property exists (case-insensitive) var prop = GetType().GetProperty(name, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.IgnoreCase); if (prop != null && prop.CanWrite) { try { var targetVal = value; if (value is JsonNode node) { targetVal = node.Deserialize(prop.PropertyType, JsonOpts); } else if (value is string str && prop.PropertyType != typeof(string)) { try { var jn = JsonNode.Parse(str); if (jn != null) targetVal = jn.Deserialize(prop.PropertyType, JsonOpts); } catch { /* ignore */ } } prop.SetValue(this, ConvertIfNeeded(targetVal, prop.PropertyType)); } catch { /* ignore */ } } } private static object? ConvertIfNeeded(object? value, Type targetType) { if (value == null) return null; if (targetType.IsAssignableFrom(value.GetType())) return value; try { return Convert.ChangeType(value, targetType); } catch { return value; } } private void SaveSettings() { SetSetting("name", Name); SetSetting("temperature", Temperature); SetSetting("backoff", Backoff); SetSetting("max_retries", MaxRetries); SetSetting("safe_mode", SafeMode); SetSetting("system_message", SystemMessage); SetSetting("model", Model); } // -------- Public API (mirrors Python) -------- public void Append(string messageUser, string messageAssistant, bool save = true) { _messages.Add(new ChatMessage { Role = "user", Content = messageUser }); if (save) SaveMessage("user", messageUser); _messages.Add(new ChatMessage { Role = "assistant", Content = messageAssistant }); if (save) SaveMessage("assistant", messageAssistant); } public void Save(string dbPath) { if (DbName != ":memory:") { Console.WriteLine("[INFO] Already using persistent database."); return; } var dbFile = dbPath.EndsWith(".db", StringComparison.OrdinalIgnoreCase) ? dbPath : dbPath + ".db"; using var dest = new SqliteConnection($"Data Source={dbFile}"); dest.Open(); _conn.BackupDatabase(dest); dest.Close(); DbName = dbFile; Console.WriteLine($"[INFO] Converted in-memory DB to persistent DB at {DbName}"); } public void PatchTime() { const string timePrefix = "Current time: `"; if (_messages.Count == 0) return; var content = _messages[0].Content ?? ""; var nowStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); if (content.StartsWith(timePrefix, StringComparison.Ordinal)) { var end = content.IndexOf('`', timePrefix.Length); if (end >= 0) { var rest = content.Substring(end + 1).TrimStart('\n'); _messages[0].Content = $"{timePrefix}{nowStr}`\n\n{rest}"; } else { _messages[0].Content = $"{timePrefix}{nowStr}`\n\n{content}"; } } else { _messages[0].Content = $"{timePrefix}{nowStr}`\n\n{content}"; } // Persist system message update using var cmd = _conn.CreateCommand(); cmd.CommandText = "UPDATE messages SET content = $c WHERE id = (SELECT MIN(id) FROM messages)"; cmd.Parameters.AddWithValue("$c", _messages[0].Content); cmd.ExecuteNonQuery(); } public async Task ChatAsync(string message, double? temperature = null, CancellationToken ct = default) { if (!SafeMode) { // command handling if (message.StartsWith("!reset", StringComparison.OrdinalIgnoreCase)) { Reset(); return "true"; } if (message.StartsWith("!instruct", StringComparison.OrdinalIgnoreCase)) { Instruct(message.Length >= 10 ? message[10..] : ""); return "true"; } if (message.StartsWith("!undo", StringComparison.OrdinalIgnoreCase)) { return Undo().ToString().ToLowerInvariant(); } if (message.StartsWith("!get", StringComparison.OrdinalIgnoreCase)) { var key = message[4..].Trim(); return GetRawSettingOr("not found", key); } if (message.StartsWith("!set", StringComparison.OrdinalIgnoreCase)) { var rest = message.Length >= 5 ? message[5..] : ""; var idx = rest.IndexOf(' '); if (idx > 0) { var nm = rest[..idx].Trim(); var val = rest[(idx + 1)..].Trim(); SetSetting(nm, TryJson(val)); return "true"; } } if (message.StartsWith("!query", StringComparison.OrdinalIgnoreCase)) { return Query(message.Length >= 6 ? message[6..] : ""); } if (message.StartsWith("!edit", StringComparison.OrdinalIgnoreCase)) { Edit(message.Length >= 5 ? message[5..] : ""); return SystemMessage; } if (message.StartsWith("!append", StringComparison.OrdinalIgnoreCase)) { var rest = message.Length >= 7 ? message[7..] : ""; var firstColon = rest.IndexOf(':'); if (firstColon >= 0 && rest.IndexOf(':', firstColon + 1) < 0) { var user = rest[..firstColon]; var assistant = rest[(firstColon + 1)..]; Append(user, assistant); return "true"; } } } PatchTime(); _messages.Add(new ChatMessage { Role = "user", Content = message }); SaveMessage("user", message); var payload = new ChatRequest { Model = Model, Messages = _messages, Temperature = temperature ?? Temperature }; var json = JsonSerializer.Serialize(payload, JsonOpts); var url = $"https://{BaseUrl}{Rel}/v1/chat/completions"; Console.WriteLine(url); int attempt = 0; string assistant = "ERROR"; double backoff = Backoff; while (attempt < MaxRetries) { try { using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; foreach (var kv in Headers) req.Headers.TryAddWithoutValidation(kv.Key, kv.Value); using var resp = await Http.SendAsync(req, ct); var text = await resp.Content.ReadAsStringAsync(ct); var parsed = JsonSerializer.Deserialize(text, JsonOpts); assistant = parsed?.Choices?[0]?.Message?.Content ?? "ERROR"; SaveSettings(); _messages.Add(new ChatMessage { Role = "assistant", Content = assistant }); SaveMessage("assistant", assistant); return TryJson(assistant); } catch (Exception ex) { attempt++; Console.WriteLine($"[WARN] Chat attempt {attempt} failed: {ex.Message}"); if (attempt < MaxRetries) { await Task.Delay(TimeSpan.FromSeconds(backoff), ct); backoff *= 2; } else { _messages.Add(new ChatMessage { Role = "assistant", Content = "ERROR" }); SaveMessage("assistant", "ERROR"); Console.WriteLine($"[CRITICAL] Failed after {MaxRetries} attempts."); return "ERROR"; } } } return "ERROR"; } public object Chat(string message, double? temperature = null) => ChatAsync(message, temperature).GetAwaiter().GetResult(); // JSON helper that mirrors Python _try_json (also strips ``` fences) private static object TryJson(string response) { var fixedStr = (response ?? "").Trim(); if (fixedStr.StartsWith("```", StringComparison.Ordinal) && fixedStr.EndsWith("```", StringComparison.Ordinal)) { var lines = fixedStr.Split('\n'); if (lines.Length >= 2) { fixedStr = string.Join("\n", lines, 1, lines.Length - 2).Trim(); } } try { var node = JsonNode.Parse(fixedStr); return node ?? response; } catch { return response; } } private object GetRawSettingOr(string fallback, string key) { using var cmd = _conn.CreateCommand(); cmd.CommandText = "SELECT json_value FROM settings WHERE name = $n LIMIT 1"; cmd.Parameters.AddWithValue("$n", key); var obj = cmd.ExecuteScalar(); return obj is string s ? s : fallback; } public void Reset() { Console.WriteLine("[INFO] Reset conversation history (keeping system message)."); ChatMessage sys = _messages.Count > 0 ? _messages[0] : new ChatMessage { Role = "system", Content = "" }; _messages.Clear(); _messages.Add(sys); using var cmd = _conn.CreateCommand(); cmd.CommandText = "DELETE FROM messages WHERE id != (SELECT MIN(id) FROM messages)"; cmd.ExecuteNonQuery(); } public void Instruct(string instructions) { SystemMessage = instructions; Console.WriteLine($"[INFO] System instructions updated."); SaveSettings(); if (_messages.Count > 0) { _messages[0].Content = instructions; using var cmd = _conn.CreateCommand(); cmd.CommandText = "UPDATE messages SET content = $c WHERE id = (SELECT MIN(id) FROM messages)"; cmd.Parameters.AddWithValue("$c", instructions); cmd.ExecuteNonQuery(); } else { _messages.Add(new ChatMessage { Role = "system", Content = instructions }); SaveMessage("system", instructions); } Reset(); } public object Query(string query) { var response = Chat(query); Undo(); return response; } private bool DeleteLastMessage() { try { if (_messages.Count == 0) return false; _messages.RemoveAt(_messages.Count - 1); using var cmd = _conn.CreateCommand(); cmd.CommandText = "DELETE FROM messages WHERE id = (SELECT MAX(id) FROM messages)"; cmd.ExecuteNonQuery(); return true; } catch (Exception e) { Console.WriteLine($"[ERROR] Undo error: {e.Message}"); return false; } } public bool Edit(string change) { var prompt = $"Apply this update: `{change}` to `{SystemMessage}` and return the result"; var result = Query(prompt); var newSys = result is JsonNode jn ? jn.ToJsonString() : result?.ToString() ?? SystemMessage; Instruct(newSys); return true; } public bool Undo() { // delete assistant + user return DeleteLastMessage() && DeleteLastMessage(); } public void Repl() { Console.WriteLine("[INFO] REPL started. Ctrl+C to quit."); while (true) { try { Console.Write("You: "); var line = Console.ReadLine(); if (string.IsNullOrWhiteSpace(line)) continue; var response = Chat(line); var typeName = response switch { JsonNode => "JsonNode", null => "null", _ => response.GetType().Name }; Console.WriteLine($"{Name}: {typeName} {FormatForPrint(response)}"); } catch (OperationCanceledException) { break; } catch (Exception ex) { Console.WriteLine($"[CRITICAL] Unexpected error: {ex.Message}"); Console.WriteLine("AI: ERROR"); } } } private static string FormatForPrint(object? obj) { if (obj is JsonNode jn) return jn.ToJsonString(new JsonSerializerOptions { WriteIndented = false }); return obj?.ToString() ?? "null"; } } internal static class Program { // Equivalent to: if __name__ == '__main__': ai = AIS(model="wizard", safe_mode=False); ai.repl() private static void Main(string[] args) { var ai = new AIS(model: "wizard", safeMode: false); ai.Repl(); } } }