631 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
		
		
			
		
	
	
			631 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
|  | 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<ChatMessage> 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<ChatChoice> 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<string, string> 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 List<ChatMessage> _messages = new(); | ||
|  |         private readonly SqliteConnection _conn; | ||
|  | 
 | ||
|  |         private Dictionary<string, string> Headers => new() | ||
|  |         { | ||
|  |             ["Authorization"] = $"Bearer {ApiKey}", | ||
|  |             ["Content-Type"]  = "application/json" | ||
|  |         }; | ||
|  | 
 | ||
|  |         // -------- ctor -------- | ||
|  |         public AIS( | ||
|  |             string? systemMessage = "You are an interviewer.", | ||
|  |             string model = "x-ai/grok-3-mini", | ||
|  |             int maxRetries = 3, | ||
|  |             double backoff = 1, | ||
|  |             string? name = null, | ||
|  |             double temperature = 0.0, | ||
|  |             string dbName = "tuna_ais_client.db", | ||
|  |             bool safeMode = true, | ||
|  |             string baseUrl = "openrouter.ai", | ||
|  |             string rel = "/api", | ||
|  |             string? apiKey = null  | ||
|  | 
 | ||
|  |         ) | ||
|  |         { | ||
|  |             Console.WriteLine($"[INFO] Initializing AIS name={name} model={model} db={dbName} api_key={apiKey}"); | ||
|  |             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<ChatMessage> LoadMessages() | ||
|  |         { | ||
|  |             var list = new List<ChatMessage>(); | ||
|  |             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<T>(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<T>(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<object> 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 assistantMessage = rest[(firstColon + 1)..]; | ||
|  |                         Append(user, assistantMessage); | ||
|  |                         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 assistantResponse = "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<ChatResponse>(text, JsonOpts); | ||
|  |                     //assistantResponse = parsed?.Choices?[0]?.Message?.Content ?? "ERROR"; | ||
|  |                     var parsed = JsonSerializer.Deserialize<ChatResponse>(text, JsonOpts); | ||
|  |                     if (parsed?.Choices != null && parsed.Choices.Count > 0) | ||
|  |                     { | ||
|  |                         var msg = parsed.Choices[0]?.Message?.Content; | ||
|  |                         assistantResponse = string.IsNullOrEmpty(msg) ? "ERROR" : msg!; | ||
|  |                     } | ||
|  |                     else | ||
|  |                     { | ||
|  |                         assistantResponse = text; | ||
|  |                     //    assistantResponse = "ERROR"; | ||
|  |                     } | ||
|  | 
 | ||
|  | 
 | ||
|  |                     SaveSettings(); | ||
|  | 
 | ||
|  |                     _messages.Add(new ChatMessage { Role = "assistant", Content = assistantResponse }); | ||
|  |                     SaveMessage("assistant", assistantResponse); | ||
|  | 
 | ||
|  |                     return TryJson(assistantResponse); | ||
|  |                 } | ||
|  |                 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"; | ||
|  |         } | ||
|  |     } | ||
|  | } |