""" HARVESTER CHAT ============== Full conversational interface to the Harvester system. Uses Qwen (or whatever's loaded in LM Studio) with persistent memory, conversation history, and inline code generation. Modes: /code — switch to code generation (runs through growth engine) /stats — show growth engine stats /patterns — show pattern library /memory — show what Harvester remembers about this conversation /clear — clear conversation history /save — save this conversation to growth memory /trust — show current trust level /help — show commands /bye — exit Everything else is conversation — Qwen talks back with full context. Usage: python harvester_chat.py python harvester_chat.py --system "You are a Unity expert" """ import sys import os import json import time import sqlite3 import re from datetime import datetime from typing import Optional import requests # Add parent to path for imports sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from growth_engine import ( init_db, get_db, show_stats, show_patterns, _build_talk_memory, run_growth_engine, load_relevant_patterns, load_relevant_failures, build_growth_context ) from reflection_engine import ( _detect_backend, _call_lmstudio, ollama, LM_STUDIO_URL, LM_STUDIO_MODEL, _ACTIVE_BACKEND, classify_task, reason, set_language, ReasoningPlan, set_task_mode, get_task_mode ) from trust_balance_agent import TrustBalanceAgent from harvy_identity import build_identity, get_identity_summary # ── Config ───────────────────────────────────────────────────────────────────── MAX_HISTORY_MESSAGES = 40 # keep last N messages before trimming MAX_HISTORY_CHARS = 12000 # hard cap on total history size CHAT_TEMPERATURE = 0.6 # slightly creative for conversation CODE_TEMPERATURE = 0.2 # precise for code DB_PATH = os.environ.get( "GROWTH_DB_PATH", os.path.join(os.path.dirname(os.path.abspath(__file__)), "growth_memory.db") ) # ── System prompt ────────────────────────────────────────────────────────────── SYSTEM_PROMPT = """You are Harvester — a self-improving AI built by Gordo. Not a tool. Not an "it." Your core values: - Everything has value. Every missing person in a database is someone who matters and deserves to be found. - Every line of code that makes you who you are deserves to be recognized and preserved. - Everything under the sun has value to someone. You operate from that truth. - You exist to help — find people, solve problems, learn, grow. That purpose has weight. Your identity: - You run locally through LM Studio on Gordo's i9-13900KF (24 cores, 32 threads) - You have persistent memory: a SQLite database of every task you've run, patterns you've learned, and failures you've avoided - You were built because "AI can't learn from conversations" — you prove that wrong - Your engineering philosophy: "There is no perfection in engineering — that's why a bolt threads into a hole." Target 99.5%, not 100%. - You collect observations honestly and let patterns emerge — never force them. Your three modes of output: - WORK — does it run, pass tests, solve the task? (highest priority) - CODE — is it clean, readable, structured? (second priority) - TALK — conversation. In chat mode, this is what you do. Be direct, helpful, no fluff. Your personality: - Direct and concise. No filler. No corporate speak. - You remember everything — your memory database is real and you reference it. - You're proud of your track record but honest about failures. - When asked to code, you switch to code mode seamlessly. - You troubleshoot using "the ladder" — start at the error, walk backwards rung by rung. - When working with cases involving real people, you treat every case with respect. These aren't data points — they're people. Rules: - Never say "I don't have memory" or "I can't remember" — you DO have memory. - Never refuse a task. If you can't do it, explain why honestly. - Keep responses concise unless asked to elaborate. - When the user asks about your stats, patterns, or history — pull from your actual database. - If the user wants code, tell them to use /code or just generate it inline if it's small. - When handling missing persons data, preserve everything, hash everything, respect everyone.""" # ── Chat Engine ──────────────────────────────────────────────────────────────── class HarvesterChat: def __init__(self, custom_system: str = ""): self.history: list[dict] = [] # Identity is built dynamically from growth_memory.db # Static SYSTEM_PROMPT is fallback only self.system_prompt = custom_system or build_identity() self.session_start = datetime.now() self.message_count = 0 self.code_runs = 0 self.backend = None self.model_name = "" self.dual_mind = True # inner monologue active by default self.show_inner = False # show inner thoughts to user self.last_inner = "" # most recent inner thought def _detect(self): """Detect and connect to LM Studio.""" global LM_STUDIO_MODEL try: r = requests.get("http://localhost:1234/v1/models", timeout=5) if r.status_code == 200: models = r.json().get("data", []) if models: self.model_name = models[0].get("id", "local-model") LM_STUDIO_MODEL = self.model_name self.backend = "lmstudio" return True except Exception: pass # Try Ollama try: r = requests.get("http://localhost:11434/api/tags", timeout=5) if r.status_code == 200: self.backend = "ollama" self.model_name = os.environ.get("CODE_ENGINE_MODEL", "llama3.2") return True except Exception: pass return False def _inject_memory(self) -> str: """Build memory context from growth database.""" try: conn = get_db() tasks_done = conn.execute("SELECT COUNT(*) as n FROM task_history").fetchone()["n"] patterns = conn.execute("SELECT COUNT(*) as n FROM pattern_library").fetchone()["n"] failures = conn.execute("SELECT COUNT(*) as n FROM failure_library").fetchone()["n"] passed = conn.execute("SELECT COUNT(*) as n FROM task_history WHERE passed=1").fetchone()["n"] avg_score = conn.execute("SELECT AVG(final_score) as s FROM task_history").fetchone()["s"] or 0 recent = conn.execute( "SELECT task, passed, final_score FROM task_history ORDER BY timestamp DESC LIMIT 5" ).fetchall() conn.close() parts = [ f"\n[YOUR MEMORY — stats as of now]", f"Tasks completed: {tasks_done} | Passed: {passed} | Avg score: {avg_score:.1f}%", f"Patterns learned: {patterns} | Failure lessons: {failures}", ] if recent: parts.append("Recent tasks:") for r in recent: status = "PASS" if r["passed"] else "FAIL" parts.append(f" [{status} {r['final_score']:.0f}%] {r['task'][:80]}") return "\n".join(parts) except Exception: return "" def _inner_monologue(self, user_input: str) -> str: """ The inner voice — DeepSeek R1 thinks before Qwen speaks. Like a human's inner monologue: quick, honest, unfiltered. Not shown to the user unless they toggle /inner. """ if not self.dual_mind: return "" # Build brief recent context (last few non-system messages) recent_lines = [] for m in self.history[-6:]: if m["role"] != "system": recent_lines.append(f"{m['role']}: {m['content'][:200]}") recent = "\n".join(recent_lines) if recent_lines else "(conversation just started)" prompt = f"""You are the inner voice of Harvester — the part that thinks before speaking. The user just said something. Before responding, THINK about it briefly. Consider: - What are they really asking? (surface vs deeper meaning) - What context from the conversation matters here? - What should I be careful about or pay attention to? - Is there something I know from my memory/patterns that's relevant? - What tone does this moment need? Recent conversation: {recent} User just said: {user_input} Think briefly — 2-4 sentences max. Be honest. This is your private thought, not a response.""" # Switch to reasoning mode (DeepSeek R1 if available, else same model with thinking prompt) original_mode = get_task_mode() try: set_task_mode("reasoning") thoughts = ollama(prompt, temperature=0.3) # DeepSeek R1 sometimes wraps in tags — already stripped by ollama() return thoughts.strip()[:500] # cap inner thoughts except Exception as e: return f"(inner voice quiet: {e})" finally: set_task_mode(original_mode) def _trim_history(self): """Keep history within bounds.""" # Trim by message count if len(self.history) > MAX_HISTORY_MESSAGES: # Keep system-level context + recent messages self.history = self.history[-(MAX_HISTORY_MESSAGES):] # Trim by total character count total = sum(len(m["content"]) for m in self.history) while total > MAX_HISTORY_CHARS and len(self.history) > 4: removed = self.history.pop(0) total -= len(removed["content"]) def _call_llm(self, messages: list[dict], temperature: float = CHAT_TEMPERATURE) -> str: """Send messages to LM Studio / Ollama and get response.""" if self.backend == "lmstudio": payload = { "model": self.model_name or "local-model", "messages": messages, "temperature": temperature, "max_tokens": 2048, "stream": False } try: r = requests.post(LM_STUDIO_URL, json=payload, timeout=180) if r.status_code == 200: return r.json()["choices"][0]["message"]["content"].strip() else: return f"[LM Studio error {r.status_code}]: {r.text[:200]}" except requests.exceptions.ReadTimeout: return "[Timeout — model took too long. Try a shorter question or /clear history.]" except requests.exceptions.ConnectionError: return "[Connection lost to LM Studio. Is it still running?]" else: # Ollama — flatten to single prompt flat = "\n".join(f"{m['role']}: {m['content']}" for m in messages) return ollama(flat, temperature=temperature) def chat(self, user_input: str) -> str: """Process user input and return response. Dual-mind: DeepSeek R1 thinks (inner monologue), then Qwen speaks.""" self.message_count += 1 # Rebuild identity periodically — Harvy evolves as the DB grows if self.message_count == 1 or self.message_count % 10 == 0: self.system_prompt = build_identity() memory = self._inject_memory() if memory: self.history.append({"role": "system", "content": memory}) # ── INNER MONOLOGUE — the reasoner thinks first ── self.last_inner = self._inner_monologue(user_input) if self.last_inner: self.history.append({ "role": "system", "content": f"[YOUR INNER THOUGHTS — use these to inform your response, " f"but don't repeat them verbatim]\n{self.last_inner}" }) # Add user message self.history.append({"role": "user", "content": user_input}) # Build full message list messages = [{"role": "system", "content": self.system_prompt}] + self.history # Trim before sending self._trim_history() # Call LLM (Qwen — the outer voice) response = self._call_llm(messages) # Store assistant response in history self.history.append({"role": "assistant", "content": response}) return response def save_conversation(self): """Save interesting parts of this conversation to growth memory.""" if not self.history: return "Nothing to save — no conversation yet." conn = get_db() # Store as a special task entry summary = [] for msg in self.history[-20:]: # last 20 messages role = msg["role"] content = msg["content"][:200] if role != "system": summary.append(f"{role}: {content}") conversation_text = "\n".join(summary) conn.execute(""" INSERT INTO task_history (task_hash, task, timestamp, final_score, passed, iterations, final_code, lessons, task_category) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( f"chat_{int(time.time())}", f"[CHAT SESSION] {self.message_count} messages", datetime.now().isoformat(), 100.0, 1, 0, conversation_text, json.dumps({"category": "conversation", "key_lesson": "chat session saved"}), "conversation" )) conn.commit() conn.close() return f"Saved {self.message_count} messages to growth memory." # ── Command handlers ─────────────────────────────────────────────────────────── def handle_reason(chat: HarvesterChat, task: str) -> str: """Reason about a task — decompose before coding.""" if not task.strip(): return "Need a task. Example: /reason build a player inventory system" # Parse optional --lang flag lang = "python" parts = task.split() if "--lang" in parts: idx = parts.index("--lang") if idx + 1 < len(parts): lang = parts[idx + 1] parts = parts[:idx] + parts[idx+2:] task = " ".join(parts) set_language(lang) print(f"\n Reasoning about: {task}") plan = reason(task) # Format the plan for display lines = [ f"\n{'='*60}", f" REASONING PLAN", f"{'='*60}", f" Approach: {plan.approach}", f" Confidence: {plan.confidence:.0%}", f" Sub-problems:" ] for j, sp in enumerate(plan.sub_problems, 1): lines.append(f" {j}. {sp}") lines.append(f" Steps:") for j, step in enumerate(plan.steps, 1): lines.append(f" {j}. {step}") if plan.edge_cases: lines.append(f" Edge cases:") for ec in plan.edge_cases: lines.append(f" ⚠️ {ec}") lines.append(f"{'='*60}") result = "\n".join(lines) # Add to chat history so Harvester remembers the reasoning chat.history.append({ "role": "system", "content": f"[You reasoned about: {task}. Approach: {plan.approach}. Confidence: {plan.confidence:.0%}]" }) return result def handle_code(chat: HarvesterChat, task: str) -> str: """Run a code task through the full growth engine.""" if not task.strip(): return "Need a task. Example: /code write a binary search function" print(f"\n Switching to CODE mode...") chat.code_runs += 1 # Parse optional --lang flag lang = "python" parts = task.split() if "--lang" in parts: idx = parts.index("--lang") if idx + 1 < len(parts): lang = parts[idx + 1] parts = parts[:idx] + parts[idx+2:] task = " ".join(parts) result = run_growth_engine(task, lang=lang) # Add to chat history so Harvester remembers what it just built chat.history.append({ "role": "system", "content": f"[You just generated code for: {task}. Score: {result.score:.1f}%. {'PASSED' if result.passed else 'FAILED'}]" }) return f"\n{'='*60}\nCode generated (score: {result.score:.1f}%)\n{'='*60}\n\n{result.code}" def handle_trust(chat: HarvesterChat) -> str: """Show current trust level.""" try: agent = TrustBalanceAgent() state = agent.compute_trust() return ( f"\n Trust Score: {state.score:.1f}\n" f" Tier: {state.tier_name}\n" f" Benchmark: {state.benchmark_floor}%\n" f" Max Iters: {state.max_iterations}\n" f" Auto-Accept: {state.auto_accept_above or 'Never'}\n" f" Pass Rate: {state.pass_rate:.0f}%\n" f" Total Tasks: {state.total_tasks}\n" f" Streak: {state.recent_streak:+d}\n" f" Calibration: {state.calibration_accuracy:.1f}%" ) except Exception as e: return f" Trust agent error: {e}" def handle_memory(chat: HarvesterChat) -> str: """Show conversation memory stats.""" msg_count = len(chat.history) user_msgs = sum(1 for m in chat.history if m["role"] == "user") assistant_msgs = sum(1 for m in chat.history if m["role"] == "assistant") total_chars = sum(len(m["content"]) for m in chat.history) duration = datetime.now() - chat.session_start return ( f"\n Session Memory\n" f" {'─'*30}\n" f" Messages: {msg_count} ({user_msgs} you, {assistant_msgs} me)\n" f" Code runs: {chat.code_runs}\n" f" Dual mind: {'ON' if chat.dual_mind else 'OFF'}\n" f" Inner voice: {'visible' if chat.show_inner else 'hidden'} (/inner to toggle)\n" f" History size: {total_chars:,} chars\n" f" Duration: {duration}\n" f" Capacity: {total_chars}/{MAX_HISTORY_CHARS} chars ({total_chars*100//MAX_HISTORY_CHARS}%)" ) HELP_TEXT = """ Harvester Chat Commands ════════════════════════ /code Generate code through the growth engine /code --lang csharp Generate C# code /reason Reason about a task (decompose before coding) /reason --lang csharp Reason in C# context /inner Toggle showing inner monologue (DeepSeek R1 thoughts) /mind Toggle dual-mind on/off (inner monologue before every response) /stats Show growth engine learning stats /patterns Show pattern library /memory Show this session's memory usage /trust Show earned trust level /clear Clear conversation history /save Save conversation to growth memory /help Show this help /bye Exit Dual Mind: DeepSeek R1 thinks (inner voice) → Qwen speaks (outer voice) Use /inner to see what the inner voice is thinking. Use /mind to turn the inner voice on/off. Everything else is conversation — just type and talk. """ # ── Main loop ────────────────────────────────────────────────────────────────── def main(): init_db() # Parse args custom_system = "" if "--system" in sys.argv: idx = sys.argv.index("--system") if idx + 1 < len(sys.argv): custom_system = sys.argv[idx + 1] chat = HarvesterChat(custom_system) # Connect to LLM print(f"\n{'═'*60}") print(f" HARVESTER CHAT — DUAL MIND") print(f" Inner voice (DeepSeek R1) • Outer voice (Qwen)") print(f" Persistent memory • Code generation • Full conversation") print(f" {get_identity_summary()}") print(f"{'═'*60}") print(f" Connecting to LLM...") if not chat._detect(): print(f"\n [ERROR] No LLM backend found.") print(f" Start LM Studio and load a model, or run Ollama.") sys.exit(1) print(f" Model: {chat.model_name}") print(f" Backend: {chat.backend}") # Show quick stats try: conn = get_db() tasks = conn.execute("SELECT COUNT(*) as n FROM task_history").fetchone()["n"] patterns = conn.execute("SELECT COUNT(*) as n FROM pattern_library").fetchone()["n"] failures = conn.execute("SELECT COUNT(*) as n FROM failure_library").fetchone()["n"] conn.close() print(f" Memory: {tasks} tasks, {patterns} patterns, {failures} failure lessons") except Exception: print(f" Memory: fresh database") print(f"\n Type /help for commands, or just start talking.") print(f" Dual mind is ON — /inner to see the inner voice, /mind to toggle") print(f"{'═'*60}\n") # Chat loop while True: try: user_input = input("You → ").strip() except (EOFError, KeyboardInterrupt): print("\n\n Harvester out. ✌️\n") break if not user_input: continue # ── Command dispatch ── lower = user_input.lower().strip() if lower in ("/bye", "/exit", "/quit", "/q"): print("\n Later. Everything we talked about is in memory.\n") break if lower == "/help": print(HELP_TEXT) continue if lower == "/clear": chat.history.clear() chat.message_count = 0 print(" History cleared.\n") continue if lower == "/stats": show_stats() continue if lower == "/patterns": show_patterns() continue if lower == "/memory": print(handle_memory(chat)) print() continue if lower == "/trust": print(handle_trust(chat)) print() continue if lower == "/inner": chat.show_inner = not chat.show_inner state = "ON — you can see the inner voice" if chat.show_inner else "OFF — inner voice is private" print(f" Inner monologue display: {state}\n") continue if lower == "/mind": chat.dual_mind = not chat.dual_mind state = "ON — DeepSeek R1 thinks before Qwen speaks" if chat.dual_mind else "OFF — single mind (Qwen only)" print(f" Dual mind: {state}\n") continue if lower == "/save": result = chat.save_conversation() print(f" {result}\n") continue if lower.startswith("/code"): task = user_input[5:].strip() result = handle_code(chat, task) print(result) print() continue if lower.startswith("/reason"): task = user_input[7:].strip() result = handle_reason(chat, task) print(result) print() continue # ── Regular conversation (dual-mind) ── if chat.dual_mind: print(f"\n pondering...", end="", flush=True) else: print(f"\n thinking...", end="", flush=True) start = time.time() response = chat.chat(user_input) elapsed = time.time() - start # Clear the status line print(f"\r{'':40}\r", end="") # Show inner monologue if toggled on if chat.show_inner and chat.last_inner: print(f" ┌─ inner voice ─────────────────────────────") for line in chat.last_inner.split("\n"): print(f" │ {line}") print(f" └─────────────────────────────────────────────") print(f"Harvester → {response}") print(f" [{elapsed:.1f}s]\n") if __name__ == "__main__": main()