diff --git a/AISApp/Program.cs b/AISApp/Program.cs index c18f8de..39e9037 100644 --- a/AISApp/Program.cs +++ b/AISApp/Program.cs @@ -1,650 +1,19 @@ 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; - -// ASP.NET Core usings (built-in) using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; +using AISApp; 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 = "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 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 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(text, JsonOpts); - //assistantResponse = parsed?.Choices?[0]?.Message?.Content ?? "ERROR"; - var parsed = JsonSerializer.Deserialize(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"; - } - } -/* - internal static class Program_old - { - // 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(); - } - } -*/ -internal static class ProgramEntry + internal static class ProgramEntry { // Minimal REST server + static file serving private sealed class ChatInput @@ -679,12 +48,14 @@ internal static class ProgramEntry FileProvider = fileProvider, RequestPath = "" }); + // Load system message from prompt.txt string systemMessage = System.IO.File.ReadAllText("prompt.txt"); // Single AIS instance for the app var ai = new AIS(model: "gemma", safeMode: false, systemMessage: systemMessage); - // Minimal chat endpoint + + // Minimal chat endpoint app.MapPost("/api/chat", async (HttpContext ctx) => { try @@ -708,4 +79,3 @@ internal static class ProgramEntry } } } -