mirror of
https://github.com/JamesTheGiblet/BuddAI.git
synced 2026-01-08 21:58:40 +00:00
- Introduced comprehensive documentation detailing features, capabilities, and architecture of BuddAI v4.0. - Highlighted the symbiotic relationship between user and AI, emphasizing personalized learning and memory retention. - Included validation results showcasing 90% accuracy across various coding tasks. - Documented the journey of development and validation from December 2025 to January 2026. - Outlined business value, commercialization potential, and future roadmap for enhancements.
1902 lines
79 KiB
Python
1902 lines
79 KiB
Python
#!/usr/bin/env python3
|
|
from urllib.parse import urlparse
|
|
import sys, os, json, logging, sqlite3, datetime, http.client, re, zipfile, shutil, queue, socket, argparse, io
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from typing import Optional, List, Dict, Tuple, Union, Generator
|
|
|
|
from anthropic import BaseModel
|
|
import psutil
|
|
|
|
from buddai_logic import CodeValidator, HardwareProfile, LearningMetrics
|
|
from buddai_memory import AdaptiveLearner, ShadowSuggestionEngine, SmartLearner
|
|
from buddai_shared import DATA_DIR, DB_PATH, MODELS, OLLAMA_HOST, OLLAMA_PORT, COMPLEX_TRIGGERS, MODULE_PATTERNS, SERVER_AVAILABLE
|
|
|
|
class OllamaConnectionPool:
|
|
def __init__(self, host: str, port: int, max_size: int = 10):
|
|
self.host = host
|
|
self.port = port
|
|
self.pool: queue.Queue = queue.Queue(maxsize=max_size)
|
|
|
|
def get_connection(self) -> http.client.HTTPConnection:
|
|
try:
|
|
return self.pool.get_nowait()
|
|
except queue.Empty:
|
|
return http.client.HTTPConnection(self.host, self.port, timeout=90)
|
|
|
|
def return_connection(self, conn: http.client.HTTPConnection):
|
|
try:
|
|
self.pool.put_nowait(conn)
|
|
except queue.Full:
|
|
conn.close()
|
|
|
|
OLLAMA_POOL = OllamaConnectionPool(OLLAMA_HOST, OLLAMA_PORT)
|
|
|
|
|
|
# --- Shadow Suggestion Engine ---
|
|
|
|
MAX_FILE_SIZE = 20 * 1024 * 1024 # 20 MB
|
|
ALLOWED_TYPES = [
|
|
"application/zip", "application/x-zip-compressed", "application/octet-stream",
|
|
"text/x-python", "text/plain", "text/x-c++src", "text/x-csrc", "text/javascript", "text/html", "text/css"
|
|
]
|
|
MAX_UPLOAD_FILES = 20
|
|
|
|
class BuddAI:
|
|
"""Executive with task breakdown"""
|
|
|
|
def is_search_query(self, message: str) -> bool:
|
|
"""Check if this is a search query that should query repo_index"""
|
|
message_lower = message.lower()
|
|
search_triggers = [
|
|
"show me", "find", "search for", "list all",
|
|
"what functions", "which repos", "do i have",
|
|
"where did i", "have i used", "examples of",
|
|
"show all", "display"
|
|
]
|
|
return any(trigger in message_lower for trigger in search_triggers)
|
|
|
|
def search_repositories(self, query: str) -> str:
|
|
"""Search repo_index for relevant functions and code"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT COUNT(*) FROM repo_index WHERE user_id = ?", (self.user_id,))
|
|
count = cursor.fetchone()[0]
|
|
print(f"\n🔍 Searching {count} indexed functions...\n")
|
|
|
|
# Extract keywords from query
|
|
keywords = re.findall(r'\b\w{4,}\b', query.lower())
|
|
# Add specific search terms
|
|
specific_terms = []
|
|
if "exponential" in query.lower() or "decay" in query.lower():
|
|
specific_terms.append("applyForge")
|
|
specific_terms.append("exp(")
|
|
if "forge" in query.lower():
|
|
specific_terms.append("Forge")
|
|
keywords.extend(specific_terms)
|
|
|
|
if not keywords:
|
|
print("❌ No search terms found")
|
|
conn.close()
|
|
return "No search terms provided."
|
|
|
|
# Build parameterized query
|
|
conditions = []
|
|
params = []
|
|
for keyword in keywords:
|
|
conditions.append("(function_name LIKE ? OR content LIKE ? OR repo_name LIKE ?)")
|
|
params.extend([f"%{keyword}%", f"%{keyword}%", f"%{keyword}%"])
|
|
|
|
sql = f"SELECT repo_name, file_path, function_name, content FROM repo_index WHERE ({' OR '.join(conditions)}) AND user_id = ? ORDER BY last_modified DESC LIMIT 10"
|
|
params.append(self.user_id)
|
|
|
|
cursor.execute(sql, params)
|
|
results = cursor.fetchall()
|
|
conn.close()
|
|
if not results:
|
|
return f"❌ No functions found matching: {', '.join(keywords)}\n\nTry: /index <path> to index more repositories"
|
|
# Format results
|
|
output = f"✅ Found {len(results)} matches for: {', '.join(set(keywords))}\n\n"
|
|
for i, (repo, file_path, func, content) in enumerate(results, 1):
|
|
# Extract relevant snippet
|
|
lines = content.split('\n')
|
|
snippet_lines = []
|
|
for line in lines[:30]: # First 30 lines
|
|
if any(kw in line.lower() for kw in keywords):
|
|
snippet_lines.append(line)
|
|
if len(snippet_lines) >= 10:
|
|
break
|
|
if not snippet_lines:
|
|
snippet_lines = lines[:10]
|
|
snippet = '\n'.join(snippet_lines)
|
|
output += f"**{i}. {func}()** in {repo}\n"
|
|
output += f" 📁 {Path(file_path).name}\n"
|
|
output += f"\n```cpp\n{snippet}\n```\n"
|
|
output += f" ---\n\n"
|
|
return output
|
|
|
|
def __init__(self, user_id: str = "default", server_mode: bool = False):
|
|
self.user_id = user_id
|
|
self.last_generated_id = None
|
|
self.last_prompt_debug = None
|
|
self.ensure_data_dir()
|
|
self.init_database()
|
|
self.session_id = self.create_session()
|
|
self.server_mode = server_mode
|
|
self.context_messages = []
|
|
self.shadow_engine = ShadowSuggestionEngine(DB_PATH, self.user_id)
|
|
self.learner = SmartLearner()
|
|
self.hardware_profile = HardwareProfile()
|
|
self.current_hardware = "ESP32-C3"
|
|
self.validator = CodeValidator()
|
|
self.adaptive_learner = AdaptiveLearner()
|
|
self.metrics = LearningMetrics()
|
|
self.fine_tuner = ModelFineTuner()
|
|
|
|
print("BuddAI Executive v4.0 - Decoupled & Personality Sync")
|
|
print("=" * 50)
|
|
print(f"Session: {self.session_id}")
|
|
print(f"FAST (5-10s) | BALANCED (15-30s)")
|
|
print(f"Smart task breakdown for complex requests")
|
|
print("=" * 50)
|
|
print("\nCommands: /fast, /balanced, /help, exit\n")
|
|
|
|
def ensure_data_dir(self) -> None:
|
|
DATA_DIR.mkdir(exist_ok=True)
|
|
|
|
def init_database(self) -> None:
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
session_id TEXT PRIMARY KEY,
|
|
user_id TEXT,
|
|
started_at TIMESTAMP,
|
|
ended_at TIMESTAMP,
|
|
title TEXT
|
|
)
|
|
""")
|
|
|
|
try:
|
|
cursor.execute("ALTER TABLE sessions ADD COLUMN title TEXT")
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
|
|
try:
|
|
cursor.execute("ALTER TABLE sessions ADD COLUMN user_id TEXT")
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id TEXT,
|
|
role TEXT,
|
|
content TEXT,
|
|
timestamp TIMESTAMP
|
|
)
|
|
""")
|
|
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS repo_index (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT,
|
|
file_path TEXT,
|
|
repo_name TEXT,
|
|
function_name TEXT,
|
|
content TEXT,
|
|
last_modified TIMESTAMP
|
|
)
|
|
""")
|
|
|
|
try:
|
|
cursor.execute("ALTER TABLE repo_index ADD COLUMN user_id TEXT")
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS style_preferences (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT,
|
|
category TEXT,
|
|
preference TEXT,
|
|
confidence FLOAT,
|
|
extracted_at TIMESTAMP
|
|
)
|
|
""")
|
|
|
|
try:
|
|
cursor.execute("ALTER TABLE style_preferences ADD COLUMN user_id TEXT")
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS feedback (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
message_id INTEGER,
|
|
positive BOOLEAN,
|
|
timestamp TIMESTAMP
|
|
)
|
|
""")
|
|
|
|
try:
|
|
cursor.execute("ALTER TABLE feedback ADD COLUMN comment TEXT")
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS corrections (
|
|
id INTEGER PRIMARY KEY,
|
|
timestamp TEXT,
|
|
original_code TEXT,
|
|
corrected_code TEXT,
|
|
reason TEXT,
|
|
context TEXT
|
|
)
|
|
""")
|
|
|
|
try:
|
|
cursor.execute("ALTER TABLE corrections ADD COLUMN processed BOOLEAN DEFAULT 0")
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS compilation_log (
|
|
id INTEGER PRIMARY KEY,
|
|
timestamp TEXT,
|
|
code TEXT,
|
|
success BOOLEAN,
|
|
errors TEXT,
|
|
hardware TEXT
|
|
)
|
|
""")
|
|
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS code_rules (
|
|
id INTEGER PRIMARY KEY,
|
|
rule_text TEXT,
|
|
pattern_find TEXT,
|
|
pattern_replace TEXT,
|
|
context TEXT,
|
|
confidence FLOAT,
|
|
learned_from TEXT,
|
|
times_applied INTEGER DEFAULT 0
|
|
)
|
|
""")
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def create_session(self) -> str:
|
|
now = datetime.now()
|
|
base_id = now.strftime("%Y%m%d_%H%M%S")
|
|
session_id = base_id
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
counter = 0
|
|
while True:
|
|
try:
|
|
cursor.execute(
|
|
"INSERT INTO sessions (session_id, user_id, started_at) VALUES (?, ?, ?)",
|
|
(session_id, self.user_id, now.isoformat())
|
|
)
|
|
conn.commit()
|
|
break
|
|
except sqlite3.IntegrityError:
|
|
counter += 1
|
|
session_id = f"{base_id}_{counter}"
|
|
|
|
conn.close()
|
|
return session_id
|
|
|
|
def end_session(self) -> None:
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"UPDATE sessions SET ended_at = ? WHERE session_id = ?",
|
|
(datetime.now().isoformat(), self.session_id)
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def save_message(self, role: str, content: str) -> int:
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)",
|
|
(self.session_id, role, content, datetime.now().isoformat())
|
|
)
|
|
msg_id = cursor.lastrowid
|
|
conn.commit()
|
|
conn.close()
|
|
return msg_id
|
|
|
|
def index_local_repositories(self, root_path: str) -> None:
|
|
"""Crawl directories and index .py, .ino, and .cpp files"""
|
|
import ast
|
|
|
|
print(f"\n🔍 Indexing repositories in: {root_path}")
|
|
path = Path(root_path)
|
|
|
|
if not path.exists():
|
|
print(f"❌ Path not found: {root_path}")
|
|
return
|
|
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
count = 0
|
|
|
|
for file_path in path.rglob('*'):
|
|
if file_path.is_file() and file_path.suffix in ['.py', '.ino', '.cpp', '.h', '.js', '.jsx', '.html', '.css']:
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
content = f.read()
|
|
|
|
functions = []
|
|
|
|
# Python parsing
|
|
if file_path.suffix == '.py':
|
|
try:
|
|
tree = ast.parse(content)
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.FunctionDef):
|
|
functions.append(node.name)
|
|
except:
|
|
pass
|
|
|
|
# C++/Arduino parsing
|
|
elif file_path.suffix in ['.ino', '.cpp', '.h']:
|
|
matches = re.findall(r'\b(?:void|int|bool|float|double|String|char)\s+(\w+)\s*\(', content)
|
|
functions.extend(matches)
|
|
|
|
# JS/Web parsing
|
|
elif file_path.suffix in ['.js', '.jsx']:
|
|
matches = re.findall(r'(?:function\s+(\w+)|const\s+(\w+)\s*=\s*(?:async\s*)?\(?.*?\)?\s*=>)', content)
|
|
functions.extend([m[0] or m[1] for m in matches if m[0] or m[1]])
|
|
|
|
# HTML/CSS - Index as whole file
|
|
elif file_path.suffix in ['.html', '.css']:
|
|
functions.append("file_content")
|
|
|
|
# Determine repo name
|
|
try:
|
|
repo_name = file_path.relative_to(path).parts[0]
|
|
except:
|
|
repo_name = "unknown"
|
|
|
|
timestamp = datetime.fromtimestamp(file_path.stat().st_mtime)
|
|
|
|
for func in functions:
|
|
cursor.execute("""
|
|
INSERT INTO repo_index (user_id, file_path, repo_name, function_name, content, last_modified)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
""", (self.user_id, str(file_path), repo_name, func, content, timestamp.isoformat()))
|
|
count += 1
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
print(f"✅ Indexed {count} functions across repositories")
|
|
|
|
def retrieve_style_context(self, message: str) -> str:
|
|
"""Search repo_index for code snippets matching the request"""
|
|
# Extract potential keywords (nouns/modules)
|
|
keywords = re.findall(r'\b\w{4,}\b', message.lower())
|
|
if not keywords:
|
|
return ""
|
|
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
# Build a search query for function names or repo names
|
|
search_terms = " OR ".join([f"function_name LIKE '%{k}%'" for k in keywords])
|
|
search_terms += " OR " + " OR ".join([f"repo_name LIKE '%{k}%'" for k in keywords])
|
|
|
|
query = f"SELECT repo_name, function_name, content FROM repo_index WHERE ({search_terms}) AND user_id = ? LIMIT 2"
|
|
|
|
cursor.execute(query, (self.user_id,))
|
|
results = cursor.fetchall()
|
|
conn.close()
|
|
|
|
if not results:
|
|
return ""
|
|
|
|
context_block = "\n[REFERENCE STYLE FROM JAMES'S PAST PROJECTS]\n"
|
|
for repo, func, content in results:
|
|
# Just grab the first 500 chars of the file to save context window
|
|
snippet = content[:500] + "..."
|
|
context_block += f"Repo: {repo} | Function: {func}\nCode:\n{snippet}\n---\n"
|
|
|
|
return context_block
|
|
|
|
def scan_style_signature(self) -> None:
|
|
"""V3.0: Analyze repo_index to extract style preferences."""
|
|
print("\n🕵️ Scanning repositories for style signature...")
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
# Get a sample of code
|
|
cursor.execute("SELECT content FROM repo_index WHERE user_id = ? ORDER BY RANDOM() LIMIT 5", (self.user_id,))
|
|
rows = cursor.fetchall()
|
|
|
|
if not rows:
|
|
print("❌ No code indexed. Run /index first.")
|
|
conn.close()
|
|
return
|
|
|
|
code_sample = "\n---\n".join([r[0][:1000] for r in rows])
|
|
|
|
prompt = f"""Analyze this code sample from James's repositories.
|
|
Extract 3 distinct coding preferences or patterns.
|
|
Format: Category: Preference
|
|
|
|
Examples:
|
|
- Serial: Uses 115200 baud
|
|
- Safety: Uses non-blocking millis()
|
|
- Pins: Prefers #define over const int
|
|
|
|
Code Sample:
|
|
{code_sample}
|
|
"""
|
|
|
|
print("⚡ Analyzing with BALANCED model...")
|
|
summary = self.call_model("balanced", prompt, system_task=True)
|
|
|
|
# Store in DB
|
|
timestamp = datetime.now().isoformat()
|
|
lines = summary.split('\n')
|
|
for line in lines:
|
|
if ':' in line:
|
|
parts = line.split(':', 1)
|
|
category = parts[0].strip('- *')
|
|
pref = parts[1].strip()
|
|
cursor.execute(
|
|
"INSERT INTO style_preferences (user_id, category, preference, confidence, extracted_at) VALUES (?, ?, ?, ?, ?, ?)",
|
|
(self.user_id, category, pref, 0.8, timestamp)
|
|
)
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
print(f"\n✅ Style Signature Updated:\n{summary}\n")
|
|
|
|
def get_recent_context(self, limit: int = 5) -> str:
|
|
"""Get recent chat context as a string"""
|
|
return json.dumps(self.context_messages[-limit:])
|
|
|
|
def save_correction(self, original_code: str, corrected_code: str, reason: str):
|
|
"""Store when James fixes BuddAI's code"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS corrections (
|
|
id INTEGER PRIMARY KEY,
|
|
timestamp TEXT,
|
|
original_code TEXT,
|
|
corrected_code TEXT,
|
|
reason TEXT,
|
|
context TEXT
|
|
)
|
|
""")
|
|
|
|
cursor.execute("""
|
|
INSERT INTO corrections
|
|
(timestamp, original_code, corrected_code, reason, context)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
""", (
|
|
datetime.now().isoformat(),
|
|
original_code,
|
|
corrected_code,
|
|
reason,
|
|
self.get_recent_context()
|
|
))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def detect_hardware(self, message: str) -> str:
|
|
"""Wrapper to detect hardware from message or return current default"""
|
|
hw = self.hardware_profile.detect_hardware(message)
|
|
return hw if hw else self.current_hardware
|
|
|
|
def get_applicable_rules(self, user_message: str) -> List[Dict]:
|
|
"""Get rules relevant to the user message"""
|
|
# user_message is currently unused
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
# Fetch rules with reasonable confidence
|
|
cursor.execute("SELECT rule_text, confidence FROM code_rules WHERE confidence > 0.6 ORDER BY confidence DESC")
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
return [{"rule_text": r[0], "confidence": r[1]} for r in rows]
|
|
|
|
def get_style_summary(self) -> str:
|
|
"""Get summary of learned style preferences"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT category, preference FROM style_preferences WHERE confidence > 0.6")
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
if not rows:
|
|
return "Standard coding style."
|
|
return ", ".join([f"{r[0]}: {r[1]}" for r in rows])
|
|
|
|
def classify_hardware(self, user_message: str) -> dict:
|
|
"""Detect what hardware this question is about"""
|
|
|
|
hardware = {
|
|
"servo": False,
|
|
"dc_motor": False,
|
|
"button": False,
|
|
"led": False,
|
|
"sensor": False,
|
|
"weapon": False
|
|
}
|
|
|
|
msg_lower = user_message.lower()
|
|
|
|
# Helper to check keywords
|
|
def has_keywords(text, keywords):
|
|
return any(word in text for word in keywords)
|
|
|
|
# Keyword definitions
|
|
servo_kws = ['servo', 'mg996', 'sg90']
|
|
motor_kws = ['l298n', 'dc motor', 'motor driver', 'motor control']
|
|
button_kws = ['button', 'switch', 'trigger']
|
|
led_kws = ['led', 'light', 'brightness']
|
|
led_kws = ['led', 'light', 'brightness', 'indicator']
|
|
# Removed 'state machine' from weapon_kws to allow abstract logic
|
|
weapon_kws = ['weapon', 'combat', 'arming', 'fire', 'spinner', 'flipper']
|
|
logic_kws = ['state machine', 'logic', 'structure', 'flow', 'armed', 'disarmed']
|
|
|
|
# 1. Check current message first
|
|
detected_in_current = False
|
|
|
|
if has_keywords(msg_lower, servo_kws):
|
|
hardware["servo"] = True
|
|
detected_in_current = True
|
|
if has_keywords(msg_lower, motor_kws):
|
|
hardware["dc_motor"] = True
|
|
detected_in_current = True
|
|
if has_keywords(msg_lower, button_kws):
|
|
hardware["button"] = True
|
|
detected_in_current = True
|
|
if has_keywords(msg_lower, led_kws):
|
|
hardware["led"] = True
|
|
detected_in_current = True
|
|
if has_keywords(msg_lower, weapon_kws):
|
|
hardware["weapon"] = True
|
|
detected_in_current = True
|
|
if has_keywords(msg_lower, logic_kws):
|
|
# Logic detected: Clear context (don't set any hardware)
|
|
detected_in_current = True
|
|
|
|
# 2. Context Switching: Only look back if NO hardware/logic detected in current message
|
|
# and message is short (likely a follow-up command like "make it spin")
|
|
if not detected_in_current and len(user_message.split()) < 10 and self.context_messages:
|
|
recent = " ".join([m['content'].lower() for m in self.context_messages[-2:] if m['role'] == 'user'])
|
|
|
|
if has_keywords(recent, servo_kws): hardware["servo"] = True
|
|
if has_keywords(recent, motor_kws): hardware["dc_motor"] = True
|
|
if has_keywords(recent, button_kws): hardware["button"] = True
|
|
if has_keywords(recent, led_kws): hardware["led"] = True
|
|
if has_keywords(recent, weapon_kws): hardware["weapon"] = True
|
|
|
|
return hardware
|
|
|
|
def get_all_rules(self) -> List[str]:
|
|
"""Get all learned rules as text"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT rule_text FROM code_rules ORDER BY confidence DESC LIMIT 50")
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
return [r[0] for r in rows]
|
|
|
|
def filter_rules_by_hardware(self, all_rules, hardware):
|
|
"""Only return rules relevant to detected hardware"""
|
|
|
|
relevant_rules = []
|
|
|
|
# Define rule categories
|
|
servo_kws = ['servo', 'attach', 'setperiodhertz']
|
|
motor_kws = ['l298n', 'in1', 'in2', 'motor driver']
|
|
weapon_kws = ['arming', 'disarm', 'fire', 'combat'] # Removed 'state machine' to prevent over-filtering
|
|
button_kws = ['button', 'switch', 'debounce', 'digitalread', 'input_pullup']
|
|
|
|
has_specific_context = hardware["servo"] or hardware["dc_motor"] or hardware["weapon"]
|
|
has_specific_context = hardware["servo"] or hardware["dc_motor"] or hardware["weapon"] or hardware["button"]
|
|
|
|
for rule in all_rules:
|
|
rule_lower = rule.lower()
|
|
|
|
is_servo_rule = any(w in rule_lower for w in servo_kws)
|
|
is_motor_rule = any(w in rule_lower for w in motor_kws)
|
|
is_weapon_rule = any(w in rule_lower for w in weapon_kws)
|
|
is_button_rule = any(w in rule_lower for w in button_kws)
|
|
|
|
# Pattern Over-application: Strict filtering
|
|
if has_specific_context:
|
|
if hardware["dc_motor"] and not hardware["servo"] and is_servo_rule: continue
|
|
if hardware["servo"] and not hardware["dc_motor"] and is_motor_rule: continue
|
|
if not hardware["weapon"] and is_weapon_rule: continue
|
|
if not hardware["button"] and is_button_rule: continue
|
|
|
|
# If question is about weapons (logic), EXCLUDE servo rules unless servo explicitly requested
|
|
if hardware["weapon"] and not hardware["servo"] and is_servo_rule: continue
|
|
|
|
else:
|
|
# Generic context: Exclude all specific hardware rules
|
|
if is_servo_rule or is_motor_rule or is_weapon_rule: continue
|
|
if is_servo_rule or is_motor_rule or is_weapon_rule or is_button_rule: continue
|
|
|
|
relevant_rules.append(rule)
|
|
|
|
return relevant_rules
|
|
|
|
def build_enhanced_prompt(self, user_message: str, hardware_detected: str = None) -> str:
|
|
"""Build prompt with FILTERED rules"""
|
|
|
|
# Classify hardware
|
|
hardware = self.classify_hardware(user_message)
|
|
|
|
# Get ALL rules
|
|
all_rules = self.get_all_rules()
|
|
|
|
# Filter by relevance
|
|
relevant_rules = self.filter_rules_by_hardware(all_rules, hardware)
|
|
|
|
# Build focused prompt
|
|
hardware_context = []
|
|
if hardware["servo"]:
|
|
hardware_context.append("SERVO CONTROL")
|
|
if hardware["dc_motor"]:
|
|
hardware_context.append("DC MOTOR CONTROL")
|
|
if hardware["button"]: hardware_context.append("BUTTON INPUTS")
|
|
if hardware["led"]: hardware_context.append("LED STATUS")
|
|
if hardware["weapon"]: hardware_context.append("WEAPON SYSTEM")
|
|
|
|
l298n_rules = ""
|
|
if hardware["dc_motor"]:
|
|
l298n_rules = """
|
|
- L298N WIRING RULES (MANDATORY):
|
|
1. IN1/IN2 = Digital Output (Direction). Use digitalWrite().
|
|
2. ENA = PWM Output (Speed). Use ledcWrite().
|
|
3. To Move: IN1/IN2 must be OPPOSITE (HIGH/LOW).
|
|
4. To Stop: IN1/IN2 both LOW.
|
|
5. DO NOT treat Motors like Servos (No 'position' or 'angle').
|
|
- SAFETY RULES (MANDATORY):
|
|
1. Implement a safety timeout (e.g., 5000ms).
|
|
2. Stop motors if no signal is received within timeout.
|
|
3. Use millis() for non-blocking timing.
|
|
"""
|
|
|
|
weapon_rules = ""
|
|
if hardware.get("weapon"):
|
|
weapon_rules = """
|
|
- COMBAT PROTOCOL (MANDATORY):
|
|
1. LOGIC FOCUS: This is a State Machine request, NOT just servo movement.
|
|
2. STATES: enum State { DISARMED, ARMING, ARMED, FIRING };
|
|
3. TRANSITIONS: DISARMED -> ARMING (2s delay) -> ARMED -> FIRING.
|
|
4. SAFETY: Auto-disarm after 10s idle. Fire only when ARMED.
|
|
5. STRUCTURE: Use switch(currentState) { case ... } for logic.
|
|
6. OUTPUTS: Control relays/LEDs/Motors based on state.
|
|
"""
|
|
|
|
# Anti-bloat rules
|
|
anti_bloat_rules = []
|
|
if not hardware["button"]:
|
|
anti_bloat_rules.append("- NO EXTRA INPUTS: Do NOT add buttons, switches, or digitalRead() unless explicitly requested.")
|
|
anti_bloat_rules.append("NO BUTTONS: Do NOT add digitalRead() or input pins.")
|
|
if not hardware["servo"]:
|
|
anti_bloat_rules.append("- NO EXTRA SERVOS: Do NOT add Servo objects or attach() unless explicitly requested.")
|
|
anti_bloat_rules.append("NO SERVOS: Do NOT add Servo objects or attach().")
|
|
if not hardware["dc_motor"]:
|
|
anti_bloat_rules.append("- NO EXTRA MOTORS: Do NOT add motor driver code (L298N) unless explicitly requested.")
|
|
anti_bloat_rules.append("NO MOTORS: Do NOT add motor driver code (L298N).")
|
|
|
|
anti_bloat = "\n".join(anti_bloat_rules)
|
|
anti_bloat = "\n".join([f"- {r}" for r in anti_bloat_rules])
|
|
|
|
# Modularity rule
|
|
modularity_rule = ""
|
|
if "function" in user_message.lower() or "naming" in user_message.lower() or "modular" in user_message.lower():
|
|
modularity_rule = """
|
|
- CODE STRUCTURE (MANDATORY):
|
|
1. NO MONOLITHIC LOOP: Break code into small, descriptive functions.
|
|
2. NAMING: Use camelCase for functions (e.g., readBatteryVoltage(), updateDisplay()).
|
|
3. loop() must ONLY call these functions, not contain raw logic.
|
|
"""
|
|
|
|
# Status LED rule
|
|
status_led_rule = ""
|
|
if hardware["led"] and ("status" in user_message.lower() or "indicator" in user_message.lower()):
|
|
status_led_rule = """
|
|
- STATUS LED RULES (MANDATORY):
|
|
1. NO BREATHING/FADING: Do not use simple PWM fading loops.
|
|
2. USE STATES: Define enum LEDStatus { OFF, IDLE, ACTIVE, ERROR };
|
|
3. IMPLEMENTATION: Create void setStatusLED(LEDStatus state).
|
|
4. PATTERNS: IDLE=Slow Blink, ACTIVE=Solid On, ERROR=Fast Blink.
|
|
"""
|
|
|
|
prompt = f"""You are generating code for: {', '.join(hardware_context)}
|
|
You are an expert embedded developer.
|
|
TARGET HARDWARE: {hardware_detected}
|
|
ACTIVE MODULES: {', '.join(hardware_context) if hardware_context else "None (Logic Only)"}
|
|
|
|
CRITICAL: Only use code patterns relevant to the hardware mentioned.
|
|
STRICT NEGATIVE CONSTRAINTS (DO NOT IGNORE):
|
|
{anti_bloat}
|
|
|
|
MANDATORY HARDWARE RULES:
|
|
{l298n_rules}
|
|
{weapon_rules}
|
|
{status_led_rule}
|
|
{anti_bloat}
|
|
{modularity_rule}
|
|
|
|
GENERAL GUIDELINES:
|
|
- If DC MOTOR: Use L298N patterns (digitalWrite, ledcWrite)
|
|
- If SERVO: Use ESP32Servo patterns (attach, write)
|
|
- DO NOT mix servo code into motor questions
|
|
- DO NOT mix motor code into servo questions
|
|
|
|
CRITICAL RULES (MUST FOLLOW):
|
|
{chr(10).join(relevant_rules)}
|
|
|
|
USER REQUEST:
|
|
{user_message}
|
|
|
|
Generate code following ALL rules above. Do not add unrequested features.
|
|
FINAL CHECK:
|
|
1. Did you add unrequested buttons? REMOVE THEM.
|
|
2. Did you add unrequested servos? REMOVE THEM.
|
|
3. Generate code ONLY for the hardware requested.
|
|
"""
|
|
|
|
return prompt
|
|
|
|
def teach_rule(self, rule_text: str):
|
|
"""Explicitly save a user-taught rule"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
INSERT INTO code_rules
|
|
(rule_text, pattern_find, pattern_replace, confidence, learned_from)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
""", (rule_text, "", "", 1.0, 'user_taught'))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def log_compilation_result(self, code: str, success: bool, errors: str = ""):
|
|
"""Track what compiles vs what fails"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS compilation_log (
|
|
id INTEGER PRIMARY KEY,
|
|
timestamp TEXT,
|
|
code TEXT,
|
|
success BOOLEAN,
|
|
errors TEXT,
|
|
hardware TEXT
|
|
)
|
|
""")
|
|
|
|
cursor.execute("""
|
|
INSERT INTO compilation_log
|
|
(timestamp, code, success, errors, hardware)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
""", (
|
|
datetime.now().isoformat(),
|
|
code,
|
|
success,
|
|
errors,
|
|
"ESP32-C3" # Your target hardware
|
|
))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def is_simple_question(self, message: str) -> bool:
|
|
"""Check if this is a simple question that should use FAST model"""
|
|
message_lower = message.lower()
|
|
|
|
simple_triggers = [
|
|
"what is", "what's", "who is", "who's", "when is",
|
|
"how do i", "can you explain", "tell me about",
|
|
"what are", "where is", "hi", "hello", "hey",
|
|
"good morning", "good evening"
|
|
]
|
|
|
|
# Also check if it's just a question without code keywords
|
|
code_keywords = ["generate", "create", "write", "build", "code", "function"]
|
|
|
|
has_simple_trigger = any(trigger in message_lower for trigger in simple_triggers)
|
|
has_code_keyword = any(keyword in message_lower for keyword in code_keywords)
|
|
|
|
# Simple if: has simple trigger AND no code keywords
|
|
return has_simple_trigger and not has_code_keyword
|
|
|
|
def is_complex(self, message: str) -> bool:
|
|
"""Check if request is too complex and should be broken down"""
|
|
message_lower = message.lower()
|
|
|
|
# Count complexity triggers
|
|
trigger_count = sum(1 for trigger in COMPLEX_TRIGGERS if trigger in message_lower)
|
|
|
|
# Count how many modules mentioned
|
|
module_count = 0
|
|
for module, keywords in MODULE_PATTERNS.items():
|
|
# module is used for key, keywords for values
|
|
if any(kw in message_lower for kw in keywords):
|
|
module_count += 1
|
|
|
|
# Complex if: multiple triggers OR 3+ modules mentioned
|
|
return trigger_count >= 2 or module_count >= 3
|
|
|
|
def extract_modules(self, message: str) -> List[str]:
|
|
"""Extract which modules are needed"""
|
|
message_lower = message.lower()
|
|
needed_modules = []
|
|
|
|
for module, keywords in MODULE_PATTERNS.items():
|
|
# module is used for key, keywords for values
|
|
if any(kw in message_lower for kw in keywords):
|
|
needed_modules.append(module)
|
|
|
|
return needed_modules
|
|
|
|
def build_modular_plan(self, modules: List[str]) -> List[Dict[str, str]]:
|
|
"""Create a build plan from modules"""
|
|
plan = []
|
|
|
|
module_tasks = {
|
|
"ble": "BLE communication setup with phone app control",
|
|
"servo": "Servo motor control for flipper/weapon",
|
|
"motor": "Motor driver setup for movement (L298N)",
|
|
"safety": "Safety timeout and failsafe systems",
|
|
"battery": "Battery voltage monitoring",
|
|
"sensor": "Sensor integration (distance/proximity)"
|
|
}
|
|
|
|
for module in modules:
|
|
if module in module_tasks:
|
|
plan.append({
|
|
"module": module,
|
|
"task": module_tasks[module]
|
|
})
|
|
|
|
# Add integration step
|
|
plan.append({
|
|
"module": "integration",
|
|
"task": "Integrate all modules into complete system"
|
|
})
|
|
|
|
return plan
|
|
|
|
def get_user_status(self) -> str:
|
|
"""Determine James's context based on defined schedule"""
|
|
now = datetime.now()
|
|
day = now.weekday() # 0=Mon, 6=Sun
|
|
t = now.hour + (now.minute / 60.0)
|
|
|
|
if day <= 4: # Mon-Fri
|
|
if 5.5 <= t < 6.5:
|
|
return "Early Morning Build Session 🌅 (5:30-6:30 AM)"
|
|
elif 6.5 <= t < 17.0:
|
|
return "Work Hours (Facilities Caretaker) 🏢"
|
|
elif 17.0 <= t < 21.0:
|
|
return "Evening Build Session 🌙 (5:00-9:00 PM)"
|
|
else:
|
|
return "Rest Time 💤"
|
|
elif day == 5: # Saturday
|
|
return "Weekend Freedom 🎨 (Creative Mode)"
|
|
else: # Sunday
|
|
if t < 21.0:
|
|
return "Weekend Freedom 🎨 (Until 9 PM)"
|
|
else:
|
|
return "Rest Time 💤"
|
|
|
|
def get_learned_rules(self) -> List[Dict]:
|
|
"""Retrieve high-confidence rules"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT rule_text, pattern_find, pattern_replace, confidence FROM code_rules WHERE confidence >= 0.8")
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
return [{"rule": r[0], "find": r[1], "replace": r[2], "confidence": r[3]} for r in rows]
|
|
|
|
def call_model(self, model_name: str, message: str, stream: bool = False, system_task: bool = False) -> Union[str, Generator[str, None, None]]:
|
|
"""Call specified model"""
|
|
try:
|
|
messages = []
|
|
|
|
if system_task:
|
|
# Direct prompt, no history, no enhancement
|
|
messages.append({"role": "user", "content": message})
|
|
else:
|
|
# Use enhanced prompt builder
|
|
enhanced_prompt = self.build_enhanced_prompt(message, self.current_hardware)
|
|
|
|
# Add conversation history (excluding old system messages)
|
|
history = [m for m in self.context_messages[-5:] if m.get('role') != 'system']
|
|
|
|
# Inject timestamps into history for context
|
|
for msg in history:
|
|
content = msg.get('content', '')
|
|
ts = msg.get('timestamp')
|
|
if ts:
|
|
try:
|
|
dt = datetime.fromisoformat(ts)
|
|
content = f"[{dt.strftime('%H:%M')}] {content}"
|
|
except ValueError:
|
|
pass
|
|
messages.append({"role": msg['role'], "content": content})
|
|
|
|
# Use enhanced prompt instead of raw user message
|
|
if history and history[-1].get('content') == message:
|
|
messages[-1]['content'] = enhanced_prompt
|
|
else:
|
|
messages.append({"role": "user", "content": enhanced_prompt})
|
|
|
|
self.last_prompt_debug = json.dumps(messages, indent=2)
|
|
|
|
body = {
|
|
"model": MODELS[model_name],
|
|
"messages": messages,
|
|
"stream": stream,
|
|
"options": {
|
|
"temperature": 0.0, # Deterministic output
|
|
"top_p": 1.0,
|
|
"top_k": 1,
|
|
"num_ctx": 1024
|
|
}
|
|
}
|
|
|
|
headers = {"Content-Type": "application/json"}
|
|
json_body = json.dumps(body)
|
|
|
|
# Retry logic for connection stability
|
|
# Attempts: 0=Normal, 1=Retry/CPU Fallback, 2=Final Retry
|
|
for attempt in range(3):
|
|
conn = None
|
|
try:
|
|
# Re-serialize body in case options changed (CPU fallback)
|
|
json_body = json.dumps(body)
|
|
|
|
conn = OLLAMA_POOL.get_connection()
|
|
conn.request("POST", "/api/chat", json_body, headers)
|
|
response = conn.getresponse()
|
|
|
|
if stream:
|
|
if response.status != 200:
|
|
error_text = response.read().decode('utf-8')
|
|
conn.close()
|
|
|
|
# GPU OOM Detection -> CPU Fallback
|
|
if "CUDA" in error_text or "buffer" in error_text:
|
|
if "num_gpu" not in body["options"]:
|
|
print("⚠️ GPU OOM detected. Switching to CPU mode...")
|
|
body["options"]["num_gpu"] = 0 # Force CPU
|
|
continue # Retry immediately
|
|
|
|
try:
|
|
err_msg = f"Error {response.status}: {json.loads(error_text).get('error', error_text)}"
|
|
except:
|
|
err_msg = f"Error {response.status}: {error_text}"
|
|
|
|
if "num_gpu" in body["options"]:
|
|
err_msg += "\n\n(⚠️ CPU Mode also failed. System RAM might be full.)"
|
|
elif "CUDA" in err_msg or "buffer" in err_msg:
|
|
err_msg += "\n\n(⚠️ GPU Out of Memory. Retrying on CPU failed.)"
|
|
|
|
return (x for x in [err_msg])
|
|
|
|
return self._stream_response(response, conn)
|
|
|
|
if response.status == 200:
|
|
data = json.loads(response.read().decode('utf-8'))
|
|
OLLAMA_POOL.return_connection(conn)
|
|
return data.get("message", {}).get("content", "No response")
|
|
else:
|
|
error_text = response.read().decode('utf-8')
|
|
conn.close()
|
|
|
|
# GPU OOM Detection -> CPU Fallback (Non-stream)
|
|
if "CUDA" in error_text or "buffer" in error_text:
|
|
if "num_gpu" not in body["options"]:
|
|
print("⚠️ GPU OOM detected. Switching to CPU mode...")
|
|
body["options"]["num_gpu"] = 0 # Force CPU
|
|
continue # Retry immediately
|
|
|
|
try:
|
|
err_msg = f"Error {response.status}: {json.loads(error_text).get('error', error_text)}"
|
|
except:
|
|
err_msg = f"Error {response.status}: {error_text}"
|
|
|
|
if "num_gpu" in body["options"]:
|
|
err_msg += "\n\n(⚠️ CPU Mode also failed.)"
|
|
elif "CUDA" in err_msg or "buffer" in err_msg:
|
|
err_msg += "\n\n(⚠️ GPU Out of Memory.)"
|
|
return err_msg
|
|
|
|
except (http.client.NotConnected, BrokenPipeError, ConnectionResetError, socket.timeout) as e:
|
|
if conn: conn.close()
|
|
if attempt == 2: # Last attempt
|
|
return f"Error: Connection failed. {str(e)}"
|
|
continue # Retry
|
|
except Exception as e:
|
|
if conn: conn.close()
|
|
return f"Error: {str(e)}"
|
|
|
|
except Exception as e:
|
|
return f"Error: {str(e)}"
|
|
|
|
def _stream_response(self, response, conn) -> Generator[str, None, None]:
|
|
"""Yield chunks from HTTP response"""
|
|
fully_consumed = False
|
|
has_content = False
|
|
try:
|
|
while True:
|
|
line = response.readline()
|
|
if not line: break
|
|
try:
|
|
data = json.loads(line.decode('utf-8'))
|
|
if "message" in data:
|
|
content = data["message"].get("content", "")
|
|
if content:
|
|
has_content = True
|
|
yield content
|
|
if data.get("done"):
|
|
fully_consumed = True
|
|
break
|
|
except: pass
|
|
except Exception as e:
|
|
yield f"\n[Stream Error: {str(e)}]"
|
|
finally:
|
|
if fully_consumed:
|
|
OLLAMA_POOL.return_connection(conn)
|
|
else:
|
|
conn.close()
|
|
|
|
if not has_content and not fully_consumed:
|
|
yield "\n[Error: Empty response from Ollama. Check if model is loaded.]"
|
|
|
|
def execute_modular_build(self, _: str, modules: List[str], plan: List[Dict[str, str]], forge_mode: str = "2") -> str:
|
|
"""Execute build plan step by step"""
|
|
print(f"\n🔨 MODULAR BUILD MODE")
|
|
print(f"Detected {len(modules)} modules: {', '.join(modules)}")
|
|
print(f"Breaking into {len(plan)} steps...\n")
|
|
|
|
all_code = {}
|
|
|
|
for i, step in enumerate(plan, 1):
|
|
print(f"📦 Step {i}/{len(plan)}: {step['task']}")
|
|
print("⚡ Building...\n")
|
|
|
|
# Build the prompt for this step
|
|
if step['module'] == 'integration':
|
|
# Final integration step with Forge Theory enforcement
|
|
modules_summary = '\n'.join([f"- {m}: {all_code[m][:150]}..." for m in modules if m in all_code])
|
|
|
|
# Ask James for the 'vibe' of the robot
|
|
print("\n⚡ FORGE THEORY TUNING:")
|
|
print("1. Aggressive (k=0.3) - High snap, combat ready")
|
|
print("2. Balanced (k=0.1) - Standard movement")
|
|
print("3. Graceful (k=0.03) - Roasting / Smooth curves")
|
|
|
|
if self.server_mode:
|
|
choice = forge_mode
|
|
else:
|
|
choice = input("Select Forge Constant [1-3, default 2]: ")
|
|
|
|
k_val = "0.1"
|
|
if choice == "1": k_val = "0.3"
|
|
elif choice == "3": k_val = "0.03"
|
|
|
|
prompt = f"""INTEGRATION TASK: Combine modules into a cohesive GilBot system.
|
|
|
|
[MODULES]
|
|
{modules_summary}
|
|
|
|
[FORGE PARAMETERS]
|
|
Set k = {k_val} for all applyForge() calls.
|
|
|
|
[REQUIREMENTS]
|
|
1. Implement applyForge() math helper.
|
|
2. Use k={k_val} to smooth motor and servo transitions.
|
|
3. Ensure naming matches James's style: activateFlipper(), setMotors().
|
|
"""
|
|
else:
|
|
# Individual module
|
|
prompt = f"Generate ESP32-C3 code for: {step['task']}. Keep it modular with clear comments."
|
|
|
|
# Call balanced model for each module
|
|
response = self.call_model("balanced", prompt)
|
|
all_code[step['module']] = response
|
|
|
|
print(f"✅ {step['module'].upper()} module complete\n")
|
|
print("-" * 50 + "\n")
|
|
|
|
# Compile final response
|
|
final = "# COMPLETE GILBOT CONTROLLER - MODULAR BUILD\n\n"
|
|
for module, code in all_code.items():
|
|
final += f"## {module.upper()} MODULE\n{code}\n\n"
|
|
|
|
return final
|
|
|
|
def apply_style_signature(self, generated_code: str) -> str:
|
|
"""Refine generated code to match James's specific naming and safety patterns"""
|
|
# Apply Hardware Profile Rules (ESP32-C3 default for now)
|
|
generated_code = self.hardware_profile.apply_hardware_rules(generated_code, self.current_hardware)
|
|
|
|
# Apply learned replacements (High Confidence Only)
|
|
rules = self.get_learned_rules()
|
|
for r in rules:
|
|
if r['confidence'] >= 0.95 and r['find'] and r['replace']:
|
|
# Simple safety check: don't replace if replacement contains spaces (likely a description)
|
|
if ' ' not in r['replace']:
|
|
try:
|
|
generated_code = re.sub(r['find'], r['replace'], generated_code)
|
|
except re.error:
|
|
pass
|
|
|
|
return generated_code
|
|
|
|
def record_feedback(self, message_id: int, feedback: bool, comment: str = "") -> Optional[str]:
|
|
"""Learn from user feedback."""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute("""
|
|
INSERT INTO feedback (message_id, positive, comment, timestamp)
|
|
VALUES (?, ?, ?, ?)
|
|
""", (message_id, feedback, comment, datetime.now().isoformat()))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
# Adjust confidence scores
|
|
self.update_style_confidence(message_id, feedback)
|
|
|
|
if not feedback:
|
|
self.analyze_failure(message_id)
|
|
return self.regenerate_response(message_id, comment)
|
|
return None
|
|
|
|
def regenerate_response(self, message_id: int, comment: str = "") -> str:
|
|
"""Regenerate a response, optionally considering feedback comment"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("SELECT session_id, id FROM messages WHERE id = ?", (message_id,))
|
|
row = cursor.fetchone()
|
|
if not row:
|
|
conn.close()
|
|
return "Error: Message not found."
|
|
|
|
session_id, current_id = row
|
|
|
|
cursor.execute(
|
|
"SELECT content FROM messages WHERE session_id = ? AND id < ? AND role = 'user' ORDER BY id DESC LIMIT 1",
|
|
(session_id, current_id)
|
|
)
|
|
user_row = cursor.fetchone()
|
|
conn.close()
|
|
|
|
if user_row:
|
|
prompt = user_row[0]
|
|
if comment:
|
|
prompt += f"\n\n[Feedback: {comment}]"
|
|
|
|
print(f"🔄 Regenerating: {prompt[:50]}...")
|
|
return self.chat(prompt)
|
|
return "Error: Original prompt not found."
|
|
|
|
def analyze_failure(self, message_id: int) -> None:
|
|
"""Analyze why a message received negative feedback"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT content FROM messages WHERE id = ?", (message_id,))
|
|
row = cursor.fetchone()
|
|
conn.close()
|
|
|
|
if row:
|
|
print(f"\n⚠️ Negative Feedback on Message #{message_id}")
|
|
print(f" Content: {row[0][:100]}...")
|
|
|
|
def update_style_confidence(self, message_id: int, positive: bool) -> None:
|
|
"""Adjust confidence of style preferences based on feedback."""
|
|
# message_id and positive are currently unused
|
|
# Placeholder for V4.0 learning loop
|
|
pass
|
|
|
|
def _route_request(self, user_message: str, force_model: Optional[str], forge_mode: str) -> str:
|
|
"""Route the request to the appropriate model or handler."""
|
|
# Determine model based on complexity
|
|
if force_model:
|
|
model = force_model
|
|
print(f"\n⚡ Using {model.upper()} model (forced)...")
|
|
return self.call_model(model, user_message)
|
|
elif self.is_complex(user_message):
|
|
modules = self.extract_modules(user_message)
|
|
plan = self.build_modular_plan(modules)
|
|
print("\n" + "=" * 50)
|
|
print("🎯 COMPLEX REQUEST DETECTED!")
|
|
print(f"Modules needed: {', '.join(modules)}")
|
|
print(f"Breaking into {len(plan)} manageable steps")
|
|
print("=" * 50)
|
|
return self.execute_modular_build(user_message, modules, plan, forge_mode)
|
|
elif self.is_search_query(user_message):
|
|
# This is a search query - query the database
|
|
return self.search_repositories(user_message)
|
|
elif self.is_simple_question(user_message):
|
|
print("\n⚡ Using FAST model (simple question)...")
|
|
# Don't force code generation prompt for simple greetings or definitions
|
|
msg_lower = user_message.lower().strip()
|
|
is_greeting = any(msg_lower.startswith(w) for w in ['hi', 'hello', 'hey', 'good morning', 'good evening']) and len(user_message.split()) < 6
|
|
is_conceptual = any(msg_lower.startswith(w) for w in ['what is', "what's", 'explain', 'tell me about', 'who is', 'can you explain'])
|
|
return self.call_model("fast", user_message, system_task=(is_greeting or is_conceptual))
|
|
else:
|
|
print("\n⚖️ Using BALANCED model...")
|
|
return self.call_model("balanced", user_message)
|
|
|
|
def chat_stream(self, user_message: str, force_model: Optional[str] = None, forge_mode: str = "2") -> Generator[str, None, None]:
|
|
"""Streaming version of chat"""
|
|
|
|
|
|
# Intercept commands
|
|
if user_message.strip().startswith('/'):
|
|
yield self.handle_slash_command(user_message.strip())
|
|
return
|
|
|
|
# Detect Hardware Context
|
|
detected_hw = self.hardware_profile.detect_hardware(user_message)
|
|
if detected_hw:
|
|
self.current_hardware = detected_hw
|
|
|
|
style_context = self.retrieve_style_context(user_message)
|
|
if style_context:
|
|
self.context_messages.append({"role": "system", "content": style_context})
|
|
|
|
user_msg_id = self.save_message("user", user_message)
|
|
self.context_messages.append({"id": user_msg_id, "role": "user", "content": user_message, "timestamp": datetime.now().isoformat()})
|
|
|
|
full_response = ""
|
|
|
|
# Route and stream
|
|
if force_model:
|
|
iterator = self.call_model(force_model, user_message, stream=True)
|
|
elif self.is_complex(user_message):
|
|
# Complex builds are not streamed token-by-token in this version
|
|
# We yield the final result as one chunk
|
|
modules = self.extract_modules(user_message)
|
|
plan = self.build_modular_plan(modules)
|
|
result = self.execute_modular_build(user_message, modules, plan, forge_mode)
|
|
iterator = [result]
|
|
elif self.is_search_query(user_message):
|
|
result = self.search_repositories(user_message)
|
|
iterator = [result]
|
|
elif self.is_simple_question(user_message):
|
|
msg_lower = user_message.lower().strip()
|
|
is_greeting = any(msg_lower.startswith(w) for w in ['hi', 'hello', 'hey', 'good morning', 'good evening']) and len(user_message.split()) < 6
|
|
is_conceptual = any(msg_lower.startswith(w) for w in ['what is', "what's", 'explain', 'tell me about', 'who is', 'can you explain'])
|
|
iterator = self.call_model("fast", user_message, stream=True, system_task=(is_greeting or is_conceptual))
|
|
else:
|
|
iterator = self.call_model("balanced", user_message, stream=True)
|
|
|
|
for chunk in iterator:
|
|
full_response += chunk
|
|
yield chunk
|
|
|
|
# Suggestions
|
|
suggestions = self.shadow_engine.get_all_suggestions(user_message, full_response)
|
|
if suggestions:
|
|
bar = "\n\nPROACTIVE: > " + " ".join([f"{i+1}. {s}" for i, s in enumerate(suggestions)])
|
|
full_response += bar
|
|
yield bar
|
|
|
|
msg_id = self.save_message("assistant", full_response)
|
|
self.last_generated_id = msg_id
|
|
self.context_messages.append({"id": msg_id, "role": "assistant", "content": full_response, "timestamp": datetime.now().isoformat()})
|
|
|
|
def extract_code(self, text: str) -> List[str]:
|
|
"""Extract code blocks from markdown"""
|
|
return re.findall(r'```(?:\w+)?\n(.*?)```', text, re.DOTALL)
|
|
|
|
def handle_slash_command(self, command: str) -> str:
|
|
"""Handle slash commands when received via chat interface"""
|
|
cmd = command.lower()
|
|
|
|
if cmd.startswith('/teach'):
|
|
rule = command[7:].strip()
|
|
if rule:
|
|
self.teach_rule(rule)
|
|
return f"✅ Learned rule: {rule}"
|
|
return "Usage: /teach <rule description>"
|
|
|
|
if cmd.startswith('/correct'):
|
|
reason = command[8:].strip()
|
|
last_response = ""
|
|
for msg in reversed(self.context_messages):
|
|
if msg['role'] == 'assistant':
|
|
last_response = msg['content']
|
|
break
|
|
if last_response:
|
|
self.save_correction(last_response, "", reason)
|
|
return "✅ Correction saved. (Run /learn to process patterns)"
|
|
return "❌ No recent message to correct."
|
|
|
|
if cmd == '/rules':
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT rule_text, confidence FROM code_rules ORDER BY confidence DESC")
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
if not rows: return "🤷 No rules learned yet."
|
|
return "🧠 Learned Rules:\n" + "\n".join([f"- {r[0]}" for r in rows])
|
|
|
|
if cmd == '/learn':
|
|
patterns = self.learner.analyze_corrections(self)
|
|
if patterns:
|
|
return f"✅ Learned {len(patterns)} new rules:\n" + "\n".join([f"- {p['rule']}" for p in patterns])
|
|
return "No new patterns found."
|
|
|
|
if cmd == '/metrics':
|
|
stats = self.metrics.calculate_accuracy()
|
|
return (f"📊 Learning Metrics (Last 30 Days):\n"
|
|
f" Accuracy: {stats['accuracy']:.1f}%\n"
|
|
f" Correction Rate: {stats['correction_rate']:.1f}%\n"
|
|
f" Trend (7d): {stats['improvement']}")
|
|
|
|
if cmd == '/debug':
|
|
if self.last_prompt_debug:
|
|
return f"🐛 Last Prompt Sent:\n```json\n{self.last_prompt_debug}\n```"
|
|
return "❌ No prompt sent yet."
|
|
|
|
if cmd == '/validate':
|
|
last_response = ""
|
|
user_context = ""
|
|
|
|
# Find last assistant message and preceding user message
|
|
for i in range(len(self.context_messages) - 1, -1, -1):
|
|
if self.context_messages[i]['role'] == 'assistant':
|
|
last_response = self.context_messages[i]['content']
|
|
if i > 0 and self.context_messages[i-1]['role'] == 'user':
|
|
user_context = self.context_messages[i-1]['content']
|
|
break
|
|
|
|
if not last_response:
|
|
return "❌ No recent code to validate."
|
|
|
|
code_blocks = self.extract_code(last_response)
|
|
if not code_blocks:
|
|
return "❌ No code blocks found in last response."
|
|
|
|
report = ["🔍 Validating last response..."]
|
|
all_valid = True
|
|
for i, code in enumerate(code_blocks, 1):
|
|
valid, issues = self.validator.validate(code, self.current_hardware, user_context)
|
|
if not valid:
|
|
all_valid = False
|
|
report.append(f"\nBlock {i} Issues:")
|
|
for issue in issues:
|
|
icon = "❌" if issue['severity'] == 'error' else "⚠️"
|
|
report.append(f" {icon} Line {issue.get('line', '?')}: {issue['message']}")
|
|
else:
|
|
report.append(f"✅ Block {i} is valid.")
|
|
|
|
if all_valid:
|
|
report.append("\n✨ All code blocks look good!")
|
|
|
|
return "\n".join(report)
|
|
|
|
if cmd == '/status':
|
|
mem_usage = "N/A"
|
|
if psutil:
|
|
process = psutil.Process(os.getpid())
|
|
mem_usage = f"{process.memory_info().rss / 1024 / 1024:.0f} MB"
|
|
|
|
return (f"🖥️ System Status:\n"
|
|
f" Session: {self.session_id}\n"
|
|
f" Hardware: {self.current_hardware}\n"
|
|
f" Memory: {mem_usage}\n"
|
|
f" Messages: {len(self.context_messages)}")
|
|
|
|
return f"Command {cmd.split()[0]} not supported in chat mode."
|
|
|
|
|
|
# --- Main Chat Method ---
|
|
def chat(self, user_message: str, force_model: Optional[str] = None, forge_mode: str = "2") -> str:
|
|
"""Main chat with smart routing and shadow suggestions"""
|
|
|
|
# Intercept commands
|
|
if user_message.strip().startswith('/'):
|
|
return self.handle_slash_command(user_message.strip())
|
|
|
|
# Detect Hardware Context
|
|
detected_hw = self.hardware_profile.detect_hardware(user_message)
|
|
if detected_hw:
|
|
self.current_hardware = detected_hw
|
|
print(f"🔧 Target Hardware Detected: {self.current_hardware}")
|
|
|
|
style_context = self.retrieve_style_context(user_message)
|
|
if style_context:
|
|
self.context_messages.append({"role": "system", "content": style_context})
|
|
|
|
user_msg_id = self.save_message("user", user_message)
|
|
self.context_messages.append({"id": user_msg_id, "role": "user", "content": user_message, "timestamp": datetime.now().isoformat()})
|
|
|
|
# Direct Schedule Check
|
|
if "what should i be doing" in user_message.lower() or "my schedule" in user_message.lower() or "schedule check" in user_message.lower():
|
|
status = self.get_user_status()
|
|
response = f"📅 **Schedule Check**\nAccording to your protocol, you should be: **{status}**"
|
|
print(f"⏰ Schedule check triggered: {status}")
|
|
msg_id = self.save_message("assistant", response)
|
|
self.last_generated_id = msg_id
|
|
self.context_messages.append({"id": msg_id, "role": "assistant", "content": response, "timestamp": datetime.now().isoformat()})
|
|
return response
|
|
|
|
response = self._route_request(user_message, force_model, forge_mode)
|
|
|
|
# Apply Style Guard
|
|
response = self.apply_style_signature(response)
|
|
|
|
# Extract code blocks
|
|
code_blocks = self.extract_code(response)
|
|
|
|
# Validate each code block
|
|
for code in code_blocks:
|
|
valid, issues = self.validator.validate(code, self.current_hardware, user_message)
|
|
|
|
if not valid:
|
|
# Auto-fix critical issues
|
|
fixed_code = self.validator.auto_fix(code, issues)
|
|
response = response.replace(code, fixed_code)
|
|
|
|
# Sanitize explanation text based on fixes
|
|
for issue in issues:
|
|
if "Debouncing detected" in issue['message']:
|
|
response = re.sub(r'(?i)(\*\*?Debouncing\*\*?:?|Debouncing)', r'~~\1~~ (Removed)', response)
|
|
|
|
# Append explanation
|
|
response += "\n\n⚠️ **Auto-corrected:**\n"
|
|
for issue in issues:
|
|
if issue['severity'] == 'error':
|
|
response += f"- {issue['message']}\n"
|
|
|
|
# Generate Suggestion Bar
|
|
suggestions = self.shadow_engine.get_all_suggestions(user_message, response)
|
|
if suggestions:
|
|
bar = "\n\nPROACTIVE: > " + " ".join([f"{i+1}. {s}" for i, s in enumerate(suggestions)])
|
|
response += bar
|
|
|
|
msg_id = self.save_message("assistant", response)
|
|
self.last_generated_id = msg_id
|
|
self.context_messages.append({"id": msg_id, "role": "assistant", "content": response, "timestamp": datetime.now().isoformat()})
|
|
|
|
return response
|
|
|
|
def get_sessions(self, limit: int = 20) -> List[Dict[str, str]]:
|
|
"""Retrieve recent sessions from DB"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT session_id, started_at, title FROM sessions WHERE user_id = ? ORDER BY started_at DESC LIMIT ?", (self.user_id, limit))
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
return [{"id": r[0], "date": r[1], "title": r[2] if len(r) > 2 else None} for r in rows]
|
|
|
|
def rename_session(self, session_id: str, new_title: str) -> None:
|
|
"""Rename a session"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute("UPDATE sessions SET title = ? WHERE session_id = ? AND user_id = ?", (new_title, session_id, self.user_id))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def delete_session(self, session_id: str) -> None:
|
|
"""Delete a session and its messages"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute("DELETE FROM sessions WHERE session_id = ? AND user_id = ?", (session_id, self.user_id))
|
|
if cursor.rowcount > 0:
|
|
cursor.execute("DELETE FROM messages WHERE session_id = ?", (session_id,))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def clear_current_session(self) -> None:
|
|
"""Clear all messages from the current session"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute("DELETE FROM messages WHERE session_id = ?", (self.session_id,))
|
|
conn.commit()
|
|
conn.close()
|
|
self.context_messages = []
|
|
|
|
def load_session(self, session_id: str) -> List[Dict[str, str]]:
|
|
"""Load a specific session context"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("SELECT 1 FROM sessions WHERE session_id = ? AND user_id = ?", (session_id, self.user_id))
|
|
if not cursor.fetchone():
|
|
conn.close()
|
|
return []
|
|
|
|
cursor.execute("SELECT id, role, content, timestamp FROM messages WHERE session_id = ? ORDER BY id ASC", (session_id,))
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
|
|
self.session_id = session_id
|
|
self.context_messages = []
|
|
loaded_history = []
|
|
for msg_id, role, content, ts in rows:
|
|
msg = {"id": msg_id, "role": role, "content": content, "timestamp": ts}
|
|
self.context_messages.append(msg)
|
|
loaded_history.append(msg)
|
|
return loaded_history
|
|
|
|
def start_new_session(self) -> str:
|
|
"""Reset context and start new session"""
|
|
self.session_id = self.create_session()
|
|
self.context_messages = []
|
|
return self.session_id
|
|
|
|
def reset_gpu(self) -> str:
|
|
"""Force unload models from GPU to free VRAM"""
|
|
try:
|
|
conn = http.client.HTTPConnection(OLLAMA_HOST, OLLAMA_PORT, timeout=10)
|
|
# Unload all known models
|
|
for model in MODELS.values():
|
|
body = json.dumps({"model": model, "keep_alive": 0})
|
|
conn.request("POST", "/api/generate", body)
|
|
resp = conn.getresponse()
|
|
resp.read() # Consume response
|
|
conn.close()
|
|
return "✅ GPU Memory Cleared (Models Unloaded)"
|
|
except Exception as e:
|
|
return f"❌ Error clearing GPU: {str(e)}"
|
|
|
|
def export_session_to_markdown(self, session_id: str = None) -> str:
|
|
"""Export session history to a Markdown file"""
|
|
sid = session_id or self.session_id
|
|
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT role, content, timestamp FROM messages WHERE session_id = ? ORDER BY id ASC", (sid,))
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
|
|
if not rows:
|
|
return "No history found."
|
|
|
|
filename = f"session_{sid}.md"
|
|
filepath = DATA_DIR / filename
|
|
|
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
f.write(f"# BuddAI Session: {sid}\n\n")
|
|
for role, content, ts in rows:
|
|
f.write(f"### {role.upper()} ({ts})\n\n{content}\n\n---\n\n")
|
|
|
|
return f"✅ Session exported to: {filepath}"
|
|
|
|
def get_session_export_data(self, session_id: str = None) -> Dict:
|
|
"""Get session data as a dictionary for export"""
|
|
sid = session_id or self.session_id
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT role, content, timestamp FROM messages WHERE session_id = ? ORDER BY id ASC", (sid,))
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
|
|
return {
|
|
"session_id": sid,
|
|
"exported_at": datetime.now().isoformat(),
|
|
"messages": [{"role": r, "content": c, "timestamp": t} for r, c, t in rows]
|
|
}
|
|
|
|
def export_session_to_json(self, session_id: str = None) -> str:
|
|
"""Export session history to a JSON file"""
|
|
data = self.get_session_export_data(session_id)
|
|
if not data["messages"]:
|
|
return "No history found."
|
|
|
|
filename = f"session_{data['session_id']}.json"
|
|
filepath = DATA_DIR / filename
|
|
|
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
|
|
return f"✅ Session exported to: {filepath}"
|
|
|
|
def import_session_from_json(self, data: Dict) -> str:
|
|
"""Import a session from JSON data"""
|
|
session_id = data.get("session_id")
|
|
messages = data.get("messages", [])
|
|
|
|
if not session_id or not messages:
|
|
raise ValueError("Invalid session JSON format")
|
|
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
# Check if session exists to avoid collision
|
|
cursor.execute("SELECT 1 FROM sessions WHERE session_id = ? AND user_id = ?", (session_id, self.user_id))
|
|
if cursor.fetchone():
|
|
# Generate new ID
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
session_id = f"{session_id}_imp_{timestamp}"
|
|
|
|
# Determine start time
|
|
started_at = datetime.now().isoformat()
|
|
if messages and "timestamp" in messages[0]:
|
|
started_at = messages[0]["timestamp"]
|
|
|
|
cursor.execute(
|
|
"INSERT INTO sessions (session_id, user_id, started_at, title) VALUES (?, ?, ?, ?)",
|
|
(session_id, self.user_id, started_at, f"Imported: {data.get('session_id')}")
|
|
)
|
|
|
|
# Insert messages
|
|
for msg in messages:
|
|
cursor.execute(
|
|
"INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)",
|
|
(session_id, msg.get("role"), msg.get("content"), msg.get("timestamp", datetime.now().isoformat()))
|
|
)
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return session_id
|
|
|
|
def create_backup(self) -> Tuple[bool, str]:
|
|
"""Create a safe backup of the database"""
|
|
if not DB_PATH.exists():
|
|
return False, "Database file not found."
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
backup_dir = DATA_DIR / "backups"
|
|
backup_dir.mkdir(exist_ok=True)
|
|
backup_path = backup_dir / f"conversations_{timestamp}.db"
|
|
|
|
try:
|
|
# Use SQLite backup API for consistency
|
|
src = sqlite3.connect(DB_PATH)
|
|
dst = sqlite3.connect(backup_path)
|
|
with dst:
|
|
src.backup(dst)
|
|
dst.close()
|
|
src.close()
|
|
return True, str(backup_path)
|
|
except Exception as e:
|
|
return False, str(e)
|
|
|
|
def run(self) -> None:
|
|
"""Main loop"""
|
|
try:
|
|
force_model = None
|
|
while True:
|
|
user_input = input("\nJames: ").strip()
|
|
if not user_input:
|
|
continue
|
|
if user_input.lower() in ['exit', 'quit']:
|
|
print("\n👋 Later!")
|
|
self.end_session()
|
|
break
|
|
if user_input.startswith('/'):
|
|
cmd = user_input.lower()
|
|
if cmd == '/fast':
|
|
force_model = "fast"
|
|
print("⚡ Next: FAST model")
|
|
continue
|
|
elif cmd == '/balanced':
|
|
force_model = "balanced"
|
|
print("⚖️ Next: BALANCED model")
|
|
continue
|
|
elif cmd == '/help':
|
|
print("\n💡 Commands:")
|
|
print("/fast - Use fast model")
|
|
print("/balanced - Use balanced model")
|
|
print("/index <path> - Index local repositories")
|
|
print("/scan - Scan style signature (V3.0)")
|
|
print("/learn - Extract patterns from corrections")
|
|
print("/analyze - Analyze session for implicit feedback")
|
|
print("/correct <reason> - Mark previous response wrong")
|
|
print("/good - Mark previous response correct")
|
|
print("/teach <rule> - Explicitly teach a rule")
|
|
print("/validate - Re-validate last response")
|
|
print("/rules - Show learned rules")
|
|
print("/metrics - Show improvement stats")
|
|
print("/train - Export corrections for fine-tuning")
|
|
print("/save - Export chat to Markdown")
|
|
print("/backup - Backup database")
|
|
print("/help - This message")
|
|
print("exit - End session\n")
|
|
continue
|
|
elif cmd.startswith('/index'):
|
|
parts = user_input.split(maxsplit=1)
|
|
if len(parts) > 1:
|
|
self.index_local_repositories(parts[1])
|
|
else:
|
|
print("Usage: /index <path_to_repos>")
|
|
continue
|
|
elif cmd == '/scan':
|
|
self.scan_style_signature()
|
|
continue
|
|
elif cmd == '/learn':
|
|
print("🧠 Analyzing corrections for patterns...")
|
|
patterns = self.learner.analyze_corrections(self)
|
|
if patterns:
|
|
print(f"✅ Learned {len(patterns)} new rules:")
|
|
for p in patterns:
|
|
print(f" - {p['rule']}")
|
|
else:
|
|
print("No new patterns found.")
|
|
continue
|
|
elif cmd == '/analyze':
|
|
self.adaptive_learner.learn_from_session(self.session_id)
|
|
continue
|
|
elif cmd.startswith('/correct'):
|
|
reason = user_input[8:].strip()
|
|
last_response = ""
|
|
# Find last assistant message
|
|
for msg in reversed(self.context_messages):
|
|
if msg['role'] == 'assistant':
|
|
last_response = msg['content']
|
|
break
|
|
self.save_correction(last_response, "", reason)
|
|
print("✅ Correction saved. Run /learn to process it.")
|
|
continue
|
|
elif cmd == '/good':
|
|
if self.last_generated_id:
|
|
self.record_feedback(self.last_generated_id, True)
|
|
print("✅ Feedback recorded: Positive")
|
|
else:
|
|
print("❌ No recent message to rate.")
|
|
continue
|
|
elif cmd.startswith('/teach'):
|
|
rule = user_input[7:].strip()
|
|
if rule:
|
|
self.teach_rule(rule)
|
|
print(f"✅ Learned rule: {rule}")
|
|
else:
|
|
print("Usage: /teach <rule description>")
|
|
continue
|
|
elif cmd == '/validate':
|
|
last_response = ""
|
|
user_context = ""
|
|
|
|
# Find last assistant message and preceding user message
|
|
for i in range(len(self.context_messages) - 1, -1, -1):
|
|
if self.context_messages[i]['role'] == 'assistant':
|
|
last_response = self.context_messages[i]['content']
|
|
if i > 0 and self.context_messages[i-1]['role'] == 'user':
|
|
user_context = self.context_messages[i-1]['content']
|
|
break
|
|
|
|
if not last_response:
|
|
print("❌ No recent code to validate.")
|
|
continue
|
|
|
|
code_blocks = self.extract_code(last_response)
|
|
if not code_blocks:
|
|
print("❌ No code blocks found in last response.")
|
|
continue
|
|
|
|
print("\n🔍 Validating last response...")
|
|
all_valid = True
|
|
for i, code in enumerate(code_blocks, 1):
|
|
valid, issues = self.validator.validate(code, self.current_hardware, user_context)
|
|
if not valid:
|
|
all_valid = False
|
|
print(f"\nBlock {i} Issues:")
|
|
for issue in issues:
|
|
icon = "❌" if issue['severity'] == 'error' else "⚠️"
|
|
print(f" {icon} Line {issue.get('line', '?')}: {issue['message']}")
|
|
else:
|
|
print(f"✅ Block {i} is valid.")
|
|
|
|
if all_valid:
|
|
print("\n✨ All code blocks look good!")
|
|
continue
|
|
elif cmd == '/rules':
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT rule_text, confidence, learned_from FROM code_rules ORDER BY confidence DESC")
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
|
|
if not rows:
|
|
print("🤷 No rules learned yet.")
|
|
else:
|
|
print(f"\n🧠 Learned Rules ({len(rows)}):")
|
|
for rule, conf, source in rows:
|
|
print(f" - [{conf:.1f}] {rule} ({source})")
|
|
continue
|
|
elif cmd == '/metrics':
|
|
stats = self.metrics.calculate_accuracy()
|
|
print("\n📊 Learning Metrics (Last 30 Days):")
|
|
print(f" Accuracy: {stats['accuracy']:.1f}%")
|
|
print(f" Correction Rate: {stats['correction_rate']:.1f}%")
|
|
print(f" Trend (7d): {stats['improvement']}")
|
|
print("")
|
|
continue
|
|
elif cmd == '/debug':
|
|
if self.last_prompt_debug:
|
|
print(f"\n🐛 Last Prompt Sent:\n{self.last_prompt_debug}\n")
|
|
else:
|
|
print("❌ No prompt sent yet.")
|
|
continue
|
|
elif cmd == '/train':
|
|
result = self.fine_tuner.prepare_training_data()
|
|
print(f"✅ {result}")
|
|
continue
|
|
elif cmd == '/backup':
|
|
success, msg = self.create_backup()
|
|
if success:
|
|
print(f"✅ Database backed up to: {msg}")
|
|
else:
|
|
print(f"❌ Backup failed: {msg}")
|
|
continue
|
|
elif cmd.startswith('/save'):
|
|
if 'json' in user_input.lower():
|
|
print(self.export_session_to_json())
|
|
else:
|
|
print(self.export_session_to_markdown())
|
|
continue
|
|
else:
|
|
print("\nUnknown command. Type /help")
|
|
continue
|
|
# Chat
|
|
response = self.chat(user_input, force_model)
|
|
print(f"\nBuddAI:\n{response}\n")
|
|
force_model = None
|
|
except KeyboardInterrupt:
|
|
print("\n\n👋 Bye!")
|
|
self.end_session()
|
|
|
|
|
|
|
|
class ModelFineTuner:
|
|
"""Fine-tune local model on YOUR corrections"""
|
|
|
|
def prepare_training_data(self):
|
|
"""Convert corrections to training format"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
SELECT original_code, corrected_code, reason
|
|
FROM corrections
|
|
""")
|
|
|
|
training_data = []
|
|
for original, corrected, reason in cursor.fetchall():
|
|
training_data.append({
|
|
"prompt": f"Generate code for: {reason}",
|
|
"completion": corrected,
|
|
"negative_example": original
|
|
})
|
|
|
|
conn.close()
|
|
|
|
# Save as JSONL for fine-tuning
|
|
output_path = DATA_DIR / 'training_data.jsonl'
|
|
with open(output_path, 'w', encoding='utf-8') as f:
|
|
for item in training_data:
|
|
f.write(json.dumps(item) + '\n')
|
|
return f"Exported {len(training_data)} examples to {output_path}"
|
|
|
|
def fine_tune_model(self):
|
|
"""Fine-tune Qwen on your corrections"""
|
|
# This requires:
|
|
# 1. Export training data
|
|
# 2. Use Ollama modelfile or external training
|
|
# 3. Create custom model: qwen2.5-coder-james:3b
|
|
pass
|