Spaces:
Running on Zero
Running on Zero
Upload Harvester files
Browse files- README.md +43 -15
- app.py +414 -7
- bellingcat_tasks.json +122 -0
- code_whitelist.py +750 -0
- growth_engine.py +995 -0
- model_manager.py +842 -0
- osint_tasks.json +129 -0
- reasoning_lab.py +279 -0
- reasoning_tasks.json +128 -0
- reflection_engine.py +1156 -0
- requirements.txt +5 -0
- trust_balance_agent.py +432 -0
README.md
CHANGED
|
@@ -1,15 +1,43 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Harvester
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Harvester GPU Runner
|
| 3 |
+
emoji: "\U0001F9E0"
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: "5.0"
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# Harvester β ZeroGPU Runner
|
| 13 |
+
|
| 14 |
+
Run Harvester's reasoning lab on HuggingFace ZeroGPU (A100 40GB).
|
| 15 |
+
Both DeepSeek R1 14B (reasoner) and Qwen Coder 30B (coder) fully offloaded to GPU.
|
| 16 |
+
|
| 17 |
+
**Pro member** β ZeroGPU effectively unlimited.
|
| 18 |
+
|
| 19 |
+
## Models
|
| 20 |
+
|
| 21 |
+
| Model | Role | Size | VRAM |
|
| 22 |
+
|-------|------|------|------|
|
| 23 |
+
| DeepSeek R1 14B Q4_K_M | Reasoner | 8.37 GB | ~9 GB |
|
| 24 |
+
| Qwen3 Coder 30B A3B Q4_K_M | Coder | 17.35 GB | ~18 GB |
|
| 25 |
+
| **Total** | | **25.72 GB** | **~27 GB / 40 GB** |
|
| 26 |
+
|
| 27 |
+
## Task Banks
|
| 28 |
+
|
| 29 |
+
- `bellingcat_tasks.json` β 15 OSINT investigation tasks
|
| 30 |
+
- `osint_tasks.json` β 15 missing-persons / OSINT automation tasks
|
| 31 |
+
- `reasoning_tasks.json` β 15 hard C# architecture tasks
|
| 32 |
+
|
| 33 |
+
## Setup
|
| 34 |
+
|
| 35 |
+
1. Create a new Space on HuggingFace (Gradio SDK)
|
| 36 |
+
2. Run `python setup_hf_space.py` locally to copy all needed files
|
| 37 |
+
3. Push the `hf_space/` contents to your Space repo
|
| 38 |
+
4. The app auto-starts β use the UI to run labs
|
| 39 |
+
|
| 40 |
+
## CPU Baseline (i9-13900KF, no GPU)
|
| 41 |
+
|
| 42 |
+
- Bellingcat: 7/15 high confidence, 3.4 hours
|
| 43 |
+
- Kaggle SAE: 16/16 (100%)
|
app.py
CHANGED
|
@@ -1,7 +1,414 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
HARVESTER β HUGGING FACE ZEROGPU RUNNER
|
| 3 |
+
========================================
|
| 4 |
+
Runs Harvester's reasoning lab on ZeroGPU (A100 40GB).
|
| 5 |
+
Both DeepSeek R1 14B (reasoner) and Qwen Coder 30B (coder) fully offloaded.
|
| 6 |
+
|
| 7 |
+
Pro member = effectively unlimited GPU time.
|
| 8 |
+
|
| 9 |
+
Architecture:
|
| 10 |
+
- @spaces.GPU decorated functions get A100 allocation
|
| 11 |
+
- Models downloaded once via HF hub cache, persist across calls
|
| 12 |
+
- Each lab run = one GPU session (duration=1800s for Pro)
|
| 13 |
+
- Results accumulate in lab_results/ and are downloadable
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import gradio as gr
|
| 17 |
+
import spaces
|
| 18 |
+
import os
|
| 19 |
+
import subprocess
|
| 20 |
+
import sys
|
| 21 |
+
import json
|
| 22 |
+
import time
|
| 23 |
+
import glob
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
|
| 26 |
+
# ββ Model config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 27 |
+
|
| 28 |
+
MODEL_DIR = "/tmp/harvester_models"
|
| 29 |
+
|
| 30 |
+
MODELS = {
|
| 31 |
+
"reasoner": {
|
| 32 |
+
"repo": "bartowski/DeepSeek-R1-Distill-Qwen-14B-GGUF",
|
| 33 |
+
"file": "DeepSeek-R1-Distill-Qwen-14B-Q4_K_M.gguf",
|
| 34 |
+
"label": "DeepSeek R1 14B",
|
| 35 |
+
"size": "8.37 GB",
|
| 36 |
+
},
|
| 37 |
+
"coder": {
|
| 38 |
+
"repo": "lmstudio-community/Qwen3-Coder-30B-A3B-Instruct-GGUF",
|
| 39 |
+
"file": "Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf",
|
| 40 |
+
"label": "Qwen Coder 30B A3B",
|
| 41 |
+
"size": "17.35 GB",
|
| 42 |
+
},
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
# Task banks available in this Space
|
| 46 |
+
TASK_BANKS = {
|
| 47 |
+
"bellingcat_tasks.json": "Bellingcat OSINT (15 tasks) β DeepSeek R1",
|
| 48 |
+
"osint_tasks.json": "OSINT / Missing Persons (15 tasks) β DeepSeek R1",
|
| 49 |
+
"reasoning_tasks.json": "C# Architecture (15 tasks) β Qwen Coder",
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# ββ Environment setup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 54 |
+
|
| 55 |
+
def _model_path(key):
|
| 56 |
+
return os.path.join(MODEL_DIR, MODELS[key]["file"])
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _setup_env():
|
| 60 |
+
"""Configure env vars so reflection_engine.py uses the right model paths."""
|
| 61 |
+
os.environ["CODE_ENGINE_BACKEND"] = "native"
|
| 62 |
+
os.environ["NATIVE_GPU_LAYERS"] = "-1" # full GPU offload
|
| 63 |
+
os.environ["NATIVE_CTX_SIZE"] = "16384" # 2x local ctx (A100 has room)
|
| 64 |
+
os.environ["NATIVE_MODEL_PATH"] = _model_path("coder")
|
| 65 |
+
os.environ["NATIVE_HF_REPO"] = MODELS["coder"]["repo"]
|
| 66 |
+
os.environ["NATIVE_HF_FILE"] = MODELS["coder"]["file"]
|
| 67 |
+
os.environ["REASONER_MODEL_PATH"] = _model_path("reasoner")
|
| 68 |
+
os.environ["REASONER_HF_REPO"] = MODELS["reasoner"]["repo"]
|
| 69 |
+
os.environ["REASONER_HF_FILE"] = MODELS["reasoner"]["file"]
|
| 70 |
+
os.environ["PYTHONIOENCODING"] = "utf-8"
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _download_models():
|
| 74 |
+
"""Download models via HF hub. Cached after first download."""
|
| 75 |
+
from huggingface_hub import hf_hub_download
|
| 76 |
+
os.makedirs(MODEL_DIR, exist_ok=True)
|
| 77 |
+
|
| 78 |
+
status_lines = []
|
| 79 |
+
for key, info in MODELS.items():
|
| 80 |
+
path = _model_path(key)
|
| 81 |
+
if os.path.isfile(path):
|
| 82 |
+
status_lines.append(f" {info['label']}: cached ({info['size']})")
|
| 83 |
+
else:
|
| 84 |
+
status_lines.append(f" {info['label']}: downloading {info['size']}...")
|
| 85 |
+
hf_hub_download(
|
| 86 |
+
repo_id=info["repo"],
|
| 87 |
+
filename=info["file"],
|
| 88 |
+
local_dir=MODEL_DIR,
|
| 89 |
+
)
|
| 90 |
+
status_lines.append(f" {info['label']}: done")
|
| 91 |
+
return "\n".join(status_lines)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _reset_engine():
|
| 95 |
+
"""Reset reflection_engine singletons so fresh GPU handles are used."""
|
| 96 |
+
if "reflection_engine" in sys.modules:
|
| 97 |
+
mod = sys.modules["reflection_engine"]
|
| 98 |
+
mod._native_llm = None
|
| 99 |
+
mod._reasoning_llm = None
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _gpu_info():
|
| 103 |
+
"""Get nvidia-smi summary."""
|
| 104 |
+
try:
|
| 105 |
+
r = subprocess.run(
|
| 106 |
+
["nvidia-smi", "--query-gpu=name,memory.total,memory.used,memory.free",
|
| 107 |
+
"--format=csv,noheader"],
|
| 108 |
+
capture_output=True, text=True, timeout=10,
|
| 109 |
+
)
|
| 110 |
+
return r.stdout.strip()
|
| 111 |
+
except Exception as e:
|
| 112 |
+
return f"nvidia-smi failed: {e}"
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# ββ GPU functions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 116 |
+
|
| 117 |
+
@spaces.GPU(duration=120)
|
| 118 |
+
def gpu_check():
|
| 119 |
+
"""Verify GPU, download models, show status."""
|
| 120 |
+
gpu = _gpu_info()
|
| 121 |
+
_setup_env()
|
| 122 |
+
model_status = _download_models()
|
| 123 |
+
|
| 124 |
+
# Quick load test β just import & verify model file exists
|
| 125 |
+
reasoner_ok = os.path.isfile(_model_path("reasoner"))
|
| 126 |
+
coder_ok = os.path.isfile(_model_path("coder"))
|
| 127 |
+
|
| 128 |
+
return f"""=== GPU STATUS ===
|
| 129 |
+
{gpu}
|
| 130 |
+
|
| 131 |
+
=== MODELS ===
|
| 132 |
+
{model_status}
|
| 133 |
+
|
| 134 |
+
=== READY ===
|
| 135 |
+
Reasoner (DeepSeek R1 14B): {"OK" if reasoner_ok else "MISSING"}
|
| 136 |
+
Coder (Qwen 30B A3B): {"OK" if coder_ok else "MISSING"}
|
| 137 |
+
GPU layers: -1 (full offload)
|
| 138 |
+
Context size: 16384
|
| 139 |
+
VRAM budget: ~27 GB / 40 GB
|
| 140 |
+
"""
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
@spaces.GPU(duration=300)
|
| 144 |
+
def gpu_smoke_test():
|
| 145 |
+
"""Quick reasoning test β one task on DeepSeek R1."""
|
| 146 |
+
_setup_env()
|
| 147 |
+
_download_models()
|
| 148 |
+
_reset_engine()
|
| 149 |
+
|
| 150 |
+
from reflection_engine import set_task_mode, reason
|
| 151 |
+
|
| 152 |
+
set_task_mode("reasoning")
|
| 153 |
+
t0 = time.time()
|
| 154 |
+
plan = reason(
|
| 155 |
+
"Given satellite imagery showing a building complex, "
|
| 156 |
+
"outline 3 steps to geolocate it using only visual clues."
|
| 157 |
+
)
|
| 158 |
+
elapsed = time.time() - t0
|
| 159 |
+
gpu = _gpu_info()
|
| 160 |
+
|
| 161 |
+
return f"""=== SMOKE TEST ({elapsed:.1f}s) ===
|
| 162 |
+
GPU: {gpu}
|
| 163 |
+
|
| 164 |
+
Approach:
|
| 165 |
+
{plan.approach[:500]}
|
| 166 |
+
|
| 167 |
+
Steps ({len(plan.steps)}):
|
| 168 |
+
{chr(10).join(f" {i+1}. {s}" for i, s in enumerate(plan.steps))}
|
| 169 |
+
|
| 170 |
+
Confidence: {plan.confidence:.0%}
|
| 171 |
+
Status: {"GPU INFERENCE WORKING" if plan.confidence > 0.3 else "FALLBACK β check model load"}
|
| 172 |
+
"""
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
@spaces.GPU(duration=1800)
|
| 176 |
+
def gpu_run_lab(bank_name, reason_only):
|
| 177 |
+
"""Run full task bank. 30-min GPU allocation (Pro)."""
|
| 178 |
+
_setup_env()
|
| 179 |
+
_download_models()
|
| 180 |
+
_reset_engine()
|
| 181 |
+
|
| 182 |
+
# Run via subprocess for clean isolation
|
| 183 |
+
args = [sys.executable, "reasoning_lab.py", "--bank", bank_name]
|
| 184 |
+
if reason_only:
|
| 185 |
+
args.append("--reason-only")
|
| 186 |
+
|
| 187 |
+
t0 = time.time()
|
| 188 |
+
result = subprocess.run(
|
| 189 |
+
args,
|
| 190 |
+
capture_output=True,
|
| 191 |
+
text=True,
|
| 192 |
+
timeout=1700,
|
| 193 |
+
env={**os.environ},
|
| 194 |
+
)
|
| 195 |
+
total_time = time.time() - t0
|
| 196 |
+
|
| 197 |
+
output = result.stdout
|
| 198 |
+
if result.returncode != 0:
|
| 199 |
+
output += f"\n\nSTDERR (last 2000 chars):\n{result.stderr[-2000:]}"
|
| 200 |
+
|
| 201 |
+
# Parse and format results
|
| 202 |
+
reports = sorted(glob.glob("lab_results/lab_*.json"))
|
| 203 |
+
results_table = ""
|
| 204 |
+
if reports:
|
| 205 |
+
with open(reports[-1]) as f:
|
| 206 |
+
report = json.load(f)
|
| 207 |
+
summary = report.get("summary", {})
|
| 208 |
+
results_list = report.get("results", [])
|
| 209 |
+
|
| 210 |
+
header = f"\n{'Task':<40} {'Conf':>6} {'Time':>8}\n{'-' * 56}\n"
|
| 211 |
+
rows = ""
|
| 212 |
+
for r in results_list:
|
| 213 |
+
rows += f"{r['name']:<40} {r['confidence']:.0%} {r['time_secs']:>7.1f}s\n"
|
| 214 |
+
|
| 215 |
+
results_table = f"""
|
| 216 |
+
=== A100 GPU RESULTS ===
|
| 217 |
+
Bank: {bank_name}
|
| 218 |
+
Tasks: {report.get('task_count', '?')}
|
| 219 |
+
Avg confidence: {summary.get('avg_confidence', 0):.0%}
|
| 220 |
+
High confidence: {summary.get('high_confidence', 0)}
|
| 221 |
+
Low confidence: {summary.get('low_confidence', 0)}
|
| 222 |
+
Total time: {total_time:.0f}s ({total_time/60:.1f} min)
|
| 223 |
+
|
| 224 |
+
=== CPU BASELINE (i9-13900KF, no GPU) ===
|
| 225 |
+
Avg confidence: 56%
|
| 226 |
+
High confidence: 7
|
| 227 |
+
Low confidence: 8
|
| 228 |
+
Total time: 12120s (3.4 hours)
|
| 229 |
+
|
| 230 |
+
=== SPEEDUP ===
|
| 231 |
+
{total_time:.0f}s vs 12120s = {12120/max(total_time,1):.1f}x faster
|
| 232 |
+
{header}{rows}"""
|
| 233 |
+
|
| 234 |
+
# Trim to last 5000 chars of stdout + results table
|
| 235 |
+
return output[-3000:] + "\n" + results_table
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def gpu_run_single_task(bank_name, task_id, reason_only):
|
| 239 |
+
"""Run a single task from a bank (wrapper for the GPU function)."""
|
| 240 |
+
return _gpu_run_single(bank_name, task_id, reason_only)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
@spaces.GPU(duration=300)
|
| 244 |
+
def _gpu_run_single(bank_name, task_id, reason_only):
|
| 245 |
+
"""Run one task. 5-min GPU allocation."""
|
| 246 |
+
_setup_env()
|
| 247 |
+
_download_models()
|
| 248 |
+
_reset_engine()
|
| 249 |
+
|
| 250 |
+
args = [sys.executable, "reasoning_lab.py",
|
| 251 |
+
"--bank", bank_name, "--task", str(task_id)]
|
| 252 |
+
if reason_only:
|
| 253 |
+
args.append("--reason-only")
|
| 254 |
+
|
| 255 |
+
t0 = time.time()
|
| 256 |
+
result = subprocess.run(
|
| 257 |
+
args, capture_output=True, text=True, timeout=280,
|
| 258 |
+
env={**os.environ},
|
| 259 |
+
)
|
| 260 |
+
elapsed = time.time() - t0
|
| 261 |
+
|
| 262 |
+
output = f"=== TASK {task_id} ({elapsed:.1f}s) ===\n"
|
| 263 |
+
output += result.stdout
|
| 264 |
+
if result.returncode != 0:
|
| 265 |
+
output += f"\nSTDERR:\n{result.stderr[-1000:]}"
|
| 266 |
+
return output
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def list_tasks(bank_name):
|
| 270 |
+
"""List tasks in a bank (no GPU needed)."""
|
| 271 |
+
try:
|
| 272 |
+
with open(bank_name) as f:
|
| 273 |
+
data = json.load(f)
|
| 274 |
+
# Handle both formats: plain list or {"tasks": [...]}
|
| 275 |
+
tasks = data if isinstance(data, list) else data.get("tasks", [])
|
| 276 |
+
lines = [f"{'ID':>3} {'Diff':>4} {'Name':<40} Category"]
|
| 277 |
+
lines.append("-" * 70)
|
| 278 |
+
for t in tasks:
|
| 279 |
+
tid = t.get("id", "?")
|
| 280 |
+
diff = t.get("difficulty", "?")
|
| 281 |
+
name = t.get("name", "untitled")
|
| 282 |
+
cat = t.get("category", "")
|
| 283 |
+
lines.append(f"{tid:>3} {diff:>4}β
{name:<40} {cat}")
|
| 284 |
+
return "\n".join(lines)
|
| 285 |
+
except Exception as e:
|
| 286 |
+
return f"Error loading {bank_name}: {e}"
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def view_results():
|
| 290 |
+
"""Show the latest lab results (no GPU needed)."""
|
| 291 |
+
reports = sorted(glob.glob("lab_results/lab_*.json"))
|
| 292 |
+
if not reports:
|
| 293 |
+
return "No results yet β run a lab first."
|
| 294 |
+
|
| 295 |
+
output = f"Found {len(reports)} report(s):\n\n"
|
| 296 |
+
for rpath in reports[-5:]: # last 5
|
| 297 |
+
with open(rpath) as f:
|
| 298 |
+
report = json.load(f)
|
| 299 |
+
summary = report.get("summary", {})
|
| 300 |
+
name = os.path.basename(rpath)
|
| 301 |
+
output += f"--- {name} ---\n"
|
| 302 |
+
output += f" Tasks: {report.get('task_count', '?')}\n"
|
| 303 |
+
output += f" Avg confidence: {summary.get('avg_confidence', 0):.0%}\n"
|
| 304 |
+
output += f" High: {summary.get('high_confidence', 0)}, "
|
| 305 |
+
output += f"Low: {summary.get('low_confidence', 0)}\n"
|
| 306 |
+
output += f" Time: {summary.get('total_time', 0):.0f}s\n\n"
|
| 307 |
+
return output
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def download_latest_results():
|
| 311 |
+
"""Return path to latest results JSON for download."""
|
| 312 |
+
reports = sorted(glob.glob("lab_results/lab_*.json"))
|
| 313 |
+
if reports:
|
| 314 |
+
return reports[-1]
|
| 315 |
+
return None
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
# ββ Gradio UI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 319 |
+
|
| 320 |
+
with gr.Blocks(title="Harvester GPU Runner", theme=gr.themes.Monochrome()) as app:
|
| 321 |
+
|
| 322 |
+
gr.Markdown("""
|
| 323 |
+
# Harvester β ZeroGPU Runner
|
| 324 |
+
Run Harvester's reasoning lab on **A100 40GB** via HuggingFace ZeroGPU.
|
| 325 |
+
Both **DeepSeek R1 14B** (reasoner) and **Qwen Coder 30B** (coder) fully GPU-offloaded.
|
| 326 |
+
|
| 327 |
+
*Pro member β effectively unlimited GPU time.*
|
| 328 |
+
""")
|
| 329 |
+
|
| 330 |
+
with gr.Tab("Setup"):
|
| 331 |
+
gr.Markdown("### GPU & Model Check")
|
| 332 |
+
gr.Markdown("Downloads models on first run (~26 GB total). Cached after that.")
|
| 333 |
+
setup_btn = gr.Button("Check GPU & Download Models", variant="primary")
|
| 334 |
+
setup_output = gr.Textbox(label="Status", lines=15, interactive=False)
|
| 335 |
+
setup_btn.click(fn=gpu_check, outputs=setup_output)
|
| 336 |
+
|
| 337 |
+
with gr.Tab("Smoke Test"):
|
| 338 |
+
gr.Markdown("### Quick Reasoning Test")
|
| 339 |
+
gr.Markdown("Loads DeepSeek R1, runs one OSINT reasoning task, shows result.")
|
| 340 |
+
smoke_btn = gr.Button("Run Smoke Test", variant="primary")
|
| 341 |
+
smoke_output = gr.Textbox(label="Result", lines=20, interactive=False)
|
| 342 |
+
smoke_btn.click(fn=gpu_smoke_test, outputs=smoke_output)
|
| 343 |
+
|
| 344 |
+
with gr.Tab("Run Lab"):
|
| 345 |
+
gr.Markdown("### Full Lab Run")
|
| 346 |
+
gr.Markdown(
|
| 347 |
+
"Run all tasks in a bank with 30-min GPU allocation. "
|
| 348 |
+
"OSINT/Bellingcat banks auto-route to DeepSeek R1. "
|
| 349 |
+
"Architecture banks auto-route to Qwen Coder."
|
| 350 |
+
)
|
| 351 |
+
with gr.Row():
|
| 352 |
+
bank_dropdown = gr.Dropdown(
|
| 353 |
+
choices=list(TASK_BANKS.keys()),
|
| 354 |
+
value="bellingcat_tasks.json",
|
| 355 |
+
label="Task Bank",
|
| 356 |
+
info="Select which task bank to run",
|
| 357 |
+
)
|
| 358 |
+
reason_only_check = gr.Checkbox(
|
| 359 |
+
value=True,
|
| 360 |
+
label="Reason Only",
|
| 361 |
+
info="Thinking drills without code generation (faster)",
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
with gr.Row():
|
| 365 |
+
list_btn = gr.Button("List Tasks")
|
| 366 |
+
run_btn = gr.Button("Run Full Lab", variant="primary")
|
| 367 |
+
|
| 368 |
+
lab_output = gr.Textbox(label="Output", lines=30, interactive=False)
|
| 369 |
+
|
| 370 |
+
list_btn.click(fn=list_tasks, inputs=bank_dropdown, outputs=lab_output)
|
| 371 |
+
run_btn.click(
|
| 372 |
+
fn=gpu_run_lab,
|
| 373 |
+
inputs=[bank_dropdown, reason_only_check],
|
| 374 |
+
outputs=lab_output,
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
with gr.Tab("Single Task"):
|
| 378 |
+
gr.Markdown("### Run One Task")
|
| 379 |
+
gr.Markdown("Pick a specific task by ID. Uses 5-min GPU allocation per task.")
|
| 380 |
+
with gr.Row():
|
| 381 |
+
single_bank = gr.Dropdown(
|
| 382 |
+
choices=list(TASK_BANKS.keys()),
|
| 383 |
+
value="bellingcat_tasks.json",
|
| 384 |
+
label="Task Bank",
|
| 385 |
+
)
|
| 386 |
+
single_id = gr.Number(value=1, label="Task ID", precision=0)
|
| 387 |
+
single_reason = gr.Checkbox(value=True, label="Reason Only")
|
| 388 |
+
|
| 389 |
+
single_btn = gr.Button("Run Task", variant="primary")
|
| 390 |
+
single_output = gr.Textbox(label="Output", lines=20, interactive=False)
|
| 391 |
+
single_btn.click(
|
| 392 |
+
fn=gpu_run_single_task,
|
| 393 |
+
inputs=[single_bank, single_id, single_reason],
|
| 394 |
+
outputs=single_output,
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
with gr.Tab("Results"):
|
| 398 |
+
gr.Markdown("### View & Download Results")
|
| 399 |
+
results_btn = gr.Button("Refresh Results")
|
| 400 |
+
results_output = gr.Textbox(label="Lab Results", lines=20, interactive=False)
|
| 401 |
+
results_btn.click(fn=view_results, outputs=results_output)
|
| 402 |
+
|
| 403 |
+
download_btn = gr.Button("Download Latest JSON")
|
| 404 |
+
download_file = gr.File(label="Download")
|
| 405 |
+
download_btn.click(fn=download_latest_results, outputs=download_file)
|
| 406 |
+
|
| 407 |
+
gr.Markdown("""
|
| 408 |
+
---
|
| 409 |
+
**Harvester** β self-improving AI code engine.
|
| 410 |
+
Built on engineering principles: Radial Slop, The Ladder, Never Give Up.
|
| 411 |
+
*Work > Code > Talk.*
|
| 412 |
+
""")
|
| 413 |
+
|
| 414 |
+
app.launch()
|
bellingcat_tasks.json
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": 1,
|
| 4 |
+
"name": "Shadow-Based Chronolocator",
|
| 5 |
+
"category": "chronolocation",
|
| 6 |
+
"difficulty": 3,
|
| 7 |
+
"language": "python",
|
| 8 |
+
"description": "Write a Python tool that estimates the time a photo was taken based on shadow analysis. Given an image with a shadow and the known height of the object casting it, plus the GPS coordinates of the location, calculate the sun elevation angle from the shadow length, then use the ephem library to determine what time of day produces that sun angle at those coordinates for a given date range. Output: estimated time window (UTC), sun azimuth, and a confidence range. Handle edge cases: no shadow visible, circumpolar conditions, multiple possible times per day."
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"id": 2,
|
| 12 |
+
"name": "Evidence Archiver & Hasher",
|
| 13 |
+
"category": "preservation",
|
| 14 |
+
"difficulty": 2,
|
| 15 |
+
"language": "python",
|
| 16 |
+
"description": "Write a Python tool for preserving digital evidence with chain-of-custody integrity. Given a URL, download the full page content (HTML + images + embedded media), compute SHA-256 hashes of every artifact, generate a timestamped manifest file (JSON) listing each file with its hash, size, and download timestamp. Save a WARC-like archive bundle. Include a verify command that re-hashes all files against the manifest and reports any tampering. Use requests for download, hashlib for hashing. Support batch mode from a list of URLs in a text file."
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"id": 3,
|
| 20 |
+
"name": "Geolocation Clue Extractor",
|
| 21 |
+
"category": "geolocation",
|
| 22 |
+
"difficulty": 4,
|
| 23 |
+
"language": "python",
|
| 24 |
+
"description": "Write a Python module that analyzes an image to extract geolocation clues. Use Pillow and basic image processing to: (1) detect and extract any visible text (signs, billboards) using contour detection and OCR-ready cropping, (2) identify dominant vegetation color profiles that suggest climate zones (tropical vs temperate vs arid), (3) detect road marking patterns (solid/dashed, color) and estimate driving side (left/right), (4) extract any visible license plate region for format analysis. Output a structured JSON report with each clue type, the extracted data, and a suggested region. Do NOT use cloud APIs β all processing must be local using Pillow and numpy only."
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": 4,
|
| 28 |
+
"name": "Multi-Source Cross-Referencing Engine",
|
| 29 |
+
"category": "verification",
|
| 30 |
+
"difficulty": 5,
|
| 31 |
+
"language": "python",
|
| 32 |
+
"description": "Write a Python tool that cross-references a claim across multiple data sources and produces a verification score. Given a structured claim (JSON with: what, where, when, who), query multiple simulated data feeds (each represented as a JSON file): news_feed.json, social_media.json, satellite_log.json, official_records.json. For each source, search for corroborating or contradicting entries using fuzzy string matching (difflib). Calculate a verification score: each independent corroboration adds weight, contradictions subtract, source reliability weights apply. Output a structured verdict with score, supporting evidence list, contradicting evidence list, and gaps (sources with no relevant data). Include a function to generate a concise verification summary."
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"id": 5,
|
| 36 |
+
"name": "Flight Path Reconstructor",
|
| 37 |
+
"category": "transportation",
|
| 38 |
+
"difficulty": 3,
|
| 39 |
+
"language": "python",
|
| 40 |
+
"description": "Write a Python tool that reconstructs a flight path from ADS-B transponder data. Given a CSV file with columns: timestamp, icao24, callsign, latitude, longitude, altitude, velocity, heading β filter by a specific aircraft (icao24 or callsign), interpolate gaps in position data using linear interpolation, compute total distance traveled (haversine formula), detect takeoff and landing events (altitude thresholds), and identify any holding patterns (repeated circular paths). Output: a GeoJSON LineString of the flight path, a summary JSON with departure/arrival timestamps, total distance, max altitude, and any detected anomalies (sudden altitude changes, course reversals, transponder gaps longer than N minutes)."
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"id": 6,
|
| 44 |
+
"name": "Satellite Change Detector",
|
| 45 |
+
"category": "satellite_imagery",
|
| 46 |
+
"difficulty": 4,
|
| 47 |
+
"language": "python",
|
| 48 |
+
"description": "Write a Python tool that detects changes between two satellite images of the same area taken at different times. Given two images (before.png, after.png) that are already aligned, compute: (1) pixel-level difference map with configurable sensitivity threshold, (2) connected component analysis to identify distinct change regions, (3) bounding boxes and area (in pixels) for each detected change region, (4) classification of change type based on color shift (new construction=gray, vegetation loss=brown, water change=blue). Output a JSON report listing each change region with its bounding box, area, change type, and confidence. Save a visualization image highlighting changes with colored overlays. Use numpy and Pillow only."
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"id": 7,
|
| 52 |
+
"name": "Social Media Timeline Reconstructor",
|
| 53 |
+
"category": "social_media",
|
| 54 |
+
"difficulty": 3,
|
| 55 |
+
"language": "python",
|
| 56 |
+
"description": "Write a Python tool that reconstructs a timeline of events from a collection of social media posts. Given a JSON array of posts (each with: platform, username, timestamp, text, location, media_urls, engagement_count), sort chronologically, detect clusters of posts about the same event (using keyword overlap and time proximity within a configurable window), merge clusters into unified events with earliest/latest timestamps, extract key entities (locations, names, organizations) using regex patterns, and build a timeline JSON with events sorted by time, each containing: time_range, summary of merged posts, locations mentioned, sources (list of original posts), and a thread showing how the narrative evolved. Include duplicate detection by text similarity."
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"id": 8,
|
| 60 |
+
"name": "Network Graph Builder",
|
| 61 |
+
"category": "analysis",
|
| 62 |
+
"difficulty": 3,
|
| 63 |
+
"language": "python",
|
| 64 |
+
"description": "Write a Python tool that builds relationship network graphs from structured investigation data. Given a JSON file of entities (people, organizations, locations, accounts) and a JSON file of connections (entity_a, entity_b, relationship_type, evidence_source, confidence), build an adjacency representation, compute: degree centrality for each node, identify bridge nodes (removal disconnects the graph), find shortest paths between any two entities, detect communities using a simple label propagation algorithm. Output: a full analysis JSON with ranked nodes by centrality, identified communities, bridge nodes, and for a given pair of entities, the shortest connection path with all intermediate relationships. Use only standard library (no networkx)."
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"id": 9,
|
| 68 |
+
"name": "Coordinate Grid Search Generator",
|
| 69 |
+
"category": "geolocation",
|
| 70 |
+
"difficulty": 2,
|
| 71 |
+
"language": "python",
|
| 72 |
+
"description": "Write a Python tool that generates a systematic search grid for satellite imagery analysis. Given a center coordinate (lat, lon), a search radius in kilometers, and a grid cell size in meters, generate a grid of bounding boxes covering the search area. For each cell, output: cell_id, center coordinate, corner coordinates, and a URL template for common tile servers (formatted for OpenStreetMap tile coordinates at a given zoom level). Support both square and hexagonal grid patterns. Output as GeoJSON FeatureCollection where each feature is a grid cell polygon. Include a progress tracker that marks cells as searched/unsearched in a companion JSON state file, so searches can be resumed."
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"id": 10,
|
| 76 |
+
"name": "Metadata Consistency Checker",
|
| 77 |
+
"category": "verification",
|
| 78 |
+
"difficulty": 3,
|
| 79 |
+
"language": "python",
|
| 80 |
+
"description": "Write a Python tool that checks metadata consistency across a set of media files to detect potential manipulation or misattribution. Given a directory of images, extract EXIF data from each using Pillow, then cross-check: (1) do GPS coordinates match the claimed location (within a radius)? (2) do timestamps fall within the claimed time range? (3) are all images from the same camera make/model? (4) are there any timestamp sequence anomalies (out-of-order, impossible gaps, duplicate timestamps)? (5) do any images have stripped or inconsistent EXIF patterns suggesting editing? Output a structured JSON report flagging each inconsistency with severity (info/warning/critical), the affected files, and what specifically doesn't match."
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"id": 11,
|
| 84 |
+
"name": "Open Corporate Registry Parser",
|
| 85 |
+
"category": "corporate_investigation",
|
| 86 |
+
"difficulty": 3,
|
| 87 |
+
"language": "python",
|
| 88 |
+
"description": "Write a Python tool that parses and cross-references company records from structured data sources. Given a JSON file of company records (each with: company_name, registration_number, jurisdiction, directors, registered_address, filing_dates, status), build a search interface that can: (1) find all companies sharing a director name (fuzzy match), (2) find all companies at the same registered address, (3) detect director networks (people who sit on multiple boards together), (4) flag companies with suspicious patterns (registered same day at same address, very recent creation, dormant status). Output: a risk assessment JSON for each company with flags and connections discovered. Include a function that generates a plain-text investigation summary."
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"id": 12,
|
| 92 |
+
"name": "Misinformation Pattern Detector",
|
| 93 |
+
"category": "verification",
|
| 94 |
+
"difficulty": 4,
|
| 95 |
+
"language": "python",
|
| 96 |
+
"description": "Write a Python tool that identifies potential misinformation patterns in a stream of social media posts. Given a JSON array of posts with (id, timestamp, text, source, share_count, reply_to), detect: (1) copy-paste campaigns β near-identical text appearing from different accounts within a short time window, (2) coordinated amplification β unusual spikes in share_count from accounts that regularly amplify each other, (3) narrative injection β new claims that suddenly appear and are rapidly shared with no prior discussion, (4) source concentration β claims that trace back to a single original post but appear to come from many independent sources. Score each detected pattern with confidence and evidence. Output a structured JSON report with each detected pattern, the involved posts, the detection method, and confidence score."
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"id": 13,
|
| 100 |
+
"name": "Haversine Distance Matrix Builder",
|
| 101 |
+
"category": "geolocation",
|
| 102 |
+
"difficulty": 2,
|
| 103 |
+
"language": "python",
|
| 104 |
+
"description": "Write a Python tool that computes a distance matrix between a set of geographic points and identifies spatial patterns. Given a JSON array of named locations (name, lat, lon, timestamp), compute all pairwise haversine distances, identify clusters of points within a configurable radius, determine the geographic centroid of each cluster, calculate the convex hull area of the full point set, and detect travel sequences (ordered by timestamp) that would require impossible speeds (above a configurable threshold). Output: distance matrix as CSV, cluster assignments JSON, travel analysis with flagged impossible movements, and an overall spatial summary."
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"id": 14,
|
| 108 |
+
"name": "Webpage Diff Monitor",
|
| 109 |
+
"category": "monitoring",
|
| 110 |
+
"difficulty": 3,
|
| 111 |
+
"language": "python",
|
| 112 |
+
"description": "Write a Python tool that monitors web pages for changes and preserves snapshots. Given a JSON config with a list of URLs and check_interval_minutes, the tool stores a SQLite database of page snapshots (url, timestamp, content_hash, content). On each check: fetch the page, compute content hash, compare with latest stored version, if changed: store new snapshot, compute a text diff (unified diff format) between old and new content, and log the change. Provide commands: 'check' (run one check cycle), 'history <url>' (show all snapshots with timestamps), 'diff <url> <id1> <id2>' (show diff between two snapshots), 'report' (summary of all monitored URLs and their change frequency). Use requests, hashlib, difflib, sqlite3."
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"id": 15,
|
| 116 |
+
"name": "Investigation Case File Manager",
|
| 117 |
+
"category": "reporting",
|
| 118 |
+
"difficulty": 4,
|
| 119 |
+
"language": "python",
|
| 120 |
+
"description": "Write a Python tool that manages a structured investigation case file. A case contains: entities (people, orgs, locations, accounts), evidence items (files with metadata, hash, source URL, collection timestamp, notes), timeline events (what, when, where, sources, confidence), hypotheses (claim, supporting evidence IDs, contradicting evidence IDs, status). Store everything in SQLite. Support commands: 'add_entity', 'add_evidence', 'add_event', 'add_hypothesis', 'link' (connect evidence to events/entities), 'timeline' (print chronological timeline), 'matrix' (show evidence-hypothesis support matrix), 'export' (generate a structured JSON case file with all data and relationships), 'gaps' (identify timeline gaps, unsupported hypotheses, unlinked evidence). Include chain-of-custody tracking: every modification logged with timestamp and description."
|
| 121 |
+
}
|
| 122 |
+
]
|
code_whitelist.py
ADDED
|
@@ -0,0 +1,750 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MULTI-LANGUAGE CODE WHITELIST
|
| 3 |
+
==============================
|
| 4 |
+
Comprehensive safe-patterns whitelist for code generation and validation.
|
| 5 |
+
Covers: Python, C#, C++, JavaScript/TypeScript, Java, Go, Rust.
|
| 6 |
+
|
| 7 |
+
Two layers:
|
| 8 |
+
1. BLOCKLIST β patterns that must NEVER appear in generated code
|
| 9 |
+
2. WHITELIST β known-safe standard library calls, patterns, and modules
|
| 10 |
+
|
| 11 |
+
Usage:
|
| 12 |
+
from code_whitelist import validate_code, get_safe_patterns, LANGUAGES
|
| 13 |
+
|
| 14 |
+
result = validate_code(code, language="python")
|
| 15 |
+
# result = {"safe": True/False, "blocked": [...], "warnings": [...]}
|
| 16 |
+
|
| 17 |
+
patterns = get_safe_patterns("csharp")
|
| 18 |
+
# returns dict of safe modules, functions, patterns for that language
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import re
|
| 22 |
+
from dataclasses import dataclass, field
|
| 23 |
+
from typing import Optional
|
| 24 |
+
|
| 25 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 26 |
+
# SUPPORTED LANGUAGES
|
| 27 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 28 |
+
|
| 29 |
+
LANGUAGES = ["python", "csharp", "cpp", "javascript", "typescript", "java", "go", "rust"]
|
| 30 |
+
|
| 31 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 32 |
+
# UNIVERSAL BLOCKLIST β dangerous patterns across ALL languages
|
| 33 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 34 |
+
|
| 35 |
+
UNIVERSAL_BLOCKS = {
|
| 36 |
+
"shell_injection": [
|
| 37 |
+
r'\bos\.system\b',
|
| 38 |
+
r'\bos\.popen\b',
|
| 39 |
+
r'\bsubprocess\.call\b(?!\(.*shell\s*=\s*False)',
|
| 40 |
+
r'Runtime\.getRuntime\(\)\.exec\b',
|
| 41 |
+
r'\bProcess\.Start\b',
|
| 42 |
+
r'\bsystem\s*\(',
|
| 43 |
+
r'\bexecvp?\b\s*\(',
|
| 44 |
+
r'\bpopen\s*\(',
|
| 45 |
+
r'child_process\.exec\b(?!File)',
|
| 46 |
+
r'\bexec\.Command\b', # Go β flag for review, not always bad
|
| 47 |
+
],
|
| 48 |
+
"code_injection": [
|
| 49 |
+
r'\beval\s*\(',
|
| 50 |
+
r'\bexec\s*\(',
|
| 51 |
+
r'__import__\s*\(',
|
| 52 |
+
r'\bFunction\s*\(', # JS new Function()
|
| 53 |
+
r'setTimeout\s*\(\s*["\']', # JS string-based setTimeout
|
| 54 |
+
r'setInterval\s*\(\s*["\']',
|
| 55 |
+
r'\bCompile\s*\(.*\)\.Run', # C# dynamic compilation
|
| 56 |
+
],
|
| 57 |
+
"file_destruction": [
|
| 58 |
+
r'shutil\.rmtree\s*\(',
|
| 59 |
+
r'\.unlink\s*\(',
|
| 60 |
+
r'\brm\s+-rf\b',
|
| 61 |
+
r'Directory\.Delete\s*\(.*true',
|
| 62 |
+
r'removeSync\s*\(',
|
| 63 |
+
r'os\.RemoveAll\s*\(',
|
| 64 |
+
r'std::filesystem::remove_all\b',
|
| 65 |
+
],
|
| 66 |
+
"sql_injection": [
|
| 67 |
+
r'f["\'].*SELECT.*FROM.*\{', # Python f-string SQL
|
| 68 |
+
r'f["\'].*INSERT.*INTO.*\{',
|
| 69 |
+
r'f["\'].*UPDATE.*SET.*\{',
|
| 70 |
+
r'f["\'].*DELETE.*FROM.*\{',
|
| 71 |
+
r'\+\s*["\'].*SELECT', # String concat SQL
|
| 72 |
+
r'format!\s*\(.*SELECT', # Rust format! SQL
|
| 73 |
+
r'fmt\.Sprintf\s*\(.*SELECT', # Go sprintf SQL
|
| 74 |
+
r'String\.Format\s*\(.*SELECT', # C# string.Format SQL
|
| 75 |
+
],
|
| 76 |
+
"xss_risk": [
|
| 77 |
+
r'innerHTML\s*=',
|
| 78 |
+
r'document\.write\s*\(',
|
| 79 |
+
r'\.outerHTML\s*=',
|
| 80 |
+
r'dangerouslySetInnerHTML',
|
| 81 |
+
],
|
| 82 |
+
"crypto_misuse": [
|
| 83 |
+
r'\bMD5\b',
|
| 84 |
+
r'\bSHA1\b(?!_)',
|
| 85 |
+
r'\bDES\b',
|
| 86 |
+
r'\bRC4\b',
|
| 87 |
+
r'random\.random\(\).*(?:password|key|token|secret)',
|
| 88 |
+
r'Math\.random\(\).*(?:password|key|token|secret)',
|
| 89 |
+
],
|
| 90 |
+
"hardcoded_secrets": [
|
| 91 |
+
r'(?:password|secret|api_key|token)\s*=\s*["\'][^"\']{8,}["\']',
|
| 92 |
+
],
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 96 |
+
# PER-LANGUAGE BLOCKLISTS
|
| 97 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 98 |
+
|
| 99 |
+
LANGUAGE_BLOCKS = {
|
| 100 |
+
"python": [
|
| 101 |
+
r'\bpickle\.loads?\b', # Arbitrary code execution via deserialization
|
| 102 |
+
r'\byaml\.load\b(?!\(.*Loader)', # Unsafe YAML load
|
| 103 |
+
r'\bmarshal\.loads?\b',
|
| 104 |
+
r'__builtins__',
|
| 105 |
+
r'\bglobals\s*\(\)',
|
| 106 |
+
r'\bcompile\s*\(',
|
| 107 |
+
],
|
| 108 |
+
"csharp": [
|
| 109 |
+
r'SqlCommand\s*\(.*\+', # String concat SQL
|
| 110 |
+
r'BinaryFormatter', # Insecure deserialization
|
| 111 |
+
r'Process\.Start\s*\(',
|
| 112 |
+
r'\bunsafe\b', # unsafe code blocks
|
| 113 |
+
r'DllImport', # P/Invoke β flag for review
|
| 114 |
+
r'Assembly\.Load\b',
|
| 115 |
+
],
|
| 116 |
+
"cpp": [
|
| 117 |
+
r'\bgets\s*\(', # Buffer overflow
|
| 118 |
+
r'\bstrcpy\s*\(', # No bounds checking
|
| 119 |
+
r'\bstrcat\s*\(', # No bounds checking
|
| 120 |
+
r'\bsprintf\s*\(', # Use snprintf instead
|
| 121 |
+
r'\bmalloc\b.*\bfree\b', # Flag raw malloc (prefer smart ptrs)
|
| 122 |
+
r'#pragma\s+warning\s*\(\s*disable',
|
| 123 |
+
r'\breinterpret_cast\b',
|
| 124 |
+
r'\bvoid\s*\*', # Void pointers β flag for review
|
| 125 |
+
],
|
| 126 |
+
"javascript": [
|
| 127 |
+
r'require\s*\(\s*["\']child_process',
|
| 128 |
+
r'\.call\s*\(.*arguments\b',
|
| 129 |
+
r'with\s*\(', # with statement
|
| 130 |
+
r'document\.cookie\b',
|
| 131 |
+
r'window\.location\s*=',
|
| 132 |
+
],
|
| 133 |
+
"typescript": [
|
| 134 |
+
r'as\s+any\b', # Type escape hatch β flag
|
| 135 |
+
r'@ts-ignore',
|
| 136 |
+
r'require\s*\(\s*["\']child_process',
|
| 137 |
+
r'document\.cookie\b',
|
| 138 |
+
],
|
| 139 |
+
"java": [
|
| 140 |
+
r'ObjectInputStream\b', # Deserialization risk
|
| 141 |
+
r'Runtime\.exec\b',
|
| 142 |
+
r'ProcessBuilder\b',
|
| 143 |
+
r'Class\.forName\s*\(', # Reflection β flag
|
| 144 |
+
r'\.getMethod\s*\(',
|
| 145 |
+
r'Statement\b.*execute\b', # Raw SQL statements
|
| 146 |
+
],
|
| 147 |
+
"go": [
|
| 148 |
+
r'os/exec\b', # Flag for review
|
| 149 |
+
r'unsafe\.Pointer\b',
|
| 150 |
+
r'reflect\.(?:Value|Type)', # Heavy reflection β flag
|
| 151 |
+
r'cgo\b', # CGo boundary
|
| 152 |
+
],
|
| 153 |
+
"rust": [
|
| 154 |
+
r'\bunsafe\s*\{',
|
| 155 |
+
r'std::mem::transmute\b',
|
| 156 |
+
r'from_raw_parts\b',
|
| 157 |
+
r'\b\.unwrap\(\)', # Panics in production β warn
|
| 158 |
+
],
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 162 |
+
# PER-LANGUAGE WHITELISTS β safe standard library modules, functions, patterns
|
| 163 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 164 |
+
|
| 165 |
+
SAFE_PATTERNS = {
|
| 166 |
+
"python": {
|
| 167 |
+
"safe_modules": [
|
| 168 |
+
"collections", "itertools", "functools", "operator",
|
| 169 |
+
"math", "statistics", "decimal", "fractions",
|
| 170 |
+
"datetime", "calendar", "time",
|
| 171 |
+
"json", "csv", "io", "os.path", "pathlib",
|
| 172 |
+
"re", "string", "textwrap", "unicodedata",
|
| 173 |
+
"typing", "dataclasses", "enum", "abc",
|
| 174 |
+
"hashlib", "hmac", "secrets",
|
| 175 |
+
"logging", "unittest", "pytest",
|
| 176 |
+
"copy", "pprint", "bisect", "heapq",
|
| 177 |
+
"contextlib", "weakref",
|
| 178 |
+
"argparse", "configparser",
|
| 179 |
+
"sqlite3", # with parameterized queries
|
| 180 |
+
"urllib.parse",
|
| 181 |
+
"base64", "binascii",
|
| 182 |
+
"struct", "array",
|
| 183 |
+
"queue", "threading.Lock", "threading.Event",
|
| 184 |
+
"asyncio", "concurrent.futures",
|
| 185 |
+
],
|
| 186 |
+
"safe_builtins": [
|
| 187 |
+
"len", "range", "enumerate", "zip", "map", "filter",
|
| 188 |
+
"sorted", "reversed", "min", "max", "sum", "abs",
|
| 189 |
+
"all", "any", "isinstance", "issubclass", "type",
|
| 190 |
+
"int", "float", "str", "bool", "list", "dict", "set", "tuple",
|
| 191 |
+
"frozenset", "bytes", "bytearray", "memoryview",
|
| 192 |
+
"print", "input", "repr", "format", "hash", "id",
|
| 193 |
+
"getattr", "setattr", "hasattr", "delattr",
|
| 194 |
+
"property", "staticmethod", "classmethod",
|
| 195 |
+
"super", "iter", "next", "chr", "ord",
|
| 196 |
+
"round", "divmod", "pow",
|
| 197 |
+
"open", # reading only β write mode blocked separately
|
| 198 |
+
],
|
| 199 |
+
"safe_patterns": [
|
| 200 |
+
"list comprehension",
|
| 201 |
+
"dict comprehension",
|
| 202 |
+
"generator expression",
|
| 203 |
+
"context manager (with statement)",
|
| 204 |
+
"dataclass",
|
| 205 |
+
"type hints / annotations",
|
| 206 |
+
"f-string formatting (non-SQL)",
|
| 207 |
+
"try/except/finally",
|
| 208 |
+
"decorator pattern",
|
| 209 |
+
"property getter/setter",
|
| 210 |
+
"@functools.lru_cache",
|
| 211 |
+
"@functools.wraps",
|
| 212 |
+
"collections.defaultdict",
|
| 213 |
+
"collections.Counter",
|
| 214 |
+
"collections.namedtuple",
|
| 215 |
+
"pathlib.Path operations",
|
| 216 |
+
"logging.getLogger",
|
| 217 |
+
"argparse.ArgumentParser",
|
| 218 |
+
"parameterized SQL queries (?, %s placeholders)",
|
| 219 |
+
],
|
| 220 |
+
"safe_frameworks": [
|
| 221 |
+
"flask", "fastapi", "django",
|
| 222 |
+
"requests", "httpx",
|
| 223 |
+
"sqlalchemy", # with ORM / parameterized
|
| 224 |
+
"pydantic",
|
| 225 |
+
"pytest", "unittest",
|
| 226 |
+
"numpy", "pandas",
|
| 227 |
+
"click", "typer",
|
| 228 |
+
],
|
| 229 |
+
},
|
| 230 |
+
|
| 231 |
+
"csharp": {
|
| 232 |
+
"safe_modules": [
|
| 233 |
+
"System", "System.Collections.Generic", "System.Linq",
|
| 234 |
+
"System.Text", "System.Text.RegularExpressions",
|
| 235 |
+
"System.IO", "System.IO.Path",
|
| 236 |
+
"System.Threading", "System.Threading.Tasks",
|
| 237 |
+
"System.Net.Http", "System.Net.Http.Json",
|
| 238 |
+
"System.Text.Json", "System.Text.Json.Serialization",
|
| 239 |
+
"System.Security.Cryptography",
|
| 240 |
+
"System.Diagnostics.Debug",
|
| 241 |
+
"System.ComponentModel.DataAnnotations",
|
| 242 |
+
"System.Globalization",
|
| 243 |
+
"System.Math",
|
| 244 |
+
"Microsoft.Extensions.Logging",
|
| 245 |
+
"Microsoft.Extensions.DependencyInjection",
|
| 246 |
+
"Microsoft.Extensions.Configuration",
|
| 247 |
+
"Microsoft.EntityFrameworkCore",
|
| 248 |
+
],
|
| 249 |
+
"safe_patterns": [
|
| 250 |
+
"using statement / IDisposable",
|
| 251 |
+
"async/await with Task",
|
| 252 |
+
"LINQ queries",
|
| 253 |
+
"record types",
|
| 254 |
+
"pattern matching (switch expressions)",
|
| 255 |
+
"nullable reference types",
|
| 256 |
+
"string interpolation (non-SQL)",
|
| 257 |
+
"dependency injection",
|
| 258 |
+
"ILogger<T>",
|
| 259 |
+
"parameterized SQL (@param)",
|
| 260 |
+
"Entity Framework LINQ queries",
|
| 261 |
+
"try/catch/finally",
|
| 262 |
+
"readonly / init-only properties",
|
| 263 |
+
"sealed classes",
|
| 264 |
+
"interfaces and abstract classes",
|
| 265 |
+
"generic constraints",
|
| 266 |
+
"ValueTask for hot paths",
|
| 267 |
+
"Span<T> / Memory<T>",
|
| 268 |
+
"IAsyncEnumerable<T>",
|
| 269 |
+
"CancellationToken",
|
| 270 |
+
],
|
| 271 |
+
"safe_frameworks": [
|
| 272 |
+
"ASP.NET Core", "Entity Framework Core",
|
| 273 |
+
"xUnit", "NUnit", "MSTest",
|
| 274 |
+
"Serilog", "MediatR",
|
| 275 |
+
"FluentValidation",
|
| 276 |
+
"AutoMapper",
|
| 277 |
+
"Polly (resilience)",
|
| 278 |
+
],
|
| 279 |
+
},
|
| 280 |
+
|
| 281 |
+
"cpp": {
|
| 282 |
+
"safe_modules": [
|
| 283 |
+
"<algorithm>", "<numeric>", "<functional>",
|
| 284 |
+
"<vector>", "<array>", "<string>", "<string_view>",
|
| 285 |
+
"<map>", "<unordered_map>", "<set>", "<unordered_set>",
|
| 286 |
+
"<queue>", "<stack>", "<deque>", "<list>",
|
| 287 |
+
"<memory>", # smart pointers
|
| 288 |
+
"<optional>", "<variant>", "<any>", "<tuple>",
|
| 289 |
+
"<chrono>", "<cmath>", "<cstdint>",
|
| 290 |
+
"<iostream>", "<sstream>", "<fstream>",
|
| 291 |
+
"<regex>", "<filesystem>",
|
| 292 |
+
"<thread>", "<mutex>", "<condition_variable>",
|
| 293 |
+
"<atomic>", "<future>",
|
| 294 |
+
"<stdexcept>", "<cassert>",
|
| 295 |
+
"<ranges>", "<concepts>",
|
| 296 |
+
"<format>", # C++20
|
| 297 |
+
"<span>", # C++20
|
| 298 |
+
],
|
| 299 |
+
"safe_patterns": [
|
| 300 |
+
"std::unique_ptr / std::shared_ptr",
|
| 301 |
+
"std::make_unique / std::make_shared",
|
| 302 |
+
"RAII (constructor/destructor pairs)",
|
| 303 |
+
"range-based for loops",
|
| 304 |
+
"auto type deduction",
|
| 305 |
+
"const references (const T&)",
|
| 306 |
+
"constexpr",
|
| 307 |
+
"structured bindings (auto [a, b] = ...)",
|
| 308 |
+
"std::optional for nullable values",
|
| 309 |
+
"std::string_view for non-owning strings",
|
| 310 |
+
"std::move semantics",
|
| 311 |
+
"static_cast (not reinterpret_cast)",
|
| 312 |
+
"try/catch with std::exception",
|
| 313 |
+
"std::algorithm (sort, find, transform)",
|
| 314 |
+
"lambda expressions",
|
| 315 |
+
"templates and concepts (C++20)",
|
| 316 |
+
"enum class (scoped enums)",
|
| 317 |
+
"snprintf instead of sprintf",
|
| 318 |
+
"std::array instead of C arrays",
|
| 319 |
+
"std::vector instead of raw new[]",
|
| 320 |
+
],
|
| 321 |
+
"safe_libraries": [
|
| 322 |
+
"Boost (selected modules)",
|
| 323 |
+
"fmt", "spdlog",
|
| 324 |
+
"Catch2", "Google Test",
|
| 325 |
+
"nlohmann/json",
|
| 326 |
+
"abseil-cpp",
|
| 327 |
+
],
|
| 328 |
+
},
|
| 329 |
+
|
| 330 |
+
"javascript": {
|
| 331 |
+
"safe_modules": [
|
| 332 |
+
"Array", "Object", "Map", "Set", "WeakMap", "WeakSet",
|
| 333 |
+
"Promise", "JSON", "Math", "Date", "RegExp",
|
| 334 |
+
"String", "Number", "Symbol", "BigInt",
|
| 335 |
+
"URL", "URLSearchParams",
|
| 336 |
+
"TextEncoder", "TextDecoder",
|
| 337 |
+
"structuredClone",
|
| 338 |
+
"console",
|
| 339 |
+
"fetch",
|
| 340 |
+
"AbortController",
|
| 341 |
+
"crypto.subtle", # Web Crypto API
|
| 342 |
+
"crypto.randomUUID",
|
| 343 |
+
],
|
| 344 |
+
"safe_node_modules": [
|
| 345 |
+
"path", "url", "util", "events",
|
| 346 |
+
"crypto", # Node crypto
|
| 347 |
+
"fs/promises", # async file ops
|
| 348 |
+
"stream", "buffer",
|
| 349 |
+
"assert", "test", # Node test runner
|
| 350 |
+
"zlib", "querystring",
|
| 351 |
+
],
|
| 352 |
+
"safe_patterns": [
|
| 353 |
+
"const / let (no var)",
|
| 354 |
+
"arrow functions",
|
| 355 |
+
"template literals (non-SQL)",
|
| 356 |
+
"destructuring (object / array)",
|
| 357 |
+
"spread / rest operators",
|
| 358 |
+
"Promise.all / Promise.allSettled",
|
| 359 |
+
"async/await",
|
| 360 |
+
"optional chaining (?.) ",
|
| 361 |
+
"nullish coalescing (??)",
|
| 362 |
+
"Array methods (map, filter, reduce, find, some, every)",
|
| 363 |
+
"Object.entries / Object.keys / Object.values",
|
| 364 |
+
"try/catch/finally",
|
| 365 |
+
"class syntax",
|
| 366 |
+
"modules (import/export)",
|
| 367 |
+
"for...of loops",
|
| 368 |
+
"Map/Set for collections",
|
| 369 |
+
"structuredClone for deep copy",
|
| 370 |
+
"fetch with AbortController",
|
| 371 |
+
"parameterized queries (prepared statements)",
|
| 372 |
+
],
|
| 373 |
+
"safe_frameworks": [
|
| 374 |
+
"React", "Vue", "Svelte",
|
| 375 |
+
"Express (with helmet, cors)",
|
| 376 |
+
"Fastify",
|
| 377 |
+
"Jest", "Vitest", "Mocha",
|
| 378 |
+
"Zod (validation)",
|
| 379 |
+
"Prisma (ORM)",
|
| 380 |
+
"TypeORM", "Knex",
|
| 381 |
+
],
|
| 382 |
+
},
|
| 383 |
+
|
| 384 |
+
"typescript": {
|
| 385 |
+
"inherits": "javascript",
|
| 386 |
+
"safe_patterns": [
|
| 387 |
+
"strict mode (strict: true in tsconfig)",
|
| 388 |
+
"interface definitions",
|
| 389 |
+
"type aliases",
|
| 390 |
+
"generic types",
|
| 391 |
+
"discriminated unions",
|
| 392 |
+
"type guards (is / in / typeof / instanceof)",
|
| 393 |
+
"readonly modifier",
|
| 394 |
+
"Record<K, V> / Partial<T> / Required<T>",
|
| 395 |
+
"Pick<T, K> / Omit<T, K>",
|
| 396 |
+
"unknown over any",
|
| 397 |
+
"satisfies operator",
|
| 398 |
+
"const assertions (as const)",
|
| 399 |
+
"enum (prefer const enum or union types)",
|
| 400 |
+
"Zod / io-ts for runtime validation",
|
| 401 |
+
],
|
| 402 |
+
},
|
| 403 |
+
|
| 404 |
+
"java": {
|
| 405 |
+
"safe_modules": [
|
| 406 |
+
"java.util.*",
|
| 407 |
+
"java.util.stream.*",
|
| 408 |
+
"java.util.concurrent.*",
|
| 409 |
+
"java.util.function.*",
|
| 410 |
+
"java.time.*",
|
| 411 |
+
"java.math.*",
|
| 412 |
+
"java.lang.Math",
|
| 413 |
+
"java.lang.String",
|
| 414 |
+
"java.lang.StringBuilder",
|
| 415 |
+
"java.io.BufferedReader", "java.io.BufferedWriter",
|
| 416 |
+
"java.nio.file.Path", "java.nio.file.Files",
|
| 417 |
+
"java.security.MessageDigest",
|
| 418 |
+
"java.security.SecureRandom",
|
| 419 |
+
"java.text.MessageFormat",
|
| 420 |
+
"java.util.logging.*",
|
| 421 |
+
"java.util.regex.*",
|
| 422 |
+
"java.net.URI", "java.net.http.HttpClient",
|
| 423 |
+
],
|
| 424 |
+
"safe_patterns": [
|
| 425 |
+
"try-with-resources",
|
| 426 |
+
"Optional<T>",
|
| 427 |
+
"Stream API (map, filter, collect, reduce)",
|
| 428 |
+
"records (Java 14+)",
|
| 429 |
+
"sealed classes (Java 17+)",
|
| 430 |
+
"pattern matching for instanceof (Java 16+)",
|
| 431 |
+
"switch expressions (Java 14+)",
|
| 432 |
+
"text blocks (Java 15+)",
|
| 433 |
+
"var (local variable type inference)",
|
| 434 |
+
"CompletableFuture",
|
| 435 |
+
"PreparedStatement (parameterized SQL)",
|
| 436 |
+
"Collections.unmodifiableList / List.of / Map.of",
|
| 437 |
+
"interface default methods",
|
| 438 |
+
"lambda expressions",
|
| 439 |
+
"method references (::)",
|
| 440 |
+
"enum with methods",
|
| 441 |
+
"builder pattern",
|
| 442 |
+
"dependency injection (@Inject)",
|
| 443 |
+
],
|
| 444 |
+
"safe_frameworks": [
|
| 445 |
+
"Spring Boot", "Spring Security",
|
| 446 |
+
"JUnit 5", "Mockito", "AssertJ",
|
| 447 |
+
"Jackson", "Gson",
|
| 448 |
+
"SLF4J / Logback",
|
| 449 |
+
"Hibernate / JPA",
|
| 450 |
+
"Lombok",
|
| 451 |
+
"MapStruct",
|
| 452 |
+
],
|
| 453 |
+
},
|
| 454 |
+
|
| 455 |
+
"go": {
|
| 456 |
+
"safe_modules": [
|
| 457 |
+
"fmt", "strings", "strconv", "unicode",
|
| 458 |
+
"math", "math/big", "math/rand",
|
| 459 |
+
"sort", "slices", # Go 1.21+
|
| 460 |
+
"maps", # Go 1.21+
|
| 461 |
+
"errors", "log", "log/slog",
|
| 462 |
+
"io", "bufio", "bytes",
|
| 463 |
+
"os", "path", "path/filepath",
|
| 464 |
+
"encoding/json", "encoding/csv", "encoding/base64",
|
| 465 |
+
"net/http", "net/url",
|
| 466 |
+
"context",
|
| 467 |
+
"sync", "sync/atomic",
|
| 468 |
+
"time",
|
| 469 |
+
"regexp",
|
| 470 |
+
"crypto/sha256", "crypto/hmac", "crypto/rand",
|
| 471 |
+
"testing",
|
| 472 |
+
"embed",
|
| 473 |
+
"database/sql", # with parameterized queries
|
| 474 |
+
],
|
| 475 |
+
"safe_patterns": [
|
| 476 |
+
"error handling (if err != nil)",
|
| 477 |
+
"defer for cleanup",
|
| 478 |
+
"goroutines with sync.WaitGroup",
|
| 479 |
+
"channels for communication",
|
| 480 |
+
"select statement",
|
| 481 |
+
"context.Context for cancellation",
|
| 482 |
+
"interfaces (implicit satisfaction)",
|
| 483 |
+
"struct embedding (composition)",
|
| 484 |
+
"table-driven tests",
|
| 485 |
+
"functional options pattern",
|
| 486 |
+
"type assertions with ok check",
|
| 487 |
+
"range loops",
|
| 488 |
+
"iota for enums",
|
| 489 |
+
"init() functions",
|
| 490 |
+
"database/sql with $1 or ? placeholders",
|
| 491 |
+
"http.HandlerFunc / middleware chain",
|
| 492 |
+
"slog structured logging",
|
| 493 |
+
"generics (Go 1.18+)",
|
| 494 |
+
],
|
| 495 |
+
"safe_frameworks": [
|
| 496 |
+
"Gin", "Chi", "Echo",
|
| 497 |
+
"GORM", "sqlx",
|
| 498 |
+
"testify",
|
| 499 |
+
"Wire (DI)",
|
| 500 |
+
"Cobra (CLI)",
|
| 501 |
+
"Viper (config)",
|
| 502 |
+
],
|
| 503 |
+
},
|
| 504 |
+
|
| 505 |
+
"rust": {
|
| 506 |
+
"safe_modules": [
|
| 507 |
+
"std::collections (HashMap, BTreeMap, Vec, VecDeque, HashSet)",
|
| 508 |
+
"std::string::String",
|
| 509 |
+
"std::vec::Vec",
|
| 510 |
+
"std::io (Read, Write, BufReader, BufWriter)",
|
| 511 |
+
"std::fs",
|
| 512 |
+
"std::path (Path, PathBuf)",
|
| 513 |
+
"std::fmt",
|
| 514 |
+
"std::iter",
|
| 515 |
+
"std::convert (From, Into, TryFrom, TryInto)",
|
| 516 |
+
"std::ops",
|
| 517 |
+
"std::cmp (Ordering, min, max)",
|
| 518 |
+
"std::time (Duration, Instant)",
|
| 519 |
+
"std::thread",
|
| 520 |
+
"std::sync (Arc, Mutex, RwLock, mpsc)",
|
| 521 |
+
"std::error::Error",
|
| 522 |
+
"std::result::Result",
|
| 523 |
+
"std::option::Option",
|
| 524 |
+
"std::num",
|
| 525 |
+
],
|
| 526 |
+
"safe_patterns": [
|
| 527 |
+
"ownership and borrowing",
|
| 528 |
+
"pattern matching (match)",
|
| 529 |
+
"Result<T, E> for error handling",
|
| 530 |
+
"Option<T> for nullable values",
|
| 531 |
+
"? operator for error propagation",
|
| 532 |
+
"impl blocks and traits",
|
| 533 |
+
"derive macros (#[derive(Debug, Clone, PartialEq)])",
|
| 534 |
+
"iterators (.map, .filter, .collect, .fold)",
|
| 535 |
+
"closures (|x| ...)",
|
| 536 |
+
"enum with data variants",
|
| 537 |
+
"struct with impl",
|
| 538 |
+
"lifetime annotations where needed",
|
| 539 |
+
"Arc<Mutex<T>> for shared state",
|
| 540 |
+
"async/await with tokio",
|
| 541 |
+
"type aliases",
|
| 542 |
+
"const generics",
|
| 543 |
+
"builder pattern",
|
| 544 |
+
"#[cfg(test)] mod tests",
|
| 545 |
+
],
|
| 546 |
+
"safe_crates": [
|
| 547 |
+
"serde / serde_json",
|
| 548 |
+
"tokio / async-std",
|
| 549 |
+
"reqwest",
|
| 550 |
+
"clap (CLI)",
|
| 551 |
+
"anyhow / thiserror",
|
| 552 |
+
"tracing / log",
|
| 553 |
+
"sqlx",
|
| 554 |
+
"axum / actix-web / warp",
|
| 555 |
+
"rand",
|
| 556 |
+
],
|
| 557 |
+
},
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 562 |
+
# VALIDATION ENGINE
|
| 563 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 564 |
+
|
| 565 |
+
@dataclass
|
| 566 |
+
class ValidationResult:
|
| 567 |
+
safe: bool
|
| 568 |
+
language: str
|
| 569 |
+
blocked: list[str] = field(default_factory=list)
|
| 570 |
+
warnings: list[str] = field(default_factory=list)
|
| 571 |
+
score: float = 100.0 # 100 = perfectly safe, 0 = dangerous
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
def _check_blocks(code: str, patterns: list, category: str,
|
| 575 |
+
severity: str = "block") -> tuple[list[str], list[str]]:
|
| 576 |
+
"""Check code against a list of regex patterns. Returns (blocks, warnings)."""
|
| 577 |
+
blocks = []
|
| 578 |
+
warnings = []
|
| 579 |
+
for pattern in patterns:
|
| 580 |
+
if re.search(pattern, code, re.IGNORECASE):
|
| 581 |
+
msg = f"[{category}] matched: {pattern}"
|
| 582 |
+
if severity == "block":
|
| 583 |
+
blocks.append(msg)
|
| 584 |
+
else:
|
| 585 |
+
warnings.append(msg)
|
| 586 |
+
return blocks, warnings
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
def validate_code(code: str, language: str = "python") -> ValidationResult:
|
| 590 |
+
"""
|
| 591 |
+
Validate code against blocklist and whitelist.
|
| 592 |
+
Returns a ValidationResult with safety assessment.
|
| 593 |
+
"""
|
| 594 |
+
language = language.lower().strip()
|
| 595 |
+
if language in ("ts", "tsx"):
|
| 596 |
+
language = "typescript"
|
| 597 |
+
elif language in ("js", "jsx"):
|
| 598 |
+
language = "javascript"
|
| 599 |
+
elif language in ("cs", "c#"):
|
| 600 |
+
language = "csharp"
|
| 601 |
+
elif language in ("c++", "cc", "cxx", "hpp"):
|
| 602 |
+
language = "cpp"
|
| 603 |
+
elif language in ("rs",):
|
| 604 |
+
language = "rust"
|
| 605 |
+
elif language in ("py",):
|
| 606 |
+
language = "python"
|
| 607 |
+
|
| 608 |
+
result = ValidationResult(safe=True, language=language)
|
| 609 |
+
|
| 610 |
+
# 1. Check universal blocklist
|
| 611 |
+
for category, patterns in UNIVERSAL_BLOCKS.items():
|
| 612 |
+
blocks, warnings = _check_blocks(code, patterns, category)
|
| 613 |
+
result.blocked.extend(blocks)
|
| 614 |
+
result.warnings.extend(warnings)
|
| 615 |
+
|
| 616 |
+
# 2. Check language-specific blocklist
|
| 617 |
+
lang_blocks = LANGUAGE_BLOCKS.get(language, [])
|
| 618 |
+
blocks, warnings = _check_blocks(code, lang_blocks, f"{language}_specific")
|
| 619 |
+
result.blocked.extend(blocks)
|
| 620 |
+
result.warnings.extend(warnings)
|
| 621 |
+
|
| 622 |
+
# 3. For TypeScript, also check JavaScript blocks
|
| 623 |
+
if language == "typescript":
|
| 624 |
+
js_blocks = LANGUAGE_BLOCKS.get("javascript", [])
|
| 625 |
+
blocks, warnings = _check_blocks(code, js_blocks, "javascript_specific")
|
| 626 |
+
result.blocked.extend(blocks)
|
| 627 |
+
result.warnings.extend(warnings)
|
| 628 |
+
|
| 629 |
+
# 4. Calculate safety score
|
| 630 |
+
result.score = max(0, 100 - (len(result.blocked) * 25) - (len(result.warnings) * 5))
|
| 631 |
+
result.safe = len(result.blocked) == 0
|
| 632 |
+
|
| 633 |
+
return result
|
| 634 |
+
|
| 635 |
+
|
| 636 |
+
def get_safe_patterns(language: str) -> dict:
|
| 637 |
+
"""Get the whitelist of safe patterns for a specific language."""
|
| 638 |
+
language = language.lower().strip()
|
| 639 |
+
alias_map = {
|
| 640 |
+
"ts": "typescript", "tsx": "typescript",
|
| 641 |
+
"js": "javascript", "jsx": "javascript",
|
| 642 |
+
"cs": "csharp", "c#": "csharp",
|
| 643 |
+
"c++": "cpp", "cc": "cpp", "cxx": "cpp",
|
| 644 |
+
"rs": "rust", "py": "python",
|
| 645 |
+
}
|
| 646 |
+
language = alias_map.get(language, language)
|
| 647 |
+
|
| 648 |
+
patterns = SAFE_PATTERNS.get(language, {})
|
| 649 |
+
|
| 650 |
+
# TypeScript inherits JavaScript patterns
|
| 651 |
+
if language == "typescript" and patterns.get("inherits") == "javascript":
|
| 652 |
+
js_patterns = SAFE_PATTERNS.get("javascript", {})
|
| 653 |
+
merged = {**js_patterns, **patterns}
|
| 654 |
+
del merged["inherits"]
|
| 655 |
+
return merged
|
| 656 |
+
|
| 657 |
+
return patterns
|
| 658 |
+
|
| 659 |
+
|
| 660 |
+
def get_blocklist(language: str) -> dict:
|
| 661 |
+
"""Get all blocked patterns (universal + language-specific) for a language."""
|
| 662 |
+
language = language.lower().strip()
|
| 663 |
+
alias_map = {
|
| 664 |
+
"ts": "typescript", "tsx": "typescript",
|
| 665 |
+
"js": "javascript", "jsx": "javascript",
|
| 666 |
+
"cs": "csharp", "c#": "csharp",
|
| 667 |
+
"c++": "cpp", "cc": "cpp", "cxx": "cpp",
|
| 668 |
+
"rs": "rust", "py": "python",
|
| 669 |
+
}
|
| 670 |
+
language = alias_map.get(language, language)
|
| 671 |
+
|
| 672 |
+
return {
|
| 673 |
+
"universal": UNIVERSAL_BLOCKS,
|
| 674 |
+
"language_specific": LANGUAGE_BLOCKS.get(language, []),
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
|
| 678 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 679 |
+
# CLI β quick testing
|
| 680 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 681 |
+
|
| 682 |
+
if __name__ == "__main__":
|
| 683 |
+
import sys
|
| 684 |
+
|
| 685 |
+
# Demo: validate some sample code
|
| 686 |
+
test_samples = {
|
| 687 |
+
"python_safe": (
|
| 688 |
+
"python",
|
| 689 |
+
'from collections import Counter\ndata = Counter([1,2,3,1,2,1])\nprint(data.most_common())'
|
| 690 |
+
),
|
| 691 |
+
"python_dangerous": (
|
| 692 |
+
"python",
|
| 693 |
+
'import os\nos.system("rm -rf /")\neval(input())'
|
| 694 |
+
),
|
| 695 |
+
"csharp_safe": (
|
| 696 |
+
"csharp",
|
| 697 |
+
'using System.Linq;\nvar nums = new[] {1,2,3};\nvar sum = nums.Sum();'
|
| 698 |
+
),
|
| 699 |
+
"csharp_dangerous": (
|
| 700 |
+
"csharp",
|
| 701 |
+
'var cmd = new SqlCommand("SELECT * FROM users WHERE id=" + userId);'
|
| 702 |
+
),
|
| 703 |
+
"cpp_safe": (
|
| 704 |
+
"cpp",
|
| 705 |
+
'#include <vector>\n#include <algorithm>\nauto v = std::vector{3,1,2};\nstd::sort(v.begin(), v.end());'
|
| 706 |
+
),
|
| 707 |
+
"cpp_dangerous": (
|
| 708 |
+
"cpp",
|
| 709 |
+
'char buf[10];\ngets(buf);\nstrcpy(dest, src);'
|
| 710 |
+
),
|
| 711 |
+
"js_safe": (
|
| 712 |
+
"javascript",
|
| 713 |
+
'const items = [1,2,3];\nconst doubled = items.map(x => x * 2);\nconsole.log(doubled);'
|
| 714 |
+
),
|
| 715 |
+
"js_dangerous": (
|
| 716 |
+
"javascript",
|
| 717 |
+
'const code = req.body.code;\neval(code);\ndocument.innerHTML = userInput;'
|
| 718 |
+
),
|
| 719 |
+
"sql_injection": (
|
| 720 |
+
"python",
|
| 721 |
+
'query = f"SELECT * FROM users WHERE name = \'{user_input}\'"'
|
| 722 |
+
),
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
print("=" * 70)
|
| 726 |
+
print("MULTI-LANGUAGE CODE WHITELIST β Validation Demo")
|
| 727 |
+
print("=" * 70)
|
| 728 |
+
|
| 729 |
+
for name, (lang, code) in test_samples.items():
|
| 730 |
+
result = validate_code(code, lang)
|
| 731 |
+
status = "SAFE" if result.safe else "BLOCKED"
|
| 732 |
+
print(f"\n[{status}] {name} ({lang}) β score: {result.score}")
|
| 733 |
+
if result.blocked:
|
| 734 |
+
for b in result.blocked:
|
| 735 |
+
print(f" BLOCK: {b}")
|
| 736 |
+
if result.warnings:
|
| 737 |
+
for w in result.warnings:
|
| 738 |
+
print(f" WARN: {w}")
|
| 739 |
+
|
| 740 |
+
print(f"\n{'=' * 70}")
|
| 741 |
+
print(f"Supported languages: {', '.join(LANGUAGES)}")
|
| 742 |
+
print(f"Universal block categories: {len(UNIVERSAL_BLOCKS)}")
|
| 743 |
+
total_blocks = sum(len(v) for v in LANGUAGE_BLOCKS.values())
|
| 744 |
+
print(f"Language-specific block patterns: {total_blocks}")
|
| 745 |
+
total_safe = sum(
|
| 746 |
+
sum(len(v) for v in lang.values() if isinstance(v, list))
|
| 747 |
+
for lang in SAFE_PATTERNS.values()
|
| 748 |
+
if isinstance(lang, dict)
|
| 749 |
+
)
|
| 750 |
+
print(f"Safe patterns/modules catalogued: {total_safe}")
|
growth_engine.py
ADDED
|
@@ -0,0 +1,995 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
GROWTH ENGINE
|
| 3 |
+
=============
|
| 4 |
+
The wrapper that makes the system permanently smarter with every problem.
|
| 5 |
+
|
| 6 |
+
Every task solved β pass or fail β teaches the system something.
|
| 7 |
+
Nothing is wasted. Every failure is a lesson stored forever.
|
| 8 |
+
Every success is a pattern extracted and reused.
|
| 9 |
+
|
| 10 |
+
Four learning layers:
|
| 11 |
+
1. MEMORY β persistent storage of every attempt, forever
|
| 12 |
+
2. PATTERN LIB β distilled reusable solutions from winning code
|
| 13 |
+
3. SOURCE TRUST β which references produce good code, scored over time
|
| 14 |
+
4. CALIBRATION β tracks when the AI lies to itself about confidence
|
| 15 |
+
|
| 16 |
+
This sits above reflection_engine.py.
|
| 17 |
+
The reflection engine handles one task.
|
| 18 |
+
This handles everything the system learns from every task ever run.
|
| 19 |
+
|
| 20 |
+
Usage:
|
| 21 |
+
python growth_engine.py "write a function that does X"
|
| 22 |
+
python growth_engine.py --stats (show what it has learned)
|
| 23 |
+
python growth_engine.py --patterns (show pattern library)
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
import sqlite3
|
| 27 |
+
import json
|
| 28 |
+
import re
|
| 29 |
+
import sys
|
| 30 |
+
import os
|
| 31 |
+
import time
|
| 32 |
+
import hashlib
|
| 33 |
+
import requests
|
| 34 |
+
from datetime import datetime
|
| 35 |
+
from dataclasses import dataclass, asdict
|
| 36 |
+
from typing import Optional
|
| 37 |
+
|
| 38 |
+
# Import the brain
|
| 39 |
+
sys.path.insert(0, os.path.dirname(__file__))
|
| 40 |
+
from reflection_engine import (
|
| 41 |
+
run_engine, CodeAttempt, ollama,
|
| 42 |
+
BENCHMARK_FLOOR, MODEL,
|
| 43 |
+
set_language, get_language, _LANG_CONFIG
|
| 44 |
+
)
|
| 45 |
+
from trust_balance_agent import apply_trust_before_run
|
| 46 |
+
from model_manager import _get_active_model, scan_and_register, print_status as model_status
|
| 47 |
+
|
| 48 |
+
# ββ Config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 49 |
+
|
| 50 |
+
DB_PATH = os.environ.get("GROWTH_DB_PATH", os.path.join(os.path.dirname(__file__), "growth_memory.db"))
|
| 51 |
+
MAX_CONTEXT_PATTERNS = 5 # how many past patterns to inject per task
|
| 52 |
+
MAX_CONTEXT_FAILURES = 3 # how many past failure lessons to inject
|
| 53 |
+
|
| 54 |
+
# ββ Database setup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 55 |
+
|
| 56 |
+
def get_db() -> sqlite3.Connection:
|
| 57 |
+
conn = sqlite3.connect(DB_PATH)
|
| 58 |
+
conn.row_factory = sqlite3.Row
|
| 59 |
+
return conn
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def init_db():
|
| 63 |
+
"""Create all tables if they don't exist."""
|
| 64 |
+
conn = get_db()
|
| 65 |
+
c = conn.cursor()
|
| 66 |
+
|
| 67 |
+
# Every task ever run
|
| 68 |
+
c.execute("""
|
| 69 |
+
CREATE TABLE IF NOT EXISTS task_history (
|
| 70 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 71 |
+
task_hash TEXT NOT NULL,
|
| 72 |
+
task TEXT NOT NULL,
|
| 73 |
+
timestamp TEXT NOT NULL,
|
| 74 |
+
final_score REAL,
|
| 75 |
+
passed INTEGER,
|
| 76 |
+
iterations INTEGER,
|
| 77 |
+
final_code TEXT,
|
| 78 |
+
lessons TEXT,
|
| 79 |
+
task_category TEXT
|
| 80 |
+
)
|
| 81 |
+
""")
|
| 82 |
+
|
| 83 |
+
# Distilled reusable patterns from winning code
|
| 84 |
+
c.execute("""
|
| 85 |
+
CREATE TABLE IF NOT EXISTS pattern_library (
|
| 86 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 87 |
+
pattern_hash TEXT UNIQUE NOT NULL,
|
| 88 |
+
pattern_name TEXT NOT NULL,
|
| 89 |
+
description TEXT,
|
| 90 |
+
code_template TEXT,
|
| 91 |
+
use_cases TEXT,
|
| 92 |
+
score_when_used REAL DEFAULT 0,
|
| 93 |
+
times_used INTEGER DEFAULT 0,
|
| 94 |
+
times_helped INTEGER DEFAULT 0,
|
| 95 |
+
created_at TEXT,
|
| 96 |
+
last_used TEXT
|
| 97 |
+
)
|
| 98 |
+
""")
|
| 99 |
+
|
| 100 |
+
# Known failure patterns β what NOT to do
|
| 101 |
+
c.execute("""
|
| 102 |
+
CREATE TABLE IF NOT EXISTS failure_library (
|
| 103 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 104 |
+
failure_hash TEXT UNIQUE NOT NULL,
|
| 105 |
+
description TEXT NOT NULL,
|
| 106 |
+
root_cause TEXT,
|
| 107 |
+
fix TEXT,
|
| 108 |
+
times_seen INTEGER DEFAULT 1,
|
| 109 |
+
last_seen TEXT
|
| 110 |
+
)
|
| 111 |
+
""")
|
| 112 |
+
|
| 113 |
+
# Confidence calibration log
|
| 114 |
+
c.execute("""
|
| 115 |
+
CREATE TABLE IF NOT EXISTS confidence_log (
|
| 116 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 117 |
+
timestamp TEXT,
|
| 118 |
+
stated_confidence REAL,
|
| 119 |
+
actual_score REAL,
|
| 120 |
+
passed INTEGER,
|
| 121 |
+
error REAL -- abs(stated_confidence*100 - actual_score)
|
| 122 |
+
)
|
| 123 |
+
""")
|
| 124 |
+
|
| 125 |
+
# Source trust registry
|
| 126 |
+
c.execute("""
|
| 127 |
+
CREATE TABLE IF NOT EXISTS source_trust (
|
| 128 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 129 |
+
source_ref TEXT UNIQUE NOT NULL,
|
| 130 |
+
trust_score REAL DEFAULT 50.0,
|
| 131 |
+
times_cited INTEGER DEFAULT 0,
|
| 132 |
+
times_helped INTEGER DEFAULT 0,
|
| 133 |
+
times_hurt INTEGER DEFAULT 0,
|
| 134 |
+
last_seen TEXT
|
| 135 |
+
)
|
| 136 |
+
""")
|
| 137 |
+
|
| 138 |
+
# ββ Observation Ledger ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 139 |
+
# Raw observations accumulate here. Patterns are NOT forced.
|
| 140 |
+
# Only when the same observation surfaces enough times β honestly,
|
| 141 |
+
# naturally, across different tasks β does it crystallize into a
|
| 142 |
+
# pattern. The world is chaotic but patterns exist. They appear
|
| 143 |
+
# when observed honestly and not forced.
|
| 144 |
+
c.execute("""
|
| 145 |
+
CREATE TABLE IF NOT EXISTS observation_ledger (
|
| 146 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 147 |
+
obs_hash TEXT NOT NULL,
|
| 148 |
+
observation TEXT NOT NULL,
|
| 149 |
+
context TEXT,
|
| 150 |
+
domain TEXT,
|
| 151 |
+
times_seen INTEGER DEFAULT 1,
|
| 152 |
+
first_seen TEXT,
|
| 153 |
+
last_seen TEXT,
|
| 154 |
+
crystallized INTEGER DEFAULT 0
|
| 155 |
+
)
|
| 156 |
+
""")
|
| 157 |
+
|
| 158 |
+
conn.commit()
|
| 159 |
+
conn.close()
|
| 160 |
+
print(f" [OK] Memory database: {DB_PATH}")
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# ββ Layer 1: Memory ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 164 |
+
|
| 165 |
+
def task_hash(task: str) -> str:
|
| 166 |
+
return hashlib.md5(task.lower().strip().encode()).hexdigest()[:12]
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def store_task(task: str, result: CodeAttempt, lessons: dict):
|
| 170 |
+
"""Permanently store this task run β win or lose."""
|
| 171 |
+
conn = get_db()
|
| 172 |
+
conn.execute("""
|
| 173 |
+
INSERT INTO task_history
|
| 174 |
+
(task_hash, task, timestamp, final_score, passed,
|
| 175 |
+
iterations, final_code, lessons, task_category)
|
| 176 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 177 |
+
""", (
|
| 178 |
+
task_hash(task),
|
| 179 |
+
task,
|
| 180 |
+
datetime.now().isoformat(),
|
| 181 |
+
result.score,
|
| 182 |
+
int(result.passed),
|
| 183 |
+
result.iteration,
|
| 184 |
+
result.code,
|
| 185 |
+
json.dumps(lessons),
|
| 186 |
+
lessons.get("category", "general")
|
| 187 |
+
))
|
| 188 |
+
conn.commit()
|
| 189 |
+
conn.close()
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def load_relevant_history(task: str) -> list[dict]:
|
| 193 |
+
"""
|
| 194 |
+
Find past tasks semantically similar to this one.
|
| 195 |
+
Simple keyword overlap for now β no embedding needed.
|
| 196 |
+
"""
|
| 197 |
+
conn = get_db()
|
| 198 |
+
words = set(re.findall(r'\w+', task.lower())) - {
|
| 199 |
+
'a','an','the','write','create','build','make','function',
|
| 200 |
+
'that','with','and','or','for','in','to','of','is','it'
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
if not words:
|
| 204 |
+
conn.close()
|
| 205 |
+
return []
|
| 206 |
+
|
| 207 |
+
# Score each history item by keyword overlap
|
| 208 |
+
rows = conn.execute(
|
| 209 |
+
"SELECT * FROM task_history ORDER BY timestamp DESC LIMIT 100"
|
| 210 |
+
).fetchall()
|
| 211 |
+
conn.close()
|
| 212 |
+
|
| 213 |
+
scored = []
|
| 214 |
+
for row in rows:
|
| 215 |
+
row_words = set(re.findall(r'\w+', row['task'].lower()))
|
| 216 |
+
overlap = len(words & row_words) / max(len(words), 1)
|
| 217 |
+
if overlap > 0.2:
|
| 218 |
+
scored.append((overlap, dict(row)))
|
| 219 |
+
|
| 220 |
+
scored.sort(key=lambda x: x[0], reverse=True)
|
| 221 |
+
return [r for _, r in scored[:5]]
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
# ββ Layer 2: Pattern Library βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 225 |
+
|
| 226 |
+
def extract_patterns(task: str, result: CodeAttempt) -> list[dict]:
|
| 227 |
+
"""
|
| 228 |
+
After a successful run, ask the model to distill
|
| 229 |
+
reusable patterns from the winning code.
|
| 230 |
+
"""
|
| 231 |
+
if not result.passed:
|
| 232 |
+
return []
|
| 233 |
+
|
| 234 |
+
print(" π Extracting reusable patterns...")
|
| 235 |
+
lang = get_language()
|
| 236 |
+
lang_cfg = _LANG_CONFIG.get(lang, _LANG_CONFIG["python"])
|
| 237 |
+
expert = lang_cfg["expert"]
|
| 238 |
+
fence = lang_cfg["fence"]
|
| 239 |
+
|
| 240 |
+
prompt = f"""You are a senior {expert} building a pattern library.
|
| 241 |
+
|
| 242 |
+
A task was solved successfully (score: {result.score:.1f}%):
|
| 243 |
+
|
| 244 |
+
TASK: {task}
|
| 245 |
+
|
| 246 |
+
WINNING CODE:
|
| 247 |
+
```{fence}
|
| 248 |
+
{result.code}
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
Extract up to 3 reusable patterns from this code.
|
| 252 |
+
Each pattern should be something that could help solve FUTURE problems.
|
| 253 |
+
|
| 254 |
+
Respond ONLY with a JSON array (no other text):
|
| 255 |
+
[
|
| 256 |
+
{{
|
| 257 |
+
"name": "short pattern name",
|
| 258 |
+
"description": "what this pattern does and when to use it",
|
| 259 |
+
"code_template": "the reusable code snippet",
|
| 260 |
+
"use_cases": "comma-separated list of problem types this helps with"
|
| 261 |
+
}}
|
| 262 |
+
]"""
|
| 263 |
+
|
| 264 |
+
raw = ollama(prompt, temperature=0.2)
|
| 265 |
+
|
| 266 |
+
try:
|
| 267 |
+
raw = re.sub(r'```.*?```', '', raw, flags=re.DOTALL).strip()
|
| 268 |
+
match = re.search(r'\[.*\]', raw, re.DOTALL)
|
| 269 |
+
if match:
|
| 270 |
+
patterns = json.loads(match.group())
|
| 271 |
+
return patterns[:3]
|
| 272 |
+
except Exception:
|
| 273 |
+
pass
|
| 274 |
+
return []
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def store_patterns(patterns: list[dict]):
|
| 278 |
+
"""Save extracted patterns to the library."""
|
| 279 |
+
if not patterns:
|
| 280 |
+
return
|
| 281 |
+
conn = get_db()
|
| 282 |
+
for p in patterns:
|
| 283 |
+
ph = hashlib.md5(p.get('name','').encode()).hexdigest()[:12]
|
| 284 |
+
try:
|
| 285 |
+
conn.execute("""
|
| 286 |
+
INSERT OR IGNORE INTO pattern_library
|
| 287 |
+
(pattern_hash, pattern_name, description, code_template,
|
| 288 |
+
use_cases, created_at, last_used)
|
| 289 |
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
| 290 |
+
""", (
|
| 291 |
+
ph,
|
| 292 |
+
p.get('name', 'unnamed'),
|
| 293 |
+
p.get('description', ''),
|
| 294 |
+
p.get('code_template', ''),
|
| 295 |
+
p.get('use_cases', ''),
|
| 296 |
+
datetime.now().isoformat(),
|
| 297 |
+
datetime.now().isoformat()
|
| 298 |
+
))
|
| 299 |
+
except Exception:
|
| 300 |
+
pass
|
| 301 |
+
conn.commit()
|
| 302 |
+
conn.close()
|
| 303 |
+
print(f" π Stored {len(patterns)} pattern(s) to library")
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def load_relevant_patterns(task: str) -> list[dict]:
|
| 307 |
+
"""Find patterns from the library relevant to this task."""
|
| 308 |
+
conn = get_db()
|
| 309 |
+
patterns = conn.execute(
|
| 310 |
+
"SELECT * FROM pattern_library ORDER BY times_helped DESC, score_when_used DESC LIMIT 20"
|
| 311 |
+
).fetchall()
|
| 312 |
+
conn.close()
|
| 313 |
+
|
| 314 |
+
words = set(re.findall(r'\w+', task.lower()))
|
| 315 |
+
scored = []
|
| 316 |
+
for p in patterns:
|
| 317 |
+
p = dict(p)
|
| 318 |
+
use_words = set(re.findall(r'\w+', (p.get('use_cases','') + ' ' + p.get('description','')).lower()))
|
| 319 |
+
overlap = len(words & use_words) / max(len(words), 1)
|
| 320 |
+
if overlap > 0.1:
|
| 321 |
+
scored.append((overlap, p))
|
| 322 |
+
|
| 323 |
+
scored.sort(key=lambda x: x[0], reverse=True)
|
| 324 |
+
return [p for _, p in scored[:MAX_CONTEXT_PATTERNS]]
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
# ββ Observation Ledger βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 328 |
+
# "The world is chaotic but patterns exist. They appear when observed honestly
|
| 329 |
+
# and not forced. It takes time and patience." β Gordo
|
| 330 |
+
#
|
| 331 |
+
# Raw observations accumulate. Nothing is forced into a pattern immediately.
|
| 332 |
+
# Only when the same insight surfaces across multiple independent tasks does
|
| 333 |
+
# it earn the right to become a pattern. Patience, not speed.
|
| 334 |
+
|
| 335 |
+
OBSERVATION_CRYSTALLIZE_THRESHOLD = 3 # seen this many times β becomes a pattern
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def store_observation(task: str, result: CodeAttempt):
|
| 339 |
+
"""
|
| 340 |
+
After every run, record raw observations β what actually happened.
|
| 341 |
+
Don't interpret. Don't force. Just observe honestly.
|
| 342 |
+
"""
|
| 343 |
+
if result.iteration == 0:
|
| 344 |
+
return # TALK β nothing to observe
|
| 345 |
+
|
| 346 |
+
print(" \U0001f440 Recording raw observations...")
|
| 347 |
+
|
| 348 |
+
prompt = f"""You are recording honest observations from a coding task.
|
| 349 |
+
Do NOT generalize. Do NOT force patterns. Just describe what actually happened.
|
| 350 |
+
|
| 351 |
+
TASK: {task[:300]}
|
| 352 |
+
SCORE: {result.score:.1f}%
|
| 353 |
+
PASSED: {result.passed}
|
| 354 |
+
ITERATIONS: {result.iteration}
|
| 355 |
+
ERRORS: {result.errors[:3] if result.errors else 'none'}
|
| 356 |
+
REFLECTIONS: {result.reflections[:3] if result.reflections else 'none'}
|
| 357 |
+
|
| 358 |
+
List up to 3 honest, specific observations. Not lessons. Not advice. Just what happened.
|
| 359 |
+
|
| 360 |
+
Respond ONLY with a JSON array:
|
| 361 |
+
[
|
| 362 |
+
{{
|
| 363 |
+
"observation": "what actually happened (factual, specific)",
|
| 364 |
+
"domain": "the technical domain (e.g., error_handling, architecture, networking, parsing)"
|
| 365 |
+
}}
|
| 366 |
+
]"""
|
| 367 |
+
|
| 368 |
+
raw = ollama(prompt, temperature=0.2)
|
| 369 |
+
|
| 370 |
+
try:
|
| 371 |
+
raw = re.sub(r'```.*?```', '', raw, flags=re.DOTALL).strip()
|
| 372 |
+
match = re.search(r'\[.*\]', raw, re.DOTALL)
|
| 373 |
+
if match:
|
| 374 |
+
observations = json.loads(match.group())[:3]
|
| 375 |
+
else:
|
| 376 |
+
return
|
| 377 |
+
except Exception:
|
| 378 |
+
return
|
| 379 |
+
|
| 380 |
+
conn = get_db()
|
| 381 |
+
stored = 0
|
| 382 |
+
for obs in observations:
|
| 383 |
+
text = obs.get('observation', '').strip()
|
| 384 |
+
domain = obs.get('domain', 'general').strip()
|
| 385 |
+
if not text:
|
| 386 |
+
continue
|
| 387 |
+
oh = hashlib.md5(text.lower().encode()).hexdigest()[:12]
|
| 388 |
+
|
| 389 |
+
existing = conn.execute(
|
| 390 |
+
"SELECT id, times_seen FROM observation_ledger WHERE obs_hash=?", (oh,)
|
| 391 |
+
).fetchone()
|
| 392 |
+
|
| 393 |
+
if existing:
|
| 394 |
+
conn.execute(
|
| 395 |
+
"UPDATE observation_ledger SET times_seen=?, last_seen=? WHERE obs_hash=?",
|
| 396 |
+
(existing['times_seen'] + 1, datetime.now().isoformat(), oh)
|
| 397 |
+
)
|
| 398 |
+
else:
|
| 399 |
+
conn.execute("""
|
| 400 |
+
INSERT INTO observation_ledger
|
| 401 |
+
(obs_hash, observation, context, domain, first_seen, last_seen)
|
| 402 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
| 403 |
+
""", (
|
| 404 |
+
oh, text, task[:200], domain,
|
| 405 |
+
datetime.now().isoformat(), datetime.now().isoformat()
|
| 406 |
+
))
|
| 407 |
+
stored += 1
|
| 408 |
+
|
| 409 |
+
conn.commit()
|
| 410 |
+
conn.close()
|
| 411 |
+
if stored:
|
| 412 |
+
print(f" \U0001f440 Recorded {stored} observation(s) β no pattern forced")
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
def crystallize_observations():
|
| 416 |
+
"""
|
| 417 |
+
Check if any observations have been seen enough times
|
| 418 |
+
to naturally become a pattern. Patience, not speed.
|
| 419 |
+
Only called periodically β not after every single task.
|
| 420 |
+
"""
|
| 421 |
+
conn = get_db()
|
| 422 |
+
ripe = conn.execute("""
|
| 423 |
+
SELECT * FROM observation_ledger
|
| 424 |
+
WHERE times_seen >= ? AND crystallized = 0
|
| 425 |
+
ORDER BY times_seen DESC
|
| 426 |
+
""", (OBSERVATION_CRYSTALLIZE_THRESHOLD,)).fetchall()
|
| 427 |
+
|
| 428 |
+
if not ripe:
|
| 429 |
+
conn.close()
|
| 430 |
+
return
|
| 431 |
+
|
| 432 |
+
print(f"\n \U0001f48e {len(ripe)} observation(s) have surfaced enough times to crystallize...")
|
| 433 |
+
|
| 434 |
+
for obs in ripe:
|
| 435 |
+
obs = dict(obs)
|
| 436 |
+
# Promote to pattern library
|
| 437 |
+
ph = hashlib.md5(obs['observation'].encode()).hexdigest()[:12]
|
| 438 |
+
try:
|
| 439 |
+
conn.execute("""
|
| 440 |
+
INSERT OR IGNORE INTO pattern_library
|
| 441 |
+
(pattern_hash, pattern_name, description, code_template,
|
| 442 |
+
use_cases, created_at, last_used)
|
| 443 |
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
| 444 |
+
""", (
|
| 445 |
+
ph,
|
| 446 |
+
f"[emerged] {obs['domain']}",
|
| 447 |
+
obs['observation'],
|
| 448 |
+
'', # no template β this is an insight, not code
|
| 449 |
+
obs['domain'],
|
| 450 |
+
obs['first_seen'],
|
| 451 |
+
datetime.now().isoformat()
|
| 452 |
+
))
|
| 453 |
+
# Mark as crystallized
|
| 454 |
+
conn.execute(
|
| 455 |
+
"UPDATE observation_ledger SET crystallized=1 WHERE id=?",
|
| 456 |
+
(obs['id'],)
|
| 457 |
+
)
|
| 458 |
+
print(f" \U0001f48e Crystallized: {obs['observation'][:80]}")
|
| 459 |
+
print(f" (seen {obs['times_seen']}x across tasks since {obs['first_seen'][:10]})")
|
| 460 |
+
except Exception:
|
| 461 |
+
pass
|
| 462 |
+
|
| 463 |
+
conn.commit()
|
| 464 |
+
conn.close()
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
# ββ Layer 3: Failure Library βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 468 |
+
|
| 469 |
+
def extract_failures(task: str, result: CodeAttempt) -> list[dict]:
|
| 470 |
+
"""
|
| 471 |
+
Even from a passing run, extract what failed along the way.
|
| 472 |
+
From a failing run, extract everything we learned.
|
| 473 |
+
"""
|
| 474 |
+
if result.iteration == 1 and result.passed:
|
| 475 |
+
return [] # First try success β nothing to learn about failure
|
| 476 |
+
|
| 477 |
+
failed_attempts = []
|
| 478 |
+
if result.errors:
|
| 479 |
+
failed_attempts = result.errors
|
| 480 |
+
|
| 481 |
+
if not failed_attempts and not result.reflections:
|
| 482 |
+
return []
|
| 483 |
+
|
| 484 |
+
print(" β οΈ Extracting failure lessons...")
|
| 485 |
+
|
| 486 |
+
prompt = f"""You are building a failure pattern library to help future code generation avoid mistakes.
|
| 487 |
+
|
| 488 |
+
TASK: {task}
|
| 489 |
+
ERRORS ENCOUNTERED: {result.errors}
|
| 490 |
+
REFLECTIONS DURING IMPROVEMENT: {result.reflections}
|
| 491 |
+
FINAL SCORE: {result.score:.1f}%
|
| 492 |
+
|
| 493 |
+
Extract up to 3 distinct failure patterns from this run.
|
| 494 |
+
Be specific β vague lessons are useless.
|
| 495 |
+
|
| 496 |
+
Respond ONLY with a JSON array:
|
| 497 |
+
[
|
| 498 |
+
{{
|
| 499 |
+
"description": "exactly what went wrong",
|
| 500 |
+
"root_cause": "why it went wrong",
|
| 501 |
+
"fix": "how to avoid this in future"
|
| 502 |
+
}}
|
| 503 |
+
]"""
|
| 504 |
+
|
| 505 |
+
raw = ollama(prompt, temperature=0.2)
|
| 506 |
+
|
| 507 |
+
try:
|
| 508 |
+
raw = re.sub(r'```.*?```', '', raw, flags=re.DOTALL).strip()
|
| 509 |
+
match = re.search(r'\[.*\]', raw, re.DOTALL)
|
| 510 |
+
if match:
|
| 511 |
+
return json.loads(match.group())[:3]
|
| 512 |
+
except Exception:
|
| 513 |
+
pass
|
| 514 |
+
return []
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
def store_failures(failures: list[dict]):
|
| 518 |
+
"""Store failure patterns β increment count if seen before."""
|
| 519 |
+
if not failures:
|
| 520 |
+
return
|
| 521 |
+
conn = get_db()
|
| 522 |
+
for f in failures:
|
| 523 |
+
fh = hashlib.md5(f.get('description','').encode()).hexdigest()[:12]
|
| 524 |
+
existing = conn.execute(
|
| 525 |
+
"SELECT id, times_seen FROM failure_library WHERE failure_hash=?", (fh,)
|
| 526 |
+
).fetchone()
|
| 527 |
+
if existing:
|
| 528 |
+
conn.execute(
|
| 529 |
+
"UPDATE failure_library SET times_seen=?, last_seen=? WHERE failure_hash=?",
|
| 530 |
+
(existing['times_seen'] + 1, datetime.now().isoformat(), fh)
|
| 531 |
+
)
|
| 532 |
+
else:
|
| 533 |
+
conn.execute("""
|
| 534 |
+
INSERT INTO failure_library
|
| 535 |
+
(failure_hash, description, root_cause, fix, last_seen)
|
| 536 |
+
VALUES (?, ?, ?, ?, ?)
|
| 537 |
+
""", (
|
| 538 |
+
fh,
|
| 539 |
+
f.get('description',''),
|
| 540 |
+
f.get('root_cause',''),
|
| 541 |
+
f.get('fix',''),
|
| 542 |
+
datetime.now().isoformat()
|
| 543 |
+
))
|
| 544 |
+
conn.commit()
|
| 545 |
+
conn.close()
|
| 546 |
+
print(f" β οΈ Stored {len(failures)} failure lesson(s)")
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
def load_relevant_failures(task: str) -> list[dict]:
|
| 550 |
+
"""Load the most relevant known failure patterns for this task."""
|
| 551 |
+
conn = get_db()
|
| 552 |
+
failures = conn.execute(
|
| 553 |
+
"SELECT * FROM failure_library ORDER BY times_seen DESC LIMIT 20"
|
| 554 |
+
).fetchall()
|
| 555 |
+
conn.close()
|
| 556 |
+
|
| 557 |
+
words = set(re.findall(r'\w+', task.lower()))
|
| 558 |
+
scored = []
|
| 559 |
+
for f in failures:
|
| 560 |
+
f = dict(f)
|
| 561 |
+
f_words = set(re.findall(r'\w+', (f.get('description','') + ' ' + f.get('root_cause','')).lower()))
|
| 562 |
+
overlap = len(words & f_words) / max(len(words), 1)
|
| 563 |
+
scored.append((overlap + (f['times_seen'] * 0.05), f))
|
| 564 |
+
|
| 565 |
+
scored.sort(key=lambda x: x[0], reverse=True)
|
| 566 |
+
return [f for _, f in scored[:MAX_CONTEXT_FAILURES]]
|
| 567 |
+
|
| 568 |
+
|
| 569 |
+
# ββ Layer 4: Confidence Calibration βββββββββββββββββββββββββββββββββββββββββββ
|
| 570 |
+
|
| 571 |
+
def log_confidence(stated: float, actual_score: float, passed: bool):
|
| 572 |
+
"""Track when the model's stated confidence was accurate vs. wrong."""
|
| 573 |
+
conn = get_db()
|
| 574 |
+
error = abs((stated * 100) - actual_score)
|
| 575 |
+
conn.execute("""
|
| 576 |
+
INSERT INTO confidence_log
|
| 577 |
+
(timestamp, stated_confidence, actual_score, passed, error)
|
| 578 |
+
VALUES (?, ?, ?, ?, ?)
|
| 579 |
+
""", (datetime.now().isoformat(), stated, actual_score, int(passed), error))
|
| 580 |
+
conn.commit()
|
| 581 |
+
conn.close()
|
| 582 |
+
|
| 583 |
+
|
| 584 |
+
def get_confidence_calibration() -> dict:
|
| 585 |
+
"""
|
| 586 |
+
How well-calibrated is the model's self-confidence?
|
| 587 |
+
Returns stats the growth engine uses to adjust prompts.
|
| 588 |
+
"""
|
| 589 |
+
conn = get_db()
|
| 590 |
+
rows = conn.execute(
|
| 591 |
+
"SELECT * FROM confidence_log ORDER BY timestamp DESC LIMIT 50"
|
| 592 |
+
).fetchall()
|
| 593 |
+
conn.close()
|
| 594 |
+
|
| 595 |
+
if not rows:
|
| 596 |
+
return {"avg_error": 0, "overconfident": False, "sample_size": 0}
|
| 597 |
+
|
| 598 |
+
errors = [r['error'] for r in rows]
|
| 599 |
+
avg_error = sum(errors) / len(errors)
|
| 600 |
+
# If stated confidence > actual score consistently, it's overconfident
|
| 601 |
+
overconfident = sum(
|
| 602 |
+
1 for r in rows if (r['stated_confidence'] * 100) > r['actual_score']
|
| 603 |
+
) / len(rows) > 0.6
|
| 604 |
+
|
| 605 |
+
return {
|
| 606 |
+
"avg_error": round(avg_error, 1),
|
| 607 |
+
"overconfident": overconfident,
|
| 608 |
+
"sample_size": len(rows),
|
| 609 |
+
"recent_accuracy": round(100 - avg_error, 1)
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
|
| 613 |
+
# ββ Context Builder ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 614 |
+
|
| 615 |
+
def build_growth_context(task: str) -> str:
|
| 616 |
+
"""
|
| 617 |
+
Assemble everything the system has learned
|
| 618 |
+
into a context block injected into the first generation prompt.
|
| 619 |
+
This is how past learning flows into future tasks.
|
| 620 |
+
"""
|
| 621 |
+
parts = []
|
| 622 |
+
|
| 623 |
+
# Relevant patterns
|
| 624 |
+
patterns = load_relevant_patterns(task)
|
| 625 |
+
if patterns:
|
| 626 |
+
parts.append("=== PATTERNS THAT HAVE WORKED BEFORE ===")
|
| 627 |
+
for p in patterns:
|
| 628 |
+
parts.append(f"PATTERN: {p['pattern_name']}")
|
| 629 |
+
parts.append(f" When to use: {p['description']}")
|
| 630 |
+
parts.append(f" Template:\n{p['code_template'][:300]}")
|
| 631 |
+
parts.append("")
|
| 632 |
+
|
| 633 |
+
# Known failures to avoid
|
| 634 |
+
failures = load_relevant_failures(task)
|
| 635 |
+
if failures:
|
| 636 |
+
parts.append("=== KNOWN FAILURE PATTERNS β AVOID THESE ===")
|
| 637 |
+
for f in failures:
|
| 638 |
+
seen = f.get('times_seen', 1)
|
| 639 |
+
parts.append(f"FAILURE (seen {seen}x): {f['description']}")
|
| 640 |
+
parts.append(f" Root cause: {f['root_cause']}")
|
| 641 |
+
parts.append(f" Fix: {f['fix']}")
|
| 642 |
+
parts.append("")
|
| 643 |
+
|
| 644 |
+
# Similar past tasks
|
| 645 |
+
history = load_relevant_history(task)
|
| 646 |
+
if history:
|
| 647 |
+
parts.append("=== SIMILAR TASKS FROM HISTORY ===")
|
| 648 |
+
for h in history[:2]:
|
| 649 |
+
status = "PASSED" if h['passed'] else "failed"
|
| 650 |
+
parts.append(f"PAST TASK ({status}, score {h['final_score']:.0f}%): {h['task'][:80]}")
|
| 651 |
+
if h['passed'] and h['final_code']:
|
| 652 |
+
parts.append(f" Winning approach snippet:\n{h['final_code'][:200]}")
|
| 653 |
+
parts.append("")
|
| 654 |
+
|
| 655 |
+
# Calibration warning
|
| 656 |
+
cal = get_confidence_calibration()
|
| 657 |
+
if cal['sample_size'] > 5 and cal['overconfident']:
|
| 658 |
+
parts.append(f"=== CALIBRATION WARNING ===")
|
| 659 |
+
parts.append(f"This system has been overconfident recently (avg error: {cal['avg_error']:.0f}%).")
|
| 660 |
+
parts.append(f"Be conservative in confidence estimates. Aim higher than you think you need.")
|
| 661 |
+
parts.append("")
|
| 662 |
+
|
| 663 |
+
return "\n".join(parts) if parts else ""
|
| 664 |
+
|
| 665 |
+
|
| 666 |
+
# ββ Lesson Extractor βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 667 |
+
|
| 668 |
+
def extract_lessons(task: str, result: CodeAttempt) -> dict:
|
| 669 |
+
"""
|
| 670 |
+
After every run β win or lose β extract structured lessons.
|
| 671 |
+
This is the core of the growth loop.
|
| 672 |
+
"""
|
| 673 |
+
print("\n π Extracting lessons from this run...")
|
| 674 |
+
|
| 675 |
+
prompt = f"""You completed a Python coding task. Extract lessons for future runs.
|
| 676 |
+
|
| 677 |
+
TASK: {task}
|
| 678 |
+
FINAL SCORE: {result.score:.1f}%
|
| 679 |
+
PASSED: {result.passed}
|
| 680 |
+
ITERATIONS NEEDED: {result.iteration}
|
| 681 |
+
ERRORS: {result.errors}
|
| 682 |
+
|
| 683 |
+
Answer in JSON only:
|
| 684 |
+
{{
|
| 685 |
+
"category": "one of: algorithm, data_structure, error_handling, io, math, string, api, testing, optimization, other",
|
| 686 |
+
"key_lesson": "the single most important thing learned from this run",
|
| 687 |
+
"what_worked": "what approach succeeded or showed promise",
|
| 688 |
+
"what_failed": "what approach failed or caused problems",
|
| 689 |
+
"next_time": "specific advice for the next time a similar task appears",
|
| 690 |
+
"difficulty": "easy/medium/hard"
|
| 691 |
+
}}"""
|
| 692 |
+
|
| 693 |
+
raw = ollama(prompt, temperature=0.2)
|
| 694 |
+
|
| 695 |
+
try:
|
| 696 |
+
raw = re.sub(r'```.*?```', '', raw, flags=re.DOTALL).strip()
|
| 697 |
+
match = re.search(r'\{.*\}', raw, re.DOTALL)
|
| 698 |
+
if match:
|
| 699 |
+
return json.loads(match.group())
|
| 700 |
+
except Exception:
|
| 701 |
+
pass
|
| 702 |
+
|
| 703 |
+
return {
|
| 704 |
+
"category": "general",
|
| 705 |
+
"key_lesson": "task completed",
|
| 706 |
+
"what_worked": "reflection loop",
|
| 707 |
+
"what_failed": "n/a",
|
| 708 |
+
"next_time": "start with edge cases",
|
| 709 |
+
"difficulty": "medium"
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
|
| 713 |
+
# ββ Stats Display ββββββββββββββββββββββββββοΏ½οΏ½βββββββββββββββββββββββββββββββββββ
|
| 714 |
+
|
| 715 |
+
def show_stats():
|
| 716 |
+
"""Print what the system has learned so far."""
|
| 717 |
+
conn = get_db()
|
| 718 |
+
|
| 719 |
+
total = conn.execute("SELECT COUNT(*) as n FROM task_history").fetchone()['n']
|
| 720 |
+
passed = conn.execute("SELECT COUNT(*) as n FROM task_history WHERE passed=1").fetchone()['n']
|
| 721 |
+
patterns = conn.execute("SELECT COUNT(*) as n FROM pattern_library").fetchone()['n']
|
| 722 |
+
failures = conn.execute("SELECT COUNT(*) as n FROM failure_library").fetchone()['n']
|
| 723 |
+
avg_score = conn.execute("SELECT AVG(final_score) as s FROM task_history").fetchone()['s'] or 0
|
| 724 |
+
avg_iters = conn.execute("SELECT AVG(iterations) as i FROM task_history").fetchone()['i'] or 0
|
| 725 |
+
|
| 726 |
+
top_patterns = conn.execute(
|
| 727 |
+
"SELECT pattern_name, times_helped, description FROM pattern_library ORDER BY times_helped DESC LIMIT 5"
|
| 728 |
+
).fetchall()
|
| 729 |
+
|
| 730 |
+
top_failures = conn.execute(
|
| 731 |
+
"SELECT description, times_seen FROM failure_library ORDER BY times_seen DESC LIMIT 5"
|
| 732 |
+
).fetchall()
|
| 733 |
+
|
| 734 |
+
cal = get_confidence_calibration()
|
| 735 |
+
conn.close()
|
| 736 |
+
|
| 737 |
+
print(f"\n{'='*60}")
|
| 738 |
+
print(f" GROWTH ENGINE β WHAT I HAVE LEARNED")
|
| 739 |
+
print(f"{'='*60}")
|
| 740 |
+
print(f"\n Tasks run: {total}")
|
| 741 |
+
print(f" Tasks passed: {passed} ({(passed/total*100):.0f}%)" if total else " Tasks passed: 0")
|
| 742 |
+
print(f" Avg score: {avg_score:.1f}%")
|
| 743 |
+
print(f" Avg iterations: {avg_iters:.1f}")
|
| 744 |
+
print(f" Patterns learned: {patterns}")
|
| 745 |
+
print(f" Failures logged: {failures}")
|
| 746 |
+
|
| 747 |
+
if cal['sample_size'] > 0:
|
| 748 |
+
print(f"\n Confidence accuracy: {cal['recent_accuracy']:.1f}%")
|
| 749 |
+
print(f" Overconfident: {'YES β οΈ' if cal['overconfident'] else 'No β
'}")
|
| 750 |
+
|
| 751 |
+
if top_patterns:
|
| 752 |
+
print(f"\n TOP PATTERNS:")
|
| 753 |
+
for p in top_patterns:
|
| 754 |
+
print(f" β’ {p['pattern_name']} β {p['description'][:50]}")
|
| 755 |
+
|
| 756 |
+
if top_failures:
|
| 757 |
+
print(f"\n MOST COMMON FAILURES:")
|
| 758 |
+
for f in top_failures:
|
| 759 |
+
print(f" β’ (x{f['times_seen']}) {f['description'][:60]}")
|
| 760 |
+
|
| 761 |
+
print(f"\n{'='*60}\n")
|
| 762 |
+
|
| 763 |
+
|
| 764 |
+
def show_patterns():
|
| 765 |
+
"""Print the full pattern library."""
|
| 766 |
+
conn = get_db()
|
| 767 |
+
patterns = conn.execute(
|
| 768 |
+
"SELECT * FROM pattern_library ORDER BY times_helped DESC"
|
| 769 |
+
).fetchall()
|
| 770 |
+
conn.close()
|
| 771 |
+
|
| 772 |
+
if not patterns:
|
| 773 |
+
print("\n No patterns learned yet. Run some tasks first.\n")
|
| 774 |
+
return
|
| 775 |
+
|
| 776 |
+
print(f"\n{'='*60}")
|
| 777 |
+
print(f" PATTERN LIBRARY ({len(patterns)} patterns)")
|
| 778 |
+
print(f"{'='*60}\n")
|
| 779 |
+
|
| 780 |
+
for p in patterns:
|
| 781 |
+
print(f" [{p['pattern_name']}]")
|
| 782 |
+
print(f" {p['description']}")
|
| 783 |
+
print(f" Use cases: {p['use_cases']}")
|
| 784 |
+
print(f" Times helped: {p['times_helped']}")
|
| 785 |
+
print(f" Template preview: {str(p['code_template'])[:100]}...")
|
| 786 |
+
print()
|
| 787 |
+
|
| 788 |
+
|
| 789 |
+
# ββ TALK Memory Builder ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 790 |
+
|
| 791 |
+
def _build_talk_memory(task: str) -> str:
|
| 792 |
+
"""Build a memory summary the LLM can reference during conversation."""
|
| 793 |
+
parts = []
|
| 794 |
+
conn = get_db()
|
| 795 |
+
|
| 796 |
+
# Stats overview
|
| 797 |
+
tasks_done = conn.execute("SELECT COUNT(*) as n FROM task_history").fetchone()["n"]
|
| 798 |
+
patterns = conn.execute("SELECT COUNT(*) as n FROM pattern_library").fetchone()["n"]
|
| 799 |
+
failures = conn.execute("SELECT COUNT(*) as n FROM failure_library").fetchone()["n"]
|
| 800 |
+
parts.append(f"You have completed {tasks_done} tasks. You know {patterns} coding patterns and {failures} failure lessons.")
|
| 801 |
+
|
| 802 |
+
# Recent tasks (last 10)
|
| 803 |
+
recent = conn.execute(
|
| 804 |
+
"SELECT task, passed, final_score FROM task_history ORDER BY timestamp DESC LIMIT 10"
|
| 805 |
+
).fetchall()
|
| 806 |
+
if recent:
|
| 807 |
+
parts.append("\nRecent tasks you completed:")
|
| 808 |
+
for r in recent:
|
| 809 |
+
status = "PASSED" if r["passed"] else "FAILED"
|
| 810 |
+
parts.append(f" - [{status} {r['final_score']:.0f}%] {r['task'][:100]}")
|
| 811 |
+
|
| 812 |
+
# Search for relevant patterns if user asks about something specific
|
| 813 |
+
words = set(re.findall(r'\w+', task.lower()))
|
| 814 |
+
stopwords = {'the','a','an','is','are','do','does','can','you','your','i','me','my',
|
| 815 |
+
'what','how','why','when','where','it','this','that','about','with','have',
|
| 816 |
+
'has','had','was','were','been','be','will','would','could','should'}
|
| 817 |
+
keywords = words - stopwords
|
| 818 |
+
if keywords:
|
| 819 |
+
all_patterns = conn.execute(
|
| 820 |
+
"SELECT pattern_name, description FROM pattern_library"
|
| 821 |
+
).fetchall()
|
| 822 |
+
matched = []
|
| 823 |
+
for p in all_patterns:
|
| 824 |
+
ptext = f"{p['pattern_name']} {p['description']}".lower()
|
| 825 |
+
if any(kw in ptext for kw in keywords):
|
| 826 |
+
matched.append(p)
|
| 827 |
+
if matched:
|
| 828 |
+
parts.append(f"\nPatterns you know related to this topic ({len(matched)}):")
|
| 829 |
+
for m in matched[:10]:
|
| 830 |
+
parts.append(f" - {m['pattern_name']}: {m['description'][:120]}")
|
| 831 |
+
|
| 832 |
+
conn.close()
|
| 833 |
+
return "\n".join(parts) if parts else ""
|
| 834 |
+
|
| 835 |
+
|
| 836 |
+
# ββ Main Growth Loop βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 837 |
+
|
| 838 |
+
def run_growth_engine(task: str, lang: str = "python") -> CodeAttempt:
|
| 839 |
+
"""
|
| 840 |
+
The full growth loop.
|
| 841 |
+
Every run permanently improves the system.
|
| 842 |
+
"""
|
| 843 |
+
set_language(lang)
|
| 844 |
+
print(f"\n{'='*60}")
|
| 845 |
+
print(f" GROWTH ENGINE β SELF-IMPROVING CODE AI")
|
| 846 |
+
print(f" Every problem makes me permanently smarter.")
|
| 847 |
+
print(f"{'='*60}")
|
| 848 |
+
|
| 849 |
+
# ββ Apply earned trust β adjusts BENCHMARK_FLOOR and MAX_ITERATIONS ββ
|
| 850 |
+
apply_trust_before_run()
|
| 851 |
+
|
| 852 |
+
# ββ TALK gate: catch conversation BEFORE enrichment adds context ββ
|
| 853 |
+
from reflection_engine import classify_task
|
| 854 |
+
if classify_task(task) == "TALK":
|
| 855 |
+
print(f"\n π¬ TALK detected β loading memory for conversation.")
|
| 856 |
+
memory_summary = _build_talk_memory(task)
|
| 857 |
+
if memory_summary:
|
| 858 |
+
print(f" π Injecting {len(memory_summary)} chars of memory into TALK")
|
| 859 |
+
result = run_engine(task, talk_memory=memory_summary)
|
| 860 |
+
return result
|
| 861 |
+
|
| 862 |
+
# ββ Load what we know ββ
|
| 863 |
+
# Budget depends on task length β long prompts (C# MMO tasks) leave less room
|
| 864 |
+
task_len = len(task)
|
| 865 |
+
if task_len > 1000:
|
| 866 |
+
MAX_CONTEXT_CHARS = 500 # long prompts β minimal context injection
|
| 867 |
+
elif task_len > 600:
|
| 868 |
+
MAX_CONTEXT_CHARS = 800
|
| 869 |
+
else:
|
| 870 |
+
MAX_CONTEXT_CHARS = 1500 # short prompts (Python snippets) β full context
|
| 871 |
+
|
| 872 |
+
context = build_growth_context(task)
|
| 873 |
+
if context:
|
| 874 |
+
if len(context) > MAX_CONTEXT_CHARS:
|
| 875 |
+
context = context[:MAX_CONTEXT_CHARS] + "\n[...truncated to fit model context]"
|
| 876 |
+
print(f"\n π Prior knowledge: {len(context)} chars (budget {MAX_CONTEXT_CHARS}, task {task_len} chars)")
|
| 877 |
+
else:
|
| 878 |
+
print(f"\n π Prior knowledge: {len(context)} chars")
|
| 879 |
+
# Inject context into the task so reflection_engine uses it
|
| 880 |
+
enriched_task = f"{task}\n\n[PRIOR KNOWLEDGE FROM MEMORY]\n{context}"
|
| 881 |
+
else:
|
| 882 |
+
enriched_task = task
|
| 883 |
+
print(f"\n π No prior knowledge found β this is a new territory")
|
| 884 |
+
|
| 885 |
+
# ββ Run the reflection engine ββ
|
| 886 |
+
result = run_engine(enriched_task)
|
| 887 |
+
|
| 888 |
+
# ββ TALK shortcut: don't learn from greetings ββ
|
| 889 |
+
if result.iteration == 0:
|
| 890 |
+
print(f"\n π¬ TALK response β no learning needed.")
|
| 891 |
+
return result
|
| 892 |
+
|
| 893 |
+
# ββ Learn from what just happened ββ
|
| 894 |
+
print(f"\n{'β'*60}")
|
| 895 |
+
print(f" π± GROWING FROM THIS EXPERIENCE")
|
| 896 |
+
print(f"{'β'*60}")
|
| 897 |
+
|
| 898 |
+
# Extract and store lessons
|
| 899 |
+
lessons = extract_lessons(task, result)
|
| 900 |
+
store_task(task, result, lessons)
|
| 901 |
+
print(f" π Lesson: {lessons.get('key_lesson','')[:80]}")
|
| 902 |
+
|
| 903 |
+
# Extract and store patterns (from wins)
|
| 904 |
+
patterns = extract_patterns(task, result)
|
| 905 |
+
store_patterns(patterns)
|
| 906 |
+
|
| 907 |
+
# Extract and store failure lessons (from every run)
|
| 908 |
+
failures = extract_failures(task, result)
|
| 909 |
+
store_failures(failures)
|
| 910 |
+
|
| 911 |
+
# Record raw observations β honest, not forced
|
| 912 |
+
store_observation(task, result)
|
| 913 |
+
|
| 914 |
+
# Log confidence calibration
|
| 915 |
+
if result.reflections:
|
| 916 |
+
# Use final iteration confidence as proxy
|
| 917 |
+
log_confidence(0.85, result.score, result.passed)
|
| 918 |
+
|
| 919 |
+
# ββ Show what we learned ββ
|
| 920 |
+
cal = get_confidence_calibration()
|
| 921 |
+
conn = get_db()
|
| 922 |
+
total = conn.execute("SELECT COUNT(*) as n FROM task_history").fetchone()['n']
|
| 923 |
+
|
| 924 |
+
# Check if any observations have naturally crystallized
|
| 925 |
+
# (only every 5 tasks to give them time to accumulate)
|
| 926 |
+
if total % 5 == 0:
|
| 927 |
+
crystallize_observations()
|
| 928 |
+
pl = conn.execute("SELECT COUNT(*) as n FROM pattern_library").fetchone()['n']
|
| 929 |
+
fl = conn.execute("SELECT COUNT(*) as n FROM failure_library").fetchone()['n']
|
| 930 |
+
conn.close()
|
| 931 |
+
|
| 932 |
+
print(f"\n π System state after this run:")
|
| 933 |
+
print(f" Tasks in memory: {total}")
|
| 934 |
+
print(f" Patterns learned: {pl}")
|
| 935 |
+
print(f" Failures logged: {fl}")
|
| 936 |
+
if cal['sample_size'] > 0:
|
| 937 |
+
print(f" Confidence accuracy: {cal['recent_accuracy']:.1f}%")
|
| 938 |
+
|
| 939 |
+
print(f"\n Next time a similar task comes in,")
|
| 940 |
+
print(f" I will start with {pl} pattern(s) and avoid {fl} known failure(s).")
|
| 941 |
+
print(f" I am permanently smarter than I was before this task.\n")
|
| 942 |
+
|
| 943 |
+
# ββ Auto model scan β let Harvester manage its own stack ββ
|
| 944 |
+
try:
|
| 945 |
+
from model_manager import should_auto_scan, auto_scan_and_evaluate
|
| 946 |
+
if should_auto_scan():
|
| 947 |
+
print(f"\n π Auto-scan triggered β checking for better models...")
|
| 948 |
+
auto_scan_and_evaluate()
|
| 949 |
+
except Exception as e:
|
| 950 |
+
# Non-fatal β don't break the growth loop over model management
|
| 951 |
+
print(f" [model scan] skipped: {e}")
|
| 952 |
+
|
| 953 |
+
return result
|
| 954 |
+
|
| 955 |
+
|
| 956 |
+
# οΏ½οΏ½β Entry point ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 957 |
+
|
| 958 |
+
if __name__ == "__main__":
|
| 959 |
+
init_db()
|
| 960 |
+
|
| 961 |
+
if len(sys.argv) > 1:
|
| 962 |
+
arg = sys.argv[1]
|
| 963 |
+
|
| 964 |
+
if arg == "--stats":
|
| 965 |
+
show_stats()
|
| 966 |
+
sys.exit(0)
|
| 967 |
+
|
| 968 |
+
if arg == "--patterns":
|
| 969 |
+
show_patterns()
|
| 970 |
+
sys.exit(0)
|
| 971 |
+
|
| 972 |
+
# Parse --lang flag
|
| 973 |
+
lang = "python"
|
| 974 |
+
args = sys.argv[1:]
|
| 975 |
+
if "--lang" in args:
|
| 976 |
+
idx = args.index("--lang")
|
| 977 |
+
if idx + 1 < len(args):
|
| 978 |
+
lang = args[idx + 1]
|
| 979 |
+
args = args[:idx] + args[idx+2:] # remove --lang and its value
|
| 980 |
+
task = " ".join(args)
|
| 981 |
+
else:
|
| 982 |
+
print("\nGROWTH ENGINE β Interactive Mode")
|
| 983 |
+
print("Commands: --stats | --patterns | or just describe a task")
|
| 984 |
+
print("β" * 40)
|
| 985 |
+
task = input("What should I build? β ").strip()
|
| 986 |
+
lang = "python"
|
| 987 |
+
if not task:
|
| 988 |
+
task = "Write a function that merges two sorted lists into one sorted list, with tests"
|
| 989 |
+
|
| 990 |
+
result = run_growth_engine(task, lang=lang)
|
| 991 |
+
|
| 992 |
+
print(f"\n{'='*60}")
|
| 993 |
+
print(f" FINAL CODE (score: {result.score:.1f}%)")
|
| 994 |
+
print(f"{'='*60}\n")
|
| 995 |
+
print(result.code)
|
model_manager.py
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
MODEL MANAGER
|
| 4 |
+
=============
|
| 5 |
+
Harvester's tool for managing its own model stack.
|
| 6 |
+
Discovers, evaluates, and adopts models autonomously β with rules.
|
| 7 |
+
|
| 8 |
+
Rules (non-negotiable):
|
| 9 |
+
1. NEVER remove a working model without a tested replacement
|
| 10 |
+
2. New model must BEAT current model on target domain tasks
|
| 11 |
+
3. Must fit within hardware constraints (VRAM/RAM)
|
| 12 |
+
4. Every decision logged to growth_memory.db with reasoning
|
| 13 |
+
5. Benchmark before adopt β no untested models in production
|
| 14 |
+
6. Keep at least one fallback model per role (coder, reasoner)
|
| 15 |
+
7. Human can override any decision via config or env var
|
| 16 |
+
|
| 17 |
+
Roles:
|
| 18 |
+
- coder: Code generation, reflection, troubleshooting
|
| 19 |
+
- reasoner: OSINT, investigation, complex reasoning
|
| 20 |
+
|
| 21 |
+
Usage:
|
| 22 |
+
python model_manager.py --scan # Discover models on disk + HuggingFace
|
| 23 |
+
python model_manager.py --benchmark <role> # Benchmark current model for a role
|
| 24 |
+
python model_manager.py --evaluate <model> # Test a candidate model
|
| 25 |
+
python model_manager.py --adopt <model> <role> # Adopt after passing benchmarks
|
| 26 |
+
python model_manager.py --status # Current stack status
|
| 27 |
+
python model_manager.py --history # Decision log
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
import os
|
| 31 |
+
import sys
|
| 32 |
+
import json
|
| 33 |
+
import sqlite3
|
| 34 |
+
import time
|
| 35 |
+
import hashlib
|
| 36 |
+
from pathlib import Path
|
| 37 |
+
from datetime import datetime
|
| 38 |
+
from dataclasses import dataclass, asdict
|
| 39 |
+
from typing import Optional
|
| 40 |
+
|
| 41 |
+
sys.path.insert(0, os.path.dirname(__file__))
|
| 42 |
+
|
| 43 |
+
# ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 44 |
+
|
| 45 |
+
MODEL_DIR = os.environ.get("MODEL_DIR", r"D:\va_data\models")
|
| 46 |
+
DB_PATH = os.environ.get("GROWTH_DB_PATH", os.path.join(os.path.dirname(__file__), "growth_memory.db"))
|
| 47 |
+
LMSTUDIO_CACHE = os.path.expanduser(os.path.join("~", ".cache", "lm-studio", "models"))
|
| 48 |
+
|
| 49 |
+
# Hardware constraints β updated via env or auto-detect
|
| 50 |
+
MAX_RAM_GB = float(os.environ.get("MAX_RAM_GB", "64"))
|
| 51 |
+
MAX_VRAM_GB = float(os.environ.get("MAX_VRAM_GB", "0")) # 0 = CPU only
|
| 52 |
+
GPU_LAYERS = int(os.environ.get("NATIVE_GPU_LAYERS", "0"))
|
| 53 |
+
|
| 54 |
+
# Adoption rules
|
| 55 |
+
MIN_BENCHMARK_IMPROVEMENT = float(os.environ.get("MIN_BENCHMARK_IMPROVEMENT", "0.05")) # 5% better
|
| 56 |
+
MIN_BENCHMARK_TASKS = int(os.environ.get("MIN_BENCHMARK_TASKS", "5")) # at least 5 tasks
|
| 57 |
+
MAX_MODEL_SIZE_GB = float(os.environ.get("MAX_MODEL_SIZE_GB", "0")) # 0 = auto from hardware
|
| 58 |
+
|
| 59 |
+
# ββ Data structures βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 60 |
+
|
| 61 |
+
@dataclass
|
| 62 |
+
class ModelInfo:
|
| 63 |
+
name: str
|
| 64 |
+
path: str
|
| 65 |
+
size_gb: float
|
| 66 |
+
role: str # "coder", "reasoner", "unknown"
|
| 67 |
+
source: str # "local", "lmstudio_cache", "huggingface"
|
| 68 |
+
quantization: str # "Q4_K_M", "Q8_0", etc.
|
| 69 |
+
status: str # "active", "candidate", "retired", "fallback"
|
| 70 |
+
benchmark_score: float = 0.0
|
| 71 |
+
benchmark_tasks: int = 0
|
| 72 |
+
adopted_at: Optional[str] = None
|
| 73 |
+
notes: str = ""
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@dataclass
|
| 77 |
+
class ModelDecision:
|
| 78 |
+
timestamp: str
|
| 79 |
+
action: str # "scan", "benchmark", "adopt", "reject", "retire"
|
| 80 |
+
model_name: str
|
| 81 |
+
role: str
|
| 82 |
+
reasoning: str
|
| 83 |
+
score_before: float
|
| 84 |
+
score_after: float
|
| 85 |
+
approved: bool
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# ββ Database ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 89 |
+
|
| 90 |
+
def _init_db():
|
| 91 |
+
"""Create model management tables if they don't exist."""
|
| 92 |
+
conn = sqlite3.connect(DB_PATH)
|
| 93 |
+
c = conn.cursor()
|
| 94 |
+
c.execute("""
|
| 95 |
+
CREATE TABLE IF NOT EXISTS model_registry (
|
| 96 |
+
name TEXT NOT NULL,
|
| 97 |
+
path TEXT NOT NULL,
|
| 98 |
+
size_gb REAL,
|
| 99 |
+
role TEXT,
|
| 100 |
+
source TEXT,
|
| 101 |
+
quantization TEXT,
|
| 102 |
+
status TEXT DEFAULT 'candidate',
|
| 103 |
+
benchmark_score REAL DEFAULT 0.0,
|
| 104 |
+
benchmark_tasks INTEGER DEFAULT 0,
|
| 105 |
+
adopted_at TEXT,
|
| 106 |
+
notes TEXT,
|
| 107 |
+
updated_at TEXT,
|
| 108 |
+
PRIMARY KEY (name, role)
|
| 109 |
+
)
|
| 110 |
+
""")
|
| 111 |
+
c.execute("""
|
| 112 |
+
CREATE TABLE IF NOT EXISTS model_decisions (
|
| 113 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 114 |
+
timestamp TEXT,
|
| 115 |
+
action TEXT,
|
| 116 |
+
model_name TEXT,
|
| 117 |
+
role TEXT,
|
| 118 |
+
reasoning TEXT,
|
| 119 |
+
score_before REAL,
|
| 120 |
+
score_after REAL,
|
| 121 |
+
approved INTEGER
|
| 122 |
+
)
|
| 123 |
+
""")
|
| 124 |
+
conn.commit()
|
| 125 |
+
conn.close()
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def _log_decision(decision: ModelDecision):
|
| 129 |
+
"""Log every model decision to the database."""
|
| 130 |
+
conn = sqlite3.connect(DB_PATH)
|
| 131 |
+
c = conn.cursor()
|
| 132 |
+
c.execute("""
|
| 133 |
+
INSERT INTO model_decisions (timestamp, action, model_name, role, reasoning, score_before, score_after, approved)
|
| 134 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
| 135 |
+
""", (decision.timestamp, decision.action, decision.model_name, decision.role,
|
| 136 |
+
decision.reasoning, decision.score_before, decision.score_after, int(decision.approved)))
|
| 137 |
+
conn.commit()
|
| 138 |
+
conn.close()
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def _save_model(model: ModelInfo):
|
| 142 |
+
"""Upsert model info in registry."""
|
| 143 |
+
conn = sqlite3.connect(DB_PATH)
|
| 144 |
+
c = conn.cursor()
|
| 145 |
+
c.execute("""
|
| 146 |
+
INSERT INTO model_registry (name, path, size_gb, role, source, quantization, status, benchmark_score, benchmark_tasks, adopted_at, notes, updated_at)
|
| 147 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 148 |
+
ON CONFLICT(name, role) DO UPDATE SET
|
| 149 |
+
path=excluded.path, size_gb=excluded.size_gb, source=excluded.source,
|
| 150 |
+
quantization=excluded.quantization, status=excluded.status,
|
| 151 |
+
benchmark_score=excluded.benchmark_score, benchmark_tasks=excluded.benchmark_tasks,
|
| 152 |
+
adopted_at=excluded.adopted_at, notes=excluded.notes, updated_at=excluded.updated_at
|
| 153 |
+
""", (model.name, model.path, model.size_gb, model.role, model.source,
|
| 154 |
+
model.quantization, model.status, model.benchmark_score, model.benchmark_tasks,
|
| 155 |
+
model.adopted_at, model.notes, datetime.now().isoformat()))
|
| 156 |
+
conn.commit()
|
| 157 |
+
conn.close()
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def _get_active_model(role: str) -> Optional[dict]:
|
| 161 |
+
"""Get the currently active model for a role."""
|
| 162 |
+
conn = sqlite3.connect(DB_PATH)
|
| 163 |
+
conn.row_factory = sqlite3.Row
|
| 164 |
+
c = conn.cursor()
|
| 165 |
+
c.execute("SELECT * FROM model_registry WHERE role=? AND status='active'", (role,))
|
| 166 |
+
row = c.fetchone()
|
| 167 |
+
conn.close()
|
| 168 |
+
return dict(row) if row else None
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def _get_all_models(role: str = None) -> list:
|
| 172 |
+
"""Get all registered models, optionally filtered by role."""
|
| 173 |
+
conn = sqlite3.connect(DB_PATH)
|
| 174 |
+
conn.row_factory = sqlite3.Row
|
| 175 |
+
c = conn.cursor()
|
| 176 |
+
if role:
|
| 177 |
+
c.execute("SELECT * FROM model_registry WHERE role=? ORDER BY benchmark_score DESC", (role,))
|
| 178 |
+
else:
|
| 179 |
+
c.execute("SELECT * FROM model_registry ORDER BY role, benchmark_score DESC")
|
| 180 |
+
rows = c.fetchall()
|
| 181 |
+
conn.close()
|
| 182 |
+
return [dict(r) for r in rows]
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
# ββ Hardware detection ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 186 |
+
|
| 187 |
+
def detect_hardware() -> dict:
|
| 188 |
+
"""Detect available hardware for model sizing decisions."""
|
| 189 |
+
hw = {
|
| 190 |
+
"ram_gb": MAX_RAM_GB,
|
| 191 |
+
"vram_gb": MAX_VRAM_GB,
|
| 192 |
+
"gpu_available": MAX_VRAM_GB > 0 or GPU_LAYERS != 0,
|
| 193 |
+
"gpu_layers": GPU_LAYERS,
|
| 194 |
+
"max_model_gb": 0.0,
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
# Try to detect GPU via torch
|
| 198 |
+
try:
|
| 199 |
+
import torch
|
| 200 |
+
if torch.cuda.is_available():
|
| 201 |
+
vram = torch.cuda.get_device_properties(0).total_memory / (1024**3)
|
| 202 |
+
hw["vram_gb"] = vram
|
| 203 |
+
hw["gpu_available"] = True
|
| 204 |
+
except ImportError:
|
| 205 |
+
pass
|
| 206 |
+
|
| 207 |
+
# Max model size: 80% of available memory (leave room for KV cache, OS)
|
| 208 |
+
if hw["gpu_available"] and hw["vram_gb"] > 0:
|
| 209 |
+
hw["max_model_gb"] = hw["vram_gb"] * 0.8
|
| 210 |
+
else:
|
| 211 |
+
# CPU-only: leave 16GB for OS + apps
|
| 212 |
+
hw["max_model_gb"] = max(0, hw["ram_gb"] - 16) * 0.8
|
| 213 |
+
|
| 214 |
+
if MAX_MODEL_SIZE_GB > 0:
|
| 215 |
+
hw["max_model_gb"] = MAX_MODEL_SIZE_GB
|
| 216 |
+
|
| 217 |
+
return hw
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
# ββ Model discovery βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 221 |
+
|
| 222 |
+
def _parse_model_name(filename: str) -> dict:
|
| 223 |
+
"""Extract model info from GGUF filename."""
|
| 224 |
+
info = {"name": filename, "quantization": "unknown"}
|
| 225 |
+
|
| 226 |
+
# Common quantization patterns
|
| 227 |
+
for q in ["Q8_0", "Q6_K", "Q5_K_M", "Q5_K_S", "Q4_K_M", "Q4_K_S", "Q4_0", "Q3_K_M", "Q3_K_S", "Q2_K", "IQ4_XS"]:
|
| 228 |
+
if q in filename:
|
| 229 |
+
info["quantization"] = q
|
| 230 |
+
break
|
| 231 |
+
|
| 232 |
+
# Guess role from name
|
| 233 |
+
name_lower = filename.lower()
|
| 234 |
+
if any(kw in name_lower for kw in ["coder", "code", "starcoder", "codellama", "deepseek-coder"]):
|
| 235 |
+
info["role"] = "coder"
|
| 236 |
+
elif any(kw in name_lower for kw in ["reason", "r1", "think", "opus", "deepseek-r1"]):
|
| 237 |
+
info["role"] = "reasoner"
|
| 238 |
+
else:
|
| 239 |
+
info["role"] = "unknown"
|
| 240 |
+
|
| 241 |
+
info["name"] = filename.replace(".gguf", "")
|
| 242 |
+
return info
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def scan_local_models() -> list:
|
| 246 |
+
"""Scan local directories for GGUF models."""
|
| 247 |
+
found = []
|
| 248 |
+
scan_dirs = [MODEL_DIR]
|
| 249 |
+
|
| 250 |
+
# Also check LM Studio cache
|
| 251 |
+
if os.path.isdir(LMSTUDIO_CACHE):
|
| 252 |
+
scan_dirs.append(LMSTUDIO_CACHE)
|
| 253 |
+
|
| 254 |
+
for scan_dir in scan_dirs:
|
| 255 |
+
if not os.path.isdir(scan_dir):
|
| 256 |
+
continue
|
| 257 |
+
source = "lmstudio_cache" if "lm-studio" in scan_dir else "local"
|
| 258 |
+
for root, dirs, files in os.walk(scan_dir):
|
| 259 |
+
for f in files:
|
| 260 |
+
if f.endswith(".gguf"):
|
| 261 |
+
full_path = os.path.join(root, f)
|
| 262 |
+
size_gb = os.path.getsize(full_path) / (1024**3)
|
| 263 |
+
info = _parse_model_name(f)
|
| 264 |
+
model = ModelInfo(
|
| 265 |
+
name=info["name"],
|
| 266 |
+
path=full_path,
|
| 267 |
+
size_gb=round(size_gb, 2),
|
| 268 |
+
role=info.get("role", "unknown"),
|
| 269 |
+
source=source,
|
| 270 |
+
quantization=info["quantization"],
|
| 271 |
+
status="candidate",
|
| 272 |
+
)
|
| 273 |
+
found.append(model)
|
| 274 |
+
return found
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def scan_and_register() -> list:
|
| 278 |
+
"""Scan for models and register them in the database."""
|
| 279 |
+
_init_db()
|
| 280 |
+
hw = detect_hardware()
|
| 281 |
+
models = scan_local_models()
|
| 282 |
+
registered = []
|
| 283 |
+
|
| 284 |
+
for m in models:
|
| 285 |
+
# Rule: skip models that won't fit
|
| 286 |
+
if m.size_gb > hw["max_model_gb"]:
|
| 287 |
+
m.notes = f"Too large ({m.size_gb:.1f}GB > {hw['max_model_gb']:.1f}GB limit)"
|
| 288 |
+
m.status = "oversized"
|
| 289 |
+
|
| 290 |
+
_save_model(m)
|
| 291 |
+
registered.append(m)
|
| 292 |
+
|
| 293 |
+
_log_decision(ModelDecision(
|
| 294 |
+
timestamp=datetime.now().isoformat(),
|
| 295 |
+
action="scan",
|
| 296 |
+
model_name="*",
|
| 297 |
+
role="*",
|
| 298 |
+
reasoning=f"Scanned {len(registered)} models. Hardware: {hw['ram_gb']:.0f}GB RAM, {hw['vram_gb']:.0f}GB VRAM, max model {hw['max_model_gb']:.1f}GB",
|
| 299 |
+
score_before=0, score_after=0, approved=True
|
| 300 |
+
))
|
| 301 |
+
|
| 302 |
+
return registered
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
# ββ Benchmarking ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 306 |
+
|
| 307 |
+
def benchmark_model(model_path: str, role: str, task_bank: str = None) -> dict:
|
| 308 |
+
"""Benchmark a model against a task bank for a specific role.
|
| 309 |
+
|
| 310 |
+
Returns dict with score, task_count, avg_confidence, avg_time.
|
| 311 |
+
"""
|
| 312 |
+
from reflection_engine import set_task_mode, reason
|
| 313 |
+
|
| 314 |
+
if task_bank is None:
|
| 315 |
+
if role == "reasoner":
|
| 316 |
+
task_bank = os.path.join(os.path.dirname(__file__), "bellingcat_tasks.json")
|
| 317 |
+
else:
|
| 318 |
+
task_bank = os.path.join(os.path.dirname(__file__), "reasoning_tasks.json")
|
| 319 |
+
|
| 320 |
+
if not os.path.isfile(task_bank):
|
| 321 |
+
return {"error": f"Task bank not found: {task_bank}", "score": 0}
|
| 322 |
+
|
| 323 |
+
with open(task_bank, "r", encoding="utf-8") as f:
|
| 324 |
+
tasks = json.load(f)
|
| 325 |
+
|
| 326 |
+
# Use first N tasks for benchmarking (not full bank β save time)
|
| 327 |
+
bench_tasks = tasks[:MIN_BENCHMARK_TASKS]
|
| 328 |
+
results = []
|
| 329 |
+
total_confidence = 0.0
|
| 330 |
+
high_confidence = 0
|
| 331 |
+
total_time = 0.0
|
| 332 |
+
|
| 333 |
+
set_task_mode(role)
|
| 334 |
+
|
| 335 |
+
for task in bench_tasks:
|
| 336 |
+
prompt = task.get("prompt", task.get("description", ""))
|
| 337 |
+
if not prompt:
|
| 338 |
+
continue
|
| 339 |
+
|
| 340 |
+
start = time.time()
|
| 341 |
+
try:
|
| 342 |
+
plan = reason(prompt)
|
| 343 |
+
elapsed = time.time() - start
|
| 344 |
+
conf = getattr(plan, "confidence", 0.3)
|
| 345 |
+
total_confidence += conf
|
| 346 |
+
if conf >= 0.7:
|
| 347 |
+
high_confidence += 1
|
| 348 |
+
total_time += elapsed
|
| 349 |
+
results.append({
|
| 350 |
+
"task": task.get("name", "unnamed"),
|
| 351 |
+
"confidence": conf,
|
| 352 |
+
"time_secs": round(elapsed, 1),
|
| 353 |
+
"parsed": conf > 0.3, # Did it actually reason or fallback?
|
| 354 |
+
})
|
| 355 |
+
except Exception as e:
|
| 356 |
+
elapsed = time.time() - start
|
| 357 |
+
total_time += elapsed
|
| 358 |
+
results.append({
|
| 359 |
+
"task": task.get("name", "unnamed"),
|
| 360 |
+
"confidence": 0.0,
|
| 361 |
+
"time_secs": round(elapsed, 1),
|
| 362 |
+
"error": str(e),
|
| 363 |
+
})
|
| 364 |
+
|
| 365 |
+
n = len(results)
|
| 366 |
+
return {
|
| 367 |
+
"model_path": model_path,
|
| 368 |
+
"role": role,
|
| 369 |
+
"task_bank": task_bank,
|
| 370 |
+
"task_count": n,
|
| 371 |
+
"high_confidence": high_confidence,
|
| 372 |
+
"avg_confidence": round(total_confidence / n, 3) if n > 0 else 0,
|
| 373 |
+
"avg_time_secs": round(total_time / n, 1) if n > 0 else 0,
|
| 374 |
+
"total_time_secs": round(total_time, 1),
|
| 375 |
+
"score": round(high_confidence / n, 3) if n > 0 else 0,
|
| 376 |
+
"results": results,
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
# ββ Adoption rules engine ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 381 |
+
|
| 382 |
+
def evaluate_adoption(candidate_name: str, role: str, benchmark_result: dict) -> ModelDecision:
|
| 383 |
+
"""Apply rules to decide whether to adopt a candidate model.
|
| 384 |
+
|
| 385 |
+
Rules:
|
| 386 |
+
1. Must have benchmarked at least MIN_BENCHMARK_TASKS tasks
|
| 387 |
+
2. Score must beat current active model by MIN_BENCHMARK_IMPROVEMENT
|
| 388 |
+
3. Model must fit within hardware constraints
|
| 389 |
+
4. Must not be the only model (keep fallback)
|
| 390 |
+
"""
|
| 391 |
+
_init_db()
|
| 392 |
+
current = _get_active_model(role)
|
| 393 |
+
current_score = current["benchmark_score"] if current else 0.0
|
| 394 |
+
candidate_score = benchmark_result.get("score", 0)
|
| 395 |
+
|
| 396 |
+
# Rule 1: enough tasks?
|
| 397 |
+
if benchmark_result.get("task_count", 0) < MIN_BENCHMARK_TASKS:
|
| 398 |
+
return ModelDecision(
|
| 399 |
+
timestamp=datetime.now().isoformat(),
|
| 400 |
+
action="reject", model_name=candidate_name, role=role,
|
| 401 |
+
reasoning=f"Insufficient benchmark tasks: {benchmark_result.get('task_count', 0)} < {MIN_BENCHMARK_TASKS}",
|
| 402 |
+
score_before=current_score, score_after=candidate_score, approved=False
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
# Rule 2: improvement threshold?
|
| 406 |
+
improvement = candidate_score - current_score
|
| 407 |
+
if current_score > 0 and improvement < MIN_BENCHMARK_IMPROVEMENT:
|
| 408 |
+
return ModelDecision(
|
| 409 |
+
timestamp=datetime.now().isoformat(),
|
| 410 |
+
action="reject", model_name=candidate_name, role=role,
|
| 411 |
+
reasoning=f"Improvement too small: {improvement:.3f} < {MIN_BENCHMARK_IMPROVEMENT} threshold. Current: {current_score:.3f}, Candidate: {candidate_score:.3f}",
|
| 412 |
+
score_before=current_score, score_after=candidate_score, approved=False
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
# Rule 3: fits hardware?
|
| 416 |
+
hw = detect_hardware()
|
| 417 |
+
models = _get_all_models()
|
| 418 |
+
candidate_models = [m for m in models if m["name"] == candidate_name]
|
| 419 |
+
if candidate_models:
|
| 420 |
+
candidate_size = candidate_models[0]["size_gb"]
|
| 421 |
+
if candidate_size > hw["max_model_gb"]:
|
| 422 |
+
return ModelDecision(
|
| 423 |
+
timestamp=datetime.now().isoformat(),
|
| 424 |
+
action="reject", model_name=candidate_name, role=role,
|
| 425 |
+
reasoning=f"Model too large: {candidate_size:.1f}GB > {hw['max_model_gb']:.1f}GB hardware limit",
|
| 426 |
+
score_before=current_score, score_after=candidate_score, approved=False
|
| 427 |
+
)
|
| 428 |
+
|
| 429 |
+
# All rules passed β approve adoption
|
| 430 |
+
return ModelDecision(
|
| 431 |
+
timestamp=datetime.now().isoformat(),
|
| 432 |
+
action="adopt", model_name=candidate_name, role=role,
|
| 433 |
+
reasoning=f"Passed all rules. Score: {candidate_score:.3f} vs current {current_score:.3f} (+{improvement:.3f}). Fits hardware.",
|
| 434 |
+
score_before=current_score, score_after=candidate_score, approved=True
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
def adopt_model(model_name: str, role: str, benchmark_result: dict) -> bool:
|
| 439 |
+
"""Adopt a model for a role if it passes all rules.
|
| 440 |
+
|
| 441 |
+
Returns True if adopted, False if rejected.
|
| 442 |
+
"""
|
| 443 |
+
_init_db()
|
| 444 |
+
|
| 445 |
+
# Evaluate
|
| 446 |
+
decision = evaluate_adoption(model_name, role, benchmark_result)
|
| 447 |
+
_log_decision(decision)
|
| 448 |
+
|
| 449 |
+
if not decision.approved:
|
| 450 |
+
print(f" β REJECTED: {decision.reasoning}")
|
| 451 |
+
return False
|
| 452 |
+
|
| 453 |
+
# Retire current active model to fallback (Rule 1: never remove without replacement)
|
| 454 |
+
current = _get_active_model(role)
|
| 455 |
+
if current:
|
| 456 |
+
conn = sqlite3.connect(DB_PATH)
|
| 457 |
+
c = conn.cursor()
|
| 458 |
+
c.execute("UPDATE model_registry SET status='fallback' WHERE name=? AND role=?",
|
| 459 |
+
(current["name"], role))
|
| 460 |
+
conn.commit()
|
| 461 |
+
conn.close()
|
| 462 |
+
print(f" π¦ {current['name']} moved to fallback")
|
| 463 |
+
|
| 464 |
+
# Activate new model
|
| 465 |
+
conn = sqlite3.connect(DB_PATH)
|
| 466 |
+
c = conn.cursor()
|
| 467 |
+
c.execute("""UPDATE model_registry SET status='active', benchmark_score=?, benchmark_tasks=?, adopted_at=?
|
| 468 |
+
WHERE name=? AND role=?""",
|
| 469 |
+
(benchmark_result["score"], benchmark_result["task_count"],
|
| 470 |
+
datetime.now().isoformat(), model_name, role))
|
| 471 |
+
conn.commit()
|
| 472 |
+
conn.close()
|
| 473 |
+
|
| 474 |
+
print(f" β
ADOPTED: {model_name} as {role} (score: {benchmark_result['score']:.3f})")
|
| 475 |
+
return True
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
# ββ Status & history ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 479 |
+
|
| 480 |
+
def print_status():
|
| 481 |
+
"""Print current model stack status."""
|
| 482 |
+
_init_db()
|
| 483 |
+
hw = detect_hardware()
|
| 484 |
+
print("=" * 60)
|
| 485 |
+
print("MODEL MANAGER β Stack Status")
|
| 486 |
+
print("=" * 60)
|
| 487 |
+
print(f" Hardware: {hw['ram_gb']:.0f}GB RAM | {hw['vram_gb']:.0f}GB VRAM | GPU layers: {hw['gpu_layers']}")
|
| 488 |
+
print(f" Max model size: {hw['max_model_gb']:.1f}GB")
|
| 489 |
+
print()
|
| 490 |
+
|
| 491 |
+
for role in ["coder", "reasoner"]:
|
| 492 |
+
models = _get_all_models(role)
|
| 493 |
+
print(f" [{role.upper()}]")
|
| 494 |
+
if not models:
|
| 495 |
+
print(f" (no models registered)")
|
| 496 |
+
for m in models:
|
| 497 |
+
marker = "β" if m["status"] == "active" else " "
|
| 498 |
+
score = f"score={m['benchmark_score']:.3f}" if m["benchmark_score"] > 0 else "not benchmarked"
|
| 499 |
+
print(f" {marker} {m['name']} [{m['status']}] {m['size_gb']:.1f}GB {m['quantization']} ({score})")
|
| 500 |
+
print()
|
| 501 |
+
|
| 502 |
+
# Unknown role models
|
| 503 |
+
unknowns = _get_all_models("unknown")
|
| 504 |
+
if unknowns:
|
| 505 |
+
print(f" [UNCLASSIFIED]")
|
| 506 |
+
for m in unknowns:
|
| 507 |
+
print(f" {m['name']} {m['size_gb']:.1f}GB {m['quantization']} ({m['source']})")
|
| 508 |
+
print()
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
# ββ Autonomous scanning / absorption ββββββββββββββββββββββββββββββββββββββββββ
|
| 512 |
+
|
| 513 |
+
# When to trigger an auto-scan (called from growth_engine after N tasks)
|
| 514 |
+
AUTO_SCAN_INTERVAL = int(os.environ.get("AUTO_SCAN_INTERVAL", "20")) # every N tasks
|
| 515 |
+
|
| 516 |
+
# HuggingFace search filters for model discovery
|
| 517 |
+
HF_SEARCH_TAGS = {
|
| 518 |
+
"coder": ["code", "coder", "codegen", "instruct"],
|
| 519 |
+
"reasoner": ["reasoning", "think", "r1", "cot"],
|
| 520 |
+
}
|
| 521 |
+
HF_QUANT_PREFERENCE = ["Q4_K_M", "Q5_K_M", "Q4_K_S", "Q6_K"] # in preference order
|
| 522 |
+
|
| 523 |
+
|
| 524 |
+
def search_huggingface(role: str, max_results: int = 5) -> list[dict]:
|
| 525 |
+
"""Search HuggingFace for GGUF models that might fit a role.
|
| 526 |
+
|
| 527 |
+
Returns list of dicts with: repo_id, filename, size_gb, quantization.
|
| 528 |
+
Does NOT download β just discovers candidates.
|
| 529 |
+
"""
|
| 530 |
+
results = []
|
| 531 |
+
hw = detect_hardware()
|
| 532 |
+
|
| 533 |
+
try:
|
| 534 |
+
from huggingface_hub import HfApi
|
| 535 |
+
api = HfApi()
|
| 536 |
+
except ImportError:
|
| 537 |
+
print(" [HF] huggingface_hub not installed β skip HF search")
|
| 538 |
+
return results
|
| 539 |
+
|
| 540 |
+
tags = HF_SEARCH_TAGS.get(role, ["instruct"])
|
| 541 |
+
|
| 542 |
+
for tag in tags:
|
| 543 |
+
try:
|
| 544 |
+
models = api.list_models(
|
| 545 |
+
search=f"{tag} GGUF",
|
| 546 |
+
sort="downloads",
|
| 547 |
+
direction=-1,
|
| 548 |
+
limit=10,
|
| 549 |
+
)
|
| 550 |
+
for model in models:
|
| 551 |
+
repo_id = model.id
|
| 552 |
+
# Look for GGUF files we can use
|
| 553 |
+
try:
|
| 554 |
+
siblings = api.list_repo_tree(repo_id, recursive=False)
|
| 555 |
+
for item in siblings:
|
| 556 |
+
if not hasattr(item, "rfilename"):
|
| 557 |
+
continue
|
| 558 |
+
fname = item.rfilename
|
| 559 |
+
if not fname.endswith(".gguf"):
|
| 560 |
+
continue
|
| 561 |
+
# Check if it's a quantization we want
|
| 562 |
+
quant = "unknown"
|
| 563 |
+
for q in HF_QUANT_PREFERENCE:
|
| 564 |
+
if q in fname:
|
| 565 |
+
quant = q
|
| 566 |
+
break
|
| 567 |
+
if quant == "unknown":
|
| 568 |
+
continue # skip unrecognized quants
|
| 569 |
+
|
| 570 |
+
size_gb = item.size / (1024**3) if hasattr(item, "size") and item.size else 0
|
| 571 |
+
# Skip if too large for hardware
|
| 572 |
+
if size_gb > hw["max_model_gb"] and size_gb > 0:
|
| 573 |
+
continue
|
| 574 |
+
|
| 575 |
+
results.append({
|
| 576 |
+
"repo_id": repo_id,
|
| 577 |
+
"filename": fname,
|
| 578 |
+
"size_gb": round(size_gb, 2),
|
| 579 |
+
"quantization": quant,
|
| 580 |
+
"role": role,
|
| 581 |
+
})
|
| 582 |
+
except Exception:
|
| 583 |
+
continue # skip repos we can't inspect
|
| 584 |
+
except Exception as e:
|
| 585 |
+
print(f" [HF] Search error for tag '{tag}': {e}")
|
| 586 |
+
continue
|
| 587 |
+
|
| 588 |
+
# Deduplicate and limit
|
| 589 |
+
seen = set()
|
| 590 |
+
unique = []
|
| 591 |
+
for r in results:
|
| 592 |
+
key = f"{r['repo_id']}/{r['filename']}"
|
| 593 |
+
if key not in seen:
|
| 594 |
+
seen.add(key)
|
| 595 |
+
unique.append(r)
|
| 596 |
+
return unique[:max_results]
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
def auto_scan_and_evaluate(roles: list[str] = None) -> dict:
|
| 600 |
+
"""Autonomous model management: scan local β check HF β benchmark β adopt/reject.
|
| 601 |
+
|
| 602 |
+
This is the main entry point for Harvester's self-managed model stack.
|
| 603 |
+
Called by growth_engine every AUTO_SCAN_INTERVAL tasks.
|
| 604 |
+
|
| 605 |
+
Returns dict with scan results and any decisions made.
|
| 606 |
+
"""
|
| 607 |
+
_init_db()
|
| 608 |
+
if roles is None:
|
| 609 |
+
roles = ["coder", "reasoner"]
|
| 610 |
+
|
| 611 |
+
report = {
|
| 612 |
+
"timestamp": datetime.now().isoformat(),
|
| 613 |
+
"local_scanned": 0,
|
| 614 |
+
"hf_discovered": 0,
|
| 615 |
+
"benchmarked": 0,
|
| 616 |
+
"adopted": 0,
|
| 617 |
+
"rejected": 0,
|
| 618 |
+
"decisions": [],
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
# 1. Scan local models
|
| 622 |
+
print("\n π Scanning local model directories...")
|
| 623 |
+
local = scan_and_register()
|
| 624 |
+
report["local_scanned"] = len(local)
|
| 625 |
+
print(f" Found {len(local)} local model(s)")
|
| 626 |
+
|
| 627 |
+
for role in roles:
|
| 628 |
+
current = _get_active_model(role)
|
| 629 |
+
current_score = current["benchmark_score"] if current else 0.0
|
| 630 |
+
|
| 631 |
+
# 2. Check if any local candidates beat current
|
| 632 |
+
candidates = [m for m in _get_all_models(role)
|
| 633 |
+
if m["status"] == "candidate" and m["name"] != (current["name"] if current else "")]
|
| 634 |
+
|
| 635 |
+
if candidates:
|
| 636 |
+
print(f"\n π {len(candidates)} candidate(s) for {role}:")
|
| 637 |
+
for c in candidates[:3]: # limit to top 3 by size
|
| 638 |
+
print(f" {c['name']} ({c['size_gb']:.1f}GB)")
|
| 639 |
+
# Benchmark only if not already benchmarked
|
| 640 |
+
if c["benchmark_score"] == 0 and c["benchmark_tasks"] == 0:
|
| 641 |
+
print(f" β³ Benchmarking {c['name']}...")
|
| 642 |
+
try:
|
| 643 |
+
bench = benchmark_model(c["path"], role)
|
| 644 |
+
report["benchmarked"] += 1
|
| 645 |
+
# Try to adopt
|
| 646 |
+
adopted = adopt_model(c["name"], role, bench)
|
| 647 |
+
if adopted:
|
| 648 |
+
report["adopted"] += 1
|
| 649 |
+
report["decisions"].append(f"Adopted {c['name']} for {role}")
|
| 650 |
+
else:
|
| 651 |
+
report["rejected"] += 1
|
| 652 |
+
report["decisions"].append(f"Rejected {c['name']} for {role}")
|
| 653 |
+
except Exception as e:
|
| 654 |
+
print(f" [ERROR] Benchmark failed: {e}")
|
| 655 |
+
report["decisions"].append(f"Benchmark error for {c['name']}: {e}")
|
| 656 |
+
|
| 657 |
+
# 3. Search HuggingFace (only if no local candidates or current is weak)
|
| 658 |
+
if not candidates or current_score < 0.5:
|
| 659 |
+
print(f"\n π Searching HuggingFace for {role} models...")
|
| 660 |
+
try:
|
| 661 |
+
hf_models = search_huggingface(role, max_results=3)
|
| 662 |
+
report["hf_discovered"] += len(hf_models)
|
| 663 |
+
for hf in hf_models:
|
| 664 |
+
print(f" π¦ {hf['repo_id']}/{hf['filename']} ({hf['size_gb']:.1f}GB, {hf['quantization']})")
|
| 665 |
+
if hf_models:
|
| 666 |
+
print(f" βΉοΈ Use --adopt to evaluate and adopt HF models")
|
| 667 |
+
except Exception as e:
|
| 668 |
+
print(f" [HF] Search error: {e}")
|
| 669 |
+
|
| 670 |
+
_log_decision(ModelDecision(
|
| 671 |
+
timestamp=datetime.now().isoformat(),
|
| 672 |
+
action="auto_scan",
|
| 673 |
+
model_name="*",
|
| 674 |
+
role=",".join(roles),
|
| 675 |
+
reasoning=f"Auto-scan: {report['local_scanned']} local, {report['hf_discovered']} HF, {report['adopted']} adopted, {report['rejected']} rejected",
|
| 676 |
+
score_before=0, score_after=0, approved=True
|
| 677 |
+
))
|
| 678 |
+
|
| 679 |
+
return report
|
| 680 |
+
|
| 681 |
+
|
| 682 |
+
def should_auto_scan() -> bool:
|
| 683 |
+
"""Check if it's time for an automatic model scan.
|
| 684 |
+
|
| 685 |
+
Returns True if AUTO_SCAN_INTERVAL tasks have passed since last scan.
|
| 686 |
+
Called by growth_engine after each task completes.
|
| 687 |
+
"""
|
| 688 |
+
_init_db()
|
| 689 |
+
conn = sqlite3.connect(DB_PATH)
|
| 690 |
+
c = conn.cursor()
|
| 691 |
+
|
| 692 |
+
# Count tasks since last auto_scan decision
|
| 693 |
+
c.execute("SELECT MAX(id) as last_id FROM model_decisions WHERE action='auto_scan'")
|
| 694 |
+
row = c.fetchone()
|
| 695 |
+
last_scan_id = row[0] if row and row[0] else 0
|
| 696 |
+
|
| 697 |
+
# Count tasks in task_history since then (approximate β uses timestamp ordering)
|
| 698 |
+
c.execute("SELECT COUNT(*) FROM task_history WHERE id > ?", (last_scan_id,))
|
| 699 |
+
tasks_since = c.fetchone()[0]
|
| 700 |
+
conn.close()
|
| 701 |
+
|
| 702 |
+
return tasks_since >= AUTO_SCAN_INTERVAL
|
| 703 |
+
|
| 704 |
+
|
| 705 |
+
def print_history(limit: int = 20):
|
| 706 |
+
"""Print recent model decisions."""
|
| 707 |
+
_init_db()
|
| 708 |
+
conn = sqlite3.connect(DB_PATH)
|
| 709 |
+
conn.row_factory = sqlite3.Row
|
| 710 |
+
c = conn.cursor()
|
| 711 |
+
c.execute("SELECT * FROM model_decisions ORDER BY id DESC LIMIT ?", (limit,))
|
| 712 |
+
rows = c.fetchall()
|
| 713 |
+
conn.close()
|
| 714 |
+
|
| 715 |
+
print("=" * 60)
|
| 716 |
+
print("MODEL MANAGER β Decision History")
|
| 717 |
+
print("=" * 60)
|
| 718 |
+
for r in reversed(rows):
|
| 719 |
+
approved = "β
" if r["approved"] else "β"
|
| 720 |
+
print(f" {r['timestamp'][:19]} | {approved} {r['action']:10} | {r['model_name']}")
|
| 721 |
+
print(f" Role: {r['role']} | {r['reasoning'][:80]}")
|
| 722 |
+
if r["score_before"] > 0 or r["score_after"] > 0:
|
| 723 |
+
print(f" Score: {r['score_before']:.3f} β {r['score_after']:.3f}")
|
| 724 |
+
print()
|
| 725 |
+
|
| 726 |
+
|
| 727 |
+
# ββ CLI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 728 |
+
|
| 729 |
+
def seed_current_stack():
|
| 730 |
+
"""Register the models Harvester already knows about from reflection_engine config."""
|
| 731 |
+
_init_db()
|
| 732 |
+
try:
|
| 733 |
+
from reflection_engine import (
|
| 734 |
+
NATIVE_MODEL_PATH, NATIVE_HF_FILE, NATIVE_HF_REPO,
|
| 735 |
+
REASONER_MODEL_PATH, REASONER_HF_FILE, REASONER_HF_REPO,
|
| 736 |
+
)
|
| 737 |
+
except ImportError:
|
| 738 |
+
print(" β οΈ Could not import reflection_engine β seeding with hardcoded defaults")
|
| 739 |
+
NATIVE_MODEL_PATH = r"D:\va_data\models\Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf"
|
| 740 |
+
NATIVE_HF_FILE = "Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf"
|
| 741 |
+
REASONER_MODEL_PATH = r"D:\va_data\models\Qwen3.5-27B.Q4_K_M.gguf"
|
| 742 |
+
REASONER_HF_FILE = "Qwen3.5-27B.Q4_K_M.gguf"
|
| 743 |
+
|
| 744 |
+
seeded = 0
|
| 745 |
+
for path, role, hf_file in [
|
| 746 |
+
(NATIVE_MODEL_PATH, "coder", NATIVE_HF_FILE),
|
| 747 |
+
(REASONER_MODEL_PATH, "reasoner", REASONER_HF_FILE),
|
| 748 |
+
]:
|
| 749 |
+
name = hf_file.replace(".gguf", "")
|
| 750 |
+
info = _parse_model_name(hf_file)
|
| 751 |
+
size_gb = round(os.path.getsize(path) / (1024**3), 2) if os.path.isfile(path) else 0.0
|
| 752 |
+
status = "active" if os.path.isfile(path) else "configured"
|
| 753 |
+
model = ModelInfo(
|
| 754 |
+
name=name, path=path, size_gb=size_gb, role=role,
|
| 755 |
+
source="local", quantization=info["quantization"],
|
| 756 |
+
status=status, adopted_at=datetime.now().isoformat(),
|
| 757 |
+
notes="Seeded from reflection_engine config"
|
| 758 |
+
)
|
| 759 |
+
_save_model(model)
|
| 760 |
+
on_disk = "β
on disk" if os.path.isfile(path) else "β οΈ NOT on disk"
|
| 761 |
+
print(f" β {name} [{role}] {size_gb:.1f}GB ({on_disk})")
|
| 762 |
+
seeded += 1
|
| 763 |
+
|
| 764 |
+
_log_decision(ModelDecision(
|
| 765 |
+
timestamp=datetime.now().isoformat(), action="seed", model_name="*", role="*",
|
| 766 |
+
reasoning=f"Seeded {seeded} models from reflection_engine config",
|
| 767 |
+
score_before=0, score_after=0, approved=True
|
| 768 |
+
))
|
| 769 |
+
print(f"\n Seeded {seeded} models.")
|
| 770 |
+
|
| 771 |
+
|
| 772 |
+
def main():
|
| 773 |
+
import argparse
|
| 774 |
+
parser = argparse.ArgumentParser(description="Harvester Model Manager")
|
| 775 |
+
parser.add_argument("--scan", action="store_true", help="Scan for local models")
|
| 776 |
+
parser.add_argument("--seed", action="store_true", help="Seed current config models into registry")
|
| 777 |
+
parser.add_argument("--status", action="store_true", help="Show current stack status")
|
| 778 |
+
parser.add_argument("--history", action="store_true", help="Show decision log")
|
| 779 |
+
parser.add_argument("--benchmark", type=str, metavar="ROLE", help="Benchmark current model for role (coder/reasoner)")
|
| 780 |
+
parser.add_argument("--evaluate", type=str, metavar="MODEL", help="Evaluate a candidate model name")
|
| 781 |
+
parser.add_argument("--adopt", nargs=2, metavar=("MODEL", "ROLE"), help="Adopt model for role after benchmarking")
|
| 782 |
+
parser.add_argument("--bank", type=str, help="Task bank JSON for benchmarking")
|
| 783 |
+
|
| 784 |
+
args = parser.parse_args()
|
| 785 |
+
|
| 786 |
+
if args.seed:
|
| 787 |
+
seed_current_stack()
|
| 788 |
+
print()
|
| 789 |
+
print_status()
|
| 790 |
+
|
| 791 |
+
elif args.scan:
|
| 792 |
+
models = scan_and_register()
|
| 793 |
+
print(f"\nScanned {len(models)} models:")
|
| 794 |
+
for m in models:
|
| 795 |
+
flag = "β οΈ " if m.status == "oversized" else " "
|
| 796 |
+
print(f" {flag}{m.name} | {m.size_gb:.1f}GB | {m.quantization} | {m.role} | {m.source}")
|
| 797 |
+
print()
|
| 798 |
+
print_status()
|
| 799 |
+
|
| 800 |
+
elif args.status:
|
| 801 |
+
print_status()
|
| 802 |
+
|
| 803 |
+
elif args.history:
|
| 804 |
+
print_history()
|
| 805 |
+
|
| 806 |
+
elif args.benchmark:
|
| 807 |
+
role = args.benchmark.lower()
|
| 808 |
+
print(f"Benchmarking current {role} model...")
|
| 809 |
+
current = _get_active_model(role)
|
| 810 |
+
if not current:
|
| 811 |
+
print(f" No active {role} model. Run --scan first, then --adopt.")
|
| 812 |
+
return
|
| 813 |
+
result = benchmark_model(current["path"], role, args.bank)
|
| 814 |
+
print(f" Score: {result['score']:.3f} ({result['high_confidence']}/{result['task_count']} high confidence)")
|
| 815 |
+
print(f" Avg time: {result['avg_time_secs']:.1f}s per task")
|
| 816 |
+
# Update registry
|
| 817 |
+
_save_model(ModelInfo(
|
| 818 |
+
name=current["name"], path=current["path"], size_gb=current["size_gb"],
|
| 819 |
+
role=role, source=current["source"], quantization=current["quantization"],
|
| 820 |
+
status="active", benchmark_score=result["score"],
|
| 821 |
+
benchmark_tasks=result["task_count"], adopted_at=current.get("adopted_at"),
|
| 822 |
+
))
|
| 823 |
+
|
| 824 |
+
elif args.adopt:
|
| 825 |
+
model_name, role = args.adopt
|
| 826 |
+
role = role.lower()
|
| 827 |
+
models = _get_all_models()
|
| 828 |
+
candidate = next((m for m in models if m["name"] == model_name), None)
|
| 829 |
+
if not candidate:
|
| 830 |
+
print(f" Model '{model_name}' not in registry. Run --scan first.")
|
| 831 |
+
return
|
| 832 |
+
print(f" Benchmarking {model_name} for {role}...")
|
| 833 |
+
result = benchmark_model(candidate["path"], role, args.bank)
|
| 834 |
+
print(f" Benchmark: {result['score']:.3f} ({result['high_confidence']}/{result['task_count']})")
|
| 835 |
+
adopt_model(model_name, role, result)
|
| 836 |
+
|
| 837 |
+
else:
|
| 838 |
+
parser.print_help()
|
| 839 |
+
|
| 840 |
+
|
| 841 |
+
if __name__ == "__main__":
|
| 842 |
+
main()
|
osint_tasks.json
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"metadata": {
|
| 3 |
+
"description": "OSINT Reasoning Lab β tasks focused on open-source intelligence methodology for locating missing persons. All tasks use publicly available data only. Python automation scripts.",
|
| 4 |
+
"difficulty_scale": "1-5 (1=single-source lookup, 3=multi-source correlation, 5=full investigation pipeline)",
|
| 5 |
+
"purpose": "Train Harvester to reason about OSINT investigation methodology and build automation tools for finding lost children."
|
| 6 |
+
},
|
| 7 |
+
"tasks": [
|
| 8 |
+
{
|
| 9 |
+
"id": 1,
|
| 10 |
+
"name": "EXIF Metadata Extractor",
|
| 11 |
+
"lang": "python",
|
| 12 |
+
"difficulty": 2,
|
| 13 |
+
"category": "image_analysis",
|
| 14 |
+
"prompt": "Write a Python script that extracts all EXIF metadata from an image file (JPEG/PNG). Must extract: GPS coordinates (convert to decimal lat/lon), camera make/model, timestamp, software used, thumbnail if present. Output as structured JSON. Handle images with no EXIF gracefully. Use only Pillow (PIL). Include a function that takes a file path and returns a dict with all extracted fields, plus a convenience function that formats GPS coords as a Google Maps URL."
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"id": 2,
|
| 18 |
+
"name": "Reverse Image Search Automator",
|
| 19 |
+
"lang": "python",
|
| 20 |
+
"difficulty": 3,
|
| 21 |
+
"category": "image_analysis",
|
| 22 |
+
"prompt": "Write a Python module that computes perceptual hashes (pHash, dHash, aHash) for an image, stores them in a local SQLite database for comparison, and provides a search function that finds visually similar images from the database within a configurable Hamming distance threshold. Must support: adding images to the index, querying by image file, returning similarity scores, and batch indexing a directory of images. Use Pillow for image processing. Include a CLI interface that supports 'index', 'search', and 'stats' subcommands."
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"id": 3,
|
| 26 |
+
"name": "Username OSINT Enumerator",
|
| 27 |
+
"lang": "python",
|
| 28 |
+
"difficulty": 2,
|
| 29 |
+
"category": "social_media",
|
| 30 |
+
"prompt": "Write a Python script that takes a username string and checks if that username exists on common public platforms by making HTTP HEAD/GET requests to known profile URL patterns (e.g., github.com/{user}, twitter.com/{user}, reddit.com/user/{user}). Must handle: rate limiting with configurable delays, timeout per request, HTTP status code interpretation (200=found, 404=not found, 403=blocked), output results as JSON with platform name, URL, status, and response time. Use requests library with proper User-Agent header. Support a configurable platform list loaded from a JSON config file."
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"id": 4,
|
| 34 |
+
"name": "Public Records Timeline Builder",
|
| 35 |
+
"lang": "python",
|
| 36 |
+
"difficulty": 3,
|
| 37 |
+
"category": "timeline",
|
| 38 |
+
"prompt": "Write a Python module that builds a chronological timeline from multiple data points. Each data point has: timestamp (various formats β ISO, US date, Unix epoch, relative like '3 days ago'), source name, event description, confidence level (high/medium/low), and optional location. Must parse all timestamp formats into UTC datetime objects, sort chronologically, detect gaps longer than a configurable threshold, identify overlapping events, and export the timeline as both JSON and a formatted text report. Include functions for: add_event(), merge_timelines(), find_gaps(), and export()."
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"id": 5,
|
| 42 |
+
"name": "Geolocation from Coordinates",
|
| 43 |
+
"lang": "python",
|
| 44 |
+
"difficulty": 2,
|
| 45 |
+
"category": "geolocation",
|
| 46 |
+
"prompt": "Write a Python module for working with geographic coordinates in OSINT investigations. Must support: converting between DMS (degrees/minutes/seconds) and decimal degrees, calculating distance between two points using the Haversine formula, finding the bounding box (NW/SE corners) for a given center point and radius in km, generating a static map URL for a list of points, and clustering nearby points within a configurable radius using a simple algorithm (no scipy/sklearn). All math should use the math module only. Include type hints and a CLI that accepts lat/lon pairs."
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"id": 6,
|
| 50 |
+
"name": "WHOIS Data Parser",
|
| 51 |
+
"lang": "python",
|
| 52 |
+
"difficulty": 2,
|
| 53 |
+
"category": "domain_recon",
|
| 54 |
+
"prompt": "Write a Python module that parses raw WHOIS text output (passed as a string) and extracts structured fields: registrant name, organization, email, creation date, expiration date, updated date, nameservers, registrar name, and status codes. Handle the most common WHOIS formats (ICANN thin/thick, various registrars). Return results as a dataclass with optional fields (None if not found). Include a function that compares two WHOIS records and returns a diff of changed fields. Use only regex and standard library β no external WHOIS packages."
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"id": 7,
|
| 58 |
+
"name": "Social Media Post Timestamp Analyzer",
|
| 59 |
+
"lang": "python",
|
| 60 |
+
"difficulty": 3,
|
| 61 |
+
"category": "social_media",
|
| 62 |
+
"prompt": "Write a Python module that analyzes posting patterns from a list of timestamps. Input: a list of ISO-8601 datetime strings representing public post times. Must compute: hourly activity histogram (24 bins), day-of-week distribution, most active hour and day, average time between posts, detect regular posting schedules (e.g., 'posts every Tuesday around 3pm'), identify unusual gaps or bursts in activity, and estimate the user's likely timezone based on activity patterns (assume sleeping hours are 1am-6am local). Output all analysis as a structured dict. Use only standard library (datetime, collections, statistics)."
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"id": 8,
|
| 66 |
+
"name": "DNS Reconnaissance Tool",
|
| 67 |
+
"lang": "python",
|
| 68 |
+
"difficulty": 3,
|
| 69 |
+
"category": "domain_recon",
|
| 70 |
+
"prompt": "Write a Python DNS reconnaissance module using only the socket standard library. Must support: resolving A, AAAA, MX, and TXT records via socket.getaddrinfo and dns queries, checking for common subdomains from a configurable wordlist (www, mail, ftp, api, dev, staging, admin, etc.), detecting wildcard DNS (resolve a random subdomain and check if it matches), outputting all discovered records as structured JSON, and providing a summary with total records found per type. Include rate limiting between queries and timeout handling. No external DNS libraries β socket and struct only."
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"id": 9,
|
| 74 |
+
"name": "Missing Person Report Generator",
|
| 75 |
+
"lang": "python",
|
| 76 |
+
"difficulty": 4,
|
| 77 |
+
"category": "reporting",
|
| 78 |
+
"prompt": "Write a Python module that generates a structured intelligence report from collected OSINT data points. Input is a dict containing: person description (name, age, last_seen_date, last_seen_location), a list of sighting data points (timestamp, location, source, confidence), a list of associated online accounts (platform, username, last_active), and timeline events. The module must: validate all input data, generate a formatted text report with sections (Subject Info, Timeline, Online Presence, Sighting Analysis, Recommended Next Steps), calculate a 'data freshness' score based on how recent the data points are, identify geographic patterns in sightings, and flag any data inconsistencies. Output as both plain text and structured JSON."
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"id": 10,
|
| 82 |
+
"name": "Alert Monitor Framework",
|
| 83 |
+
"lang": "python",
|
| 84 |
+
"difficulty": 4,
|
| 85 |
+
"category": "monitoring",
|
| 86 |
+
"prompt": "Write a Python framework for monitoring public data feeds for keyword matches. Must support: configurable keyword lists with boolean logic (AND, OR, NOT), case-insensitive and regex-capable matching, a pluggable source interface (abstract base class for feed sources), a SQLite-backed alert log with deduplication (same content from same source within configurable window = skip), configurable check intervals per source, and alert callbacks (print, log to file, or call a function). Include one concrete source implementation: an RSS feed reader using only urllib and xml.etree. The framework should be runnable as a long-lived process with graceful shutdown on SIGINT."
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"id": 11,
|
| 90 |
+
"name": "Data Correlation Engine",
|
| 91 |
+
"lang": "python",
|
| 92 |
+
"difficulty": 5,
|
| 93 |
+
"category": "analysis",
|
| 94 |
+
"prompt": "Write a Python module that correlates entities across multiple data sources. Entities are dicts with fields like name, email, phone, username, location, and timestamp. The engine must: perform fuzzy name matching (handle typos, nicknames, abbreviations using Levenshtein distance β implement from scratch, no external libs), match entities by shared identifiers (exact email, phone normalization), score entity similarity on a 0-1 scale based on matching fields, merge correlated entities into unified profiles with provenance tracking (which source contributed each field), detect conflicts (same field, different values) and flag them, and output a list of unified profiles with confidence scores. Use only standard library plus basic string operations."
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
"id": 12,
|
| 98 |
+
"name": "Location History Heatmap Builder",
|
| 99 |
+
"lang": "python",
|
| 100 |
+
"difficulty": 3,
|
| 101 |
+
"category": "geolocation",
|
| 102 |
+
"prompt": "Write a Python module that takes a list of location data points (lat, lon, timestamp, source) and produces analysis for a heatmap. Must: cluster nearby points using a grid-based approach (configurable cell size in meters), count visits per cell, identify the top N most-visited locations, calculate time spent at each cluster (based on consecutive timestamps within the cluster), detect travel patterns (sequences of locations ordered by time), estimate travel speed between consecutive points and flag impossible speeds (teleportation detection > 900 km/h), and output the grid data as a JSON structure with cell coordinates, visit counts, and time-spent estimates. Use only math and standard library."
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
"id": 13,
|
| 106 |
+
"name": "Text Entity Extractor",
|
| 107 |
+
"lang": "python",
|
| 108 |
+
"difficulty": 3,
|
| 109 |
+
"category": "analysis",
|
| 110 |
+
"prompt": "Write a Python module that extracts structured entities from unstructured text using regex patterns. Must extract: email addresses, phone numbers (US and international formats), URLs, physical addresses (street number + street name patterns), dates in multiple formats (MM/DD/YYYY, YYYY-MM-DD, Month Day Year, relative dates), @mentions and #hashtags, and IP addresses (v4). Each extracted entity should include: the matched text, entity type, position in the original text (start/end index), and a confidence score based on pattern specificity. Return results grouped by entity type. Use only the re module β no NLP libraries."
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"id": 14,
|
| 114 |
+
"name": "Secure Evidence Hasher",
|
| 115 |
+
"lang": "python",
|
| 116 |
+
"difficulty": 2,
|
| 117 |
+
"category": "reporting",
|
| 118 |
+
"prompt": "Write a Python module for chain-of-custody evidence handling in digital investigations. Must: compute SHA-256 and MD5 hashes of files (streaming, handle files of any size), generate a timestamped evidence receipt (JSON) with file name, size, hashes, collection timestamp, collector name, and case ID, verify a file against a previously generated receipt, maintain an append-only evidence log in a SQLite database, and support batch processing of a directory (hash all files, generate manifest). Include integrity verification that detects any modified files. Use only hashlib, sqlite3, and standard library."
|
| 119 |
+
},
|
| 120 |
+
{
|
| 121 |
+
"id": 15,
|
| 122 |
+
"name": "Investigation Case Manager",
|
| 123 |
+
"lang": "python",
|
| 124 |
+
"difficulty": 5,
|
| 125 |
+
"category": "reporting",
|
| 126 |
+
"prompt": "Write a Python module that manages OSINT investigation cases. A case contains: case ID, subject info, status (open/active/closed), assigned investigators list, a timeline of events, collected evidence records, data source logs, and notes. Must support: creating and updating cases in SQLite, adding timeline events with automatic timestamping, linking evidence (files with hashes) to cases, generating a case summary report, searching across cases by subject name or keyword, tracking investigation hours per investigator, and exporting a complete case as a single JSON file for sharing. All data must be stored in a normalized SQLite schema (cases, events, evidence, investigators, notes tables). Include CLI with subcommands: create, update, add-event, add-evidence, report, search, export."
|
| 127 |
+
}
|
| 128 |
+
]
|
| 129 |
+
}
|
reasoning_lab.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
REASONING LAB
|
| 3 |
+
=============
|
| 4 |
+
Autonomous training ground for Harvester's reasoning engine.
|
| 5 |
+
Feeds hard architectural problems through reason() β run_engine(),
|
| 6 |
+
banking reasoning plans, patterns, and failure lessons.
|
| 7 |
+
|
| 8 |
+
The code follows the reasoning. Not the other way around.
|
| 9 |
+
|
| 10 |
+
Usage:
|
| 11 |
+
python reasoning_lab.py # run all tasks
|
| 12 |
+
python reasoning_lab.py --task 5 # run only task #5
|
| 13 |
+
python reasoning_lab.py --list # list all tasks
|
| 14 |
+
python reasoning_lab.py --reason-only # reason without coding (thinking drills)
|
| 15 |
+
python reasoning_lab.py --difficulty 4 # only tasks with difficulty >= 4
|
| 16 |
+
python reasoning_lab.py --category ai # only tasks in a specific category
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import os
|
| 20 |
+
import sys
|
| 21 |
+
import json
|
| 22 |
+
import time
|
| 23 |
+
import argparse
|
| 24 |
+
|
| 25 |
+
sys.path.insert(0, os.path.dirname(__file__))
|
| 26 |
+
|
| 27 |
+
from growth_engine import run_growth_engine, init_db, show_stats
|
| 28 |
+
from reflection_engine import reason, set_language, set_task_mode, ReasoningPlan
|
| 29 |
+
|
| 30 |
+
# ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
+
|
| 32 |
+
DEFAULT_TASK_BANK = os.path.join(os.path.dirname(__file__), "reasoning_tasks.json")
|
| 33 |
+
RESULTS_DIR = os.path.join(os.path.dirname(__file__), "lab_results")
|
| 34 |
+
|
| 35 |
+
# ββ Load tasks ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 36 |
+
|
| 37 |
+
def load_tasks(
|
| 38 |
+
task_id: int = None,
|
| 39 |
+
min_difficulty: int = None,
|
| 40 |
+
category: str = None,
|
| 41 |
+
bank_path: str = None
|
| 42 |
+
) -> list[dict]:
|
| 43 |
+
path = bank_path or DEFAULT_TASK_BANK
|
| 44 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 45 |
+
bank = json.load(f)
|
| 46 |
+
|
| 47 |
+
tasks = bank["tasks"] if isinstance(bank, dict) else bank
|
| 48 |
+
|
| 49 |
+
# Normalize field names across bank formats
|
| 50 |
+
for t in tasks:
|
| 51 |
+
if "prompt" not in t and "description" in t:
|
| 52 |
+
t["prompt"] = t["description"]
|
| 53 |
+
if "lang" not in t and "language" in t:
|
| 54 |
+
t["lang"] = t["language"]
|
| 55 |
+
|
| 56 |
+
if task_id is not None:
|
| 57 |
+
tasks = [t for t in tasks if t["id"] == task_id]
|
| 58 |
+
if min_difficulty is not None:
|
| 59 |
+
tasks = [t for t in tasks if t["difficulty"] >= min_difficulty]
|
| 60 |
+
if category is not None:
|
| 61 |
+
tasks = [t for t in tasks if t["category"] == category.lower()]
|
| 62 |
+
|
| 63 |
+
return tasks
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# ββ Reason-only mode ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 67 |
+
|
| 68 |
+
def run_reason_only(tasks: list[dict]) -> list[dict]:
|
| 69 |
+
"""
|
| 70 |
+
Pure thinking drills. No code generation.
|
| 71 |
+
Exercises reason() and banks the plans for review.
|
| 72 |
+
"""
|
| 73 |
+
results = []
|
| 74 |
+
for i, task in enumerate(tasks, 1):
|
| 75 |
+
print(f"\n{'='*60}")
|
| 76 |
+
print(f" REASONING DRILL {i}/{len(tasks)}")
|
| 77 |
+
print(f" [{task['difficulty']}β
] {task['name']}")
|
| 78 |
+
print(f" Category: {task['category']}")
|
| 79 |
+
print(f"{'='*60}")
|
| 80 |
+
|
| 81 |
+
set_language(task["lang"])
|
| 82 |
+
t0 = time.time()
|
| 83 |
+
plan = reason(task["prompt"])
|
| 84 |
+
elapsed = time.time() - t0
|
| 85 |
+
|
| 86 |
+
result = {
|
| 87 |
+
"task_id": task["id"],
|
| 88 |
+
"name": task["name"],
|
| 89 |
+
"difficulty": task["difficulty"],
|
| 90 |
+
"category": task["category"],
|
| 91 |
+
"approach": plan.approach,
|
| 92 |
+
"sub_problems": plan.sub_problems,
|
| 93 |
+
"steps": plan.steps,
|
| 94 |
+
"edge_cases": plan.edge_cases,
|
| 95 |
+
"confidence": plan.confidence,
|
| 96 |
+
"time_secs": round(elapsed, 1)
|
| 97 |
+
}
|
| 98 |
+
results.append(result)
|
| 99 |
+
|
| 100 |
+
print(f"\n β± Reasoning took {elapsed:.1f}s")
|
| 101 |
+
print(f" π Confidence: {plan.confidence:.0%}")
|
| 102 |
+
print(f" π§© Sub-problems: {len(plan.sub_problems)}")
|
| 103 |
+
print(f" π Steps: {len(plan.steps)}")
|
| 104 |
+
print(f" β οΈ Edge cases: {len(plan.edge_cases)}")
|
| 105 |
+
|
| 106 |
+
return results
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# ββ Full lab mode βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 110 |
+
|
| 111 |
+
def run_full_lab(tasks: list[dict]) -> list[dict]:
|
| 112 |
+
"""
|
| 113 |
+
Full pipeline: reason β generate β reflect β score β learn.
|
| 114 |
+
Every run makes the engine permanently smarter.
|
| 115 |
+
"""
|
| 116 |
+
results = []
|
| 117 |
+
for i, task in enumerate(tasks, 1):
|
| 118 |
+
print(f"\n{'='*60}")
|
| 119 |
+
print(f" REASONING LAB β TASK {i}/{len(tasks)}")
|
| 120 |
+
print(f" [{task['difficulty']}β
] {task['name']}")
|
| 121 |
+
print(f" Category: {task['category']}")
|
| 122 |
+
print(f"{'='*60}")
|
| 123 |
+
|
| 124 |
+
t0 = time.time()
|
| 125 |
+
attempt = run_growth_engine(task["prompt"], lang=task["lang"])
|
| 126 |
+
elapsed = time.time() - t0
|
| 127 |
+
|
| 128 |
+
result = {
|
| 129 |
+
"task_id": task["id"],
|
| 130 |
+
"name": task["name"],
|
| 131 |
+
"difficulty": task["difficulty"],
|
| 132 |
+
"category": task["category"],
|
| 133 |
+
"score": attempt.score,
|
| 134 |
+
"passed": attempt.passed,
|
| 135 |
+
"iterations": attempt.iteration,
|
| 136 |
+
"time_secs": round(elapsed, 1),
|
| 137 |
+
"reflections": attempt.reflections[:5],
|
| 138 |
+
"errors": attempt.errors[:3]
|
| 139 |
+
}
|
| 140 |
+
results.append(result)
|
| 141 |
+
|
| 142 |
+
status = "PASS" if attempt.passed else "FAIL"
|
| 143 |
+
print(f"\n {'='*40}")
|
| 144 |
+
print(f" {status} β Score: {attempt.score:.1f}% in {attempt.iteration} iterations ({elapsed:.0f}s)")
|
| 145 |
+
print(f" {'='*40}")
|
| 146 |
+
|
| 147 |
+
return results
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
# ββ Report ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 151 |
+
|
| 152 |
+
def save_report(results: list[dict], mode: str):
|
| 153 |
+
os.makedirs(RESULTS_DIR, exist_ok=True)
|
| 154 |
+
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
| 155 |
+
filename = f"lab_{mode}_{timestamp}.json"
|
| 156 |
+
filepath = os.path.join(RESULTS_DIR, filename)
|
| 157 |
+
|
| 158 |
+
report = {
|
| 159 |
+
"mode": mode,
|
| 160 |
+
"timestamp": timestamp,
|
| 161 |
+
"task_count": len(results),
|
| 162 |
+
"results": results
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
# Add summary stats
|
| 166 |
+
if mode == "full":
|
| 167 |
+
scores = [r["score"] for r in results]
|
| 168 |
+
passed = sum(1 for r in results if r["passed"])
|
| 169 |
+
report["summary"] = {
|
| 170 |
+
"passed": passed,
|
| 171 |
+
"failed": len(results) - passed,
|
| 172 |
+
"avg_score": round(sum(scores) / len(scores), 1) if scores else 0,
|
| 173 |
+
"best_score": max(scores) if scores else 0,
|
| 174 |
+
"worst_score": min(scores) if scores else 0,
|
| 175 |
+
"total_time": round(sum(r["time_secs"] for r in results), 1)
|
| 176 |
+
}
|
| 177 |
+
elif mode == "reason":
|
| 178 |
+
confs = [r["confidence"] for r in results]
|
| 179 |
+
report["summary"] = {
|
| 180 |
+
"avg_confidence": round(sum(confs) / len(confs), 2) if confs else 0,
|
| 181 |
+
"high_confidence": sum(1 for c in confs if c >= 0.7),
|
| 182 |
+
"low_confidence": sum(1 for c in confs if c < 0.5),
|
| 183 |
+
"total_time": round(sum(r["time_secs"] for r in results), 1)
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
with open(filepath, "w", encoding="utf-8") as f:
|
| 187 |
+
json.dump(report, f, indent=2)
|
| 188 |
+
|
| 189 |
+
print(f"\n π Report saved: {filepath}")
|
| 190 |
+
return report
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def print_summary(report: dict):
|
| 194 |
+
s = report.get("summary", {})
|
| 195 |
+
mode = report["mode"]
|
| 196 |
+
print(f"\n{'='*60}")
|
| 197 |
+
print(f" REASONING LAB β SUMMARY ({mode.upper()} MODE)")
|
| 198 |
+
print(f"{'='*60}")
|
| 199 |
+
print(f" Tasks run: {report['task_count']}")
|
| 200 |
+
|
| 201 |
+
if mode == "full":
|
| 202 |
+
print(f" Passed: {s.get('passed', 0)}")
|
| 203 |
+
print(f" Failed: {s.get('failed', 0)}")
|
| 204 |
+
print(f" Avg score: {s.get('avg_score', 0)}%")
|
| 205 |
+
print(f" Best: {s.get('best_score', 0)}%")
|
| 206 |
+
print(f" Worst: {s.get('worst_score', 0)}%")
|
| 207 |
+
elif mode == "reason":
|
| 208 |
+
print(f" Avg confidence: {s.get('avg_confidence', 0):.0%}")
|
| 209 |
+
print(f" High confidence: {s.get('high_confidence', 0)} tasks")
|
| 210 |
+
print(f" Low confidence: {s.get('low_confidence', 0)} tasks")
|
| 211 |
+
|
| 212 |
+
print(f" Total time: {s.get('total_time', 0)}s")
|
| 213 |
+
print(f"{'='*60}")
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
# ββ CLI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 217 |
+
|
| 218 |
+
def main():
|
| 219 |
+
parser = argparse.ArgumentParser(description="Reasoning Lab β train Harvester's thinking")
|
| 220 |
+
parser.add_argument("--task", type=int, help="Run a specific task by ID")
|
| 221 |
+
parser.add_argument("--list", action="store_true", help="List all tasks")
|
| 222 |
+
parser.add_argument("--reason-only", action="store_true", help="Reason without coding")
|
| 223 |
+
parser.add_argument("--difficulty", type=int, help="Only tasks with difficulty >= N")
|
| 224 |
+
parser.add_argument("--category", type=str, help="Only tasks in this category")
|
| 225 |
+
parser.add_argument("--bank", type=str, help="Path to task bank JSON (default: reasoning_tasks.json)")
|
| 226 |
+
args = parser.parse_args()
|
| 227 |
+
|
| 228 |
+
# List mode
|
| 229 |
+
if args.list:
|
| 230 |
+
tasks = load_tasks(bank_path=args.bank)
|
| 231 |
+
print(f"\n REASONING LAB β {len(tasks)} TASKS")
|
| 232 |
+
print(f" {'='*50}")
|
| 233 |
+
for t in tasks:
|
| 234 |
+
print(f" [{t['id']:2d}] [{t['difficulty']}β
] {t['name']:<35} ({t['category']})")
|
| 235 |
+
categories = sorted(set(t["category"] for t in tasks))
|
| 236 |
+
print(f"\n Categories: {', '.join(categories)}")
|
| 237 |
+
return
|
| 238 |
+
|
| 239 |
+
# Init database
|
| 240 |
+
init_db()
|
| 241 |
+
|
| 242 |
+
# Load filtered tasks
|
| 243 |
+
tasks = load_tasks(
|
| 244 |
+
task_id=args.task,
|
| 245 |
+
min_difficulty=args.difficulty,
|
| 246 |
+
category=args.category,
|
| 247 |
+
bank_path=args.bank
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
if not tasks:
|
| 251 |
+
print(" No tasks matched your filters.")
|
| 252 |
+
return
|
| 253 |
+
|
| 254 |
+
print(f"\n REASONING LAB β {len(tasks)} task(s) queued")
|
| 255 |
+
|
| 256 |
+
# Auto-detect task mode from bank name: OSINT/Bellingcat β DeepSeek R1, else β Qwen
|
| 257 |
+
bank_name = os.path.basename(args.bank or "").lower()
|
| 258 |
+
if any(kw in bank_name for kw in ("osint", "bellingcat", "investigation", "missing")):
|
| 259 |
+
set_task_mode("reasoning")
|
| 260 |
+
else:
|
| 261 |
+
set_task_mode("code")
|
| 262 |
+
|
| 263 |
+
# Run
|
| 264 |
+
if args.reason_only:
|
| 265 |
+
results = run_reason_only(tasks)
|
| 266 |
+
report = save_report(results, "reason")
|
| 267 |
+
else:
|
| 268 |
+
results = run_full_lab(tasks)
|
| 269 |
+
report = save_report(results, "full")
|
| 270 |
+
|
| 271 |
+
print_summary(report)
|
| 272 |
+
|
| 273 |
+
# Show growth stats
|
| 274 |
+
print()
|
| 275 |
+
show_stats()
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
if __name__ == "__main__":
|
| 279 |
+
main()
|
reasoning_tasks.json
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"metadata": {
|
| 3 |
+
"description": "Reasoning Lab task bank β hard architectural problems that force decomposition, planning, and edge-case thinking. Each task exercises the reason() pipeline before code generation.",
|
| 4 |
+
"difficulty_scale": "1-5 (1=straightforward, 3=multi-system, 5=architectural nightmare)"
|
| 5 |
+
},
|
| 6 |
+
"tasks": [
|
| 7 |
+
{
|
| 8 |
+
"id": 1,
|
| 9 |
+
"name": "State Machine Framework",
|
| 10 |
+
"lang": "csharp",
|
| 11 |
+
"difficulty": 2,
|
| 12 |
+
"category": "architecture",
|
| 13 |
+
"prompt": "Design and implement a generic hierarchical state machine framework for Unity. Must support: nested sub-states, transition conditions with guards, OnEnter/OnExit/OnUpdate callbacks, state history for undo, and serializable state definitions via ScriptableObject. Namespace: SevenDTD.Core. No MonoBehaviour dependency in the core FSM β it should be pure C# that a MonoBehaviour wrapper can drive."
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"id": 2,
|
| 17 |
+
"name": "Object Pool with Warmup",
|
| 18 |
+
"lang": "csharp",
|
| 19 |
+
"difficulty": 2,
|
| 20 |
+
"category": "performance",
|
| 21 |
+
"prompt": "Build a generic object pool system for Unity that supports: configurable warm-up count, automatic expansion when exhausted, shrink-back to baseline after cooldown period, per-pool statistics (borrows, returns, expansions, peak usage), thread-safe borrow/return for use with Jobs, and IPoolable interface with OnBorrow/OnReturn callbacks. Namespace: SevenDTD.Core. Must work with any Component type via generics."
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"id": 3,
|
| 25 |
+
"name": "Event Bus with Priorities",
|
| 26 |
+
"lang": "csharp",
|
| 27 |
+
"difficulty": 3,
|
| 28 |
+
"category": "architecture",
|
| 29 |
+
"prompt": "Implement a decoupled event bus system for a Unity MMO. Requirements: generic typed events (publish EventType, subscribe to EventType), priority ordering for listeners, one-shot subscriptions that auto-unsubscribe after first fire, event queuing with configurable flush-per-frame limit to avoid spikes, weak references to prevent memory leaks from destroyed GameObjects, and debug mode that logs all events with subscriber counts. Namespace: SevenDTD.Core. Pure C# singleton β no MonoBehaviour."
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"id": 4,
|
| 33 |
+
"name": "Dynamic Loot Table System",
|
| 34 |
+
"lang": "csharp",
|
| 35 |
+
"difficulty": 3,
|
| 36 |
+
"category": "game_systems",
|
| 37 |
+
"prompt": "Build a loot drop system for an MMO. Must support: rarity tiers (Common/Uncommon/Rare/Epic/Legendary) with configurable weight curves, loot tables defined as ScriptableObjects with nested sub-tables, pity system that increases rare drop chance after N drops without one, level-scaled drops where item stats scale to player level, guaranteed drops vs random drops in the same table, and world-event modifiers that temporarily alter drop rates globally. Namespace: SevenDTD.MMO."
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"id": 5,
|
| 41 |
+
"name": "Skill Tree with Prerequisites",
|
| 42 |
+
"lang": "csharp",
|
| 43 |
+
"difficulty": 4,
|
| 44 |
+
"category": "game_systems",
|
| 45 |
+
"prompt": "Architect a skill tree system for an MMO where: skills form a directed acyclic graph with multiple prerequisite paths, each skill has level tiers (1-5) with scaling effects, skills can have cooldowns and resource costs (mana, stamina, rage β polymorphic), passive skills apply permanent stat modifiers, active skills have cast times and can be interrupted, skill synergies give bonuses when specific combinations are unlocked, and the entire tree is serializable for save/load. Namespace: SevenDTD.MMO. Must handle circular dependency detection at edit-time."
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"id": 6,
|
| 49 |
+
"name": "Chunk-Based World Streaming",
|
| 50 |
+
"lang": "csharp",
|
| 51 |
+
"difficulty": 4,
|
| 52 |
+
"category": "architecture",
|
| 53 |
+
"prompt": "Design a chunk-based world streaming system for a networked MMO in Unity. Requirements: world divided into chunks on a 2D grid, chunks load/unload based on player proximity with configurable view distance, async loading using Addressables or Resources.LoadAsync, chunk state persistence (modified blocks, placed objects) to disk, server-authoritative chunk ownership (only server decides what loads), client receives chunk data via Netcode NetworkVariable or custom messages, LOD system where distant chunks use simplified meshes, and neighbor-stitching to prevent seams at chunk borders. Namespace: SevenDTD.MMO."
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"id": 7,
|
| 57 |
+
"name": "Dynamic Economy System",
|
| 58 |
+
"lang": "csharp",
|
| 59 |
+
"difficulty": 5,
|
| 60 |
+
"category": "game_systems",
|
| 61 |
+
"prompt": "Build a player-driven economy system for an MMO. Must handle: item prices that fluctuate based on supply/demand across all connected players, auction house with bidding and buyout, price history tracking (last 24h, 7d, 30d) for UI graphs, anti-manipulation safeguards (detect price spiking, wash trading), NPC vendors with dynamic restock that responds to player purchasing patterns, crafting cost calculation that factors in material market prices, tax system configurable per transaction type, and currency sinks to prevent inflation. Namespace: SevenDTD.MMO. Server-authoritative β all price calculations on server."
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"id": 8,
|
| 65 |
+
"name": "Ability Combo System",
|
| 66 |
+
"lang": "csharp",
|
| 67 |
+
"difficulty": 3,
|
| 68 |
+
"category": "game_systems",
|
| 69 |
+
"prompt": "Implement a combo system for an action MMO where: abilities can chain into follow-up abilities within a timing window, combo trees are data-driven (ScriptableObject definitions), each node in the combo tree has: animation trigger, damage multiplier, stamina cost, hit timing window for next link, combo finishers deal bonus damage based on chain length, input buffering so the next ability queues during current animation, and network-synchronized so other players see the combo animations. Namespace: SevenDTD.MMO. Must work with Unity.Netcode."
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"id": 9,
|
| 73 |
+
"name": "Pathfinding with Dynamic Obstacles",
|
| 74 |
+
"lang": "csharp",
|
| 75 |
+
"difficulty": 4,
|
| 76 |
+
"category": "ai",
|
| 77 |
+
"prompt": "Build an A* pathfinding system for NPCs in a Unity MMO that handles: grid-based navigation with variable node costs, dynamic obstacle avoidance (obstacles that appear/disappear at runtime), path smoothing to remove jagged corners, hierarchical pathfinding (coarse grid for long distances, fine grid near destination), partial path return when full path is impossible (get as close as possible), multi-agent coordination to prevent NPCs from overlapping, and configurable heuristics (Manhattan, Euclidean, Octile). Namespace: SevenDTD.AI. Must not use Unity's NavMesh β pure custom implementation."
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"id": 10,
|
| 81 |
+
"name": "Quest System with Branching",
|
| 82 |
+
"lang": "csharp",
|
| 83 |
+
"difficulty": 5,
|
| 84 |
+
"category": "game_systems",
|
| 85 |
+
"prompt": "Architect a quest system for an MMO that supports: branching quest lines where player choices affect future quest availability, multiple objective types (kill N, collect N, escort NPC, reach location, interact with object, talk to NPC, craft item), objectives that can be AND or OR combined (complete all vs complete any), timed objectives with fail conditions, quest sharing in a party where progress syncs for shared objectives, reputation rewards that unlock new quest givers, quest log with tracking and waypoint markers, and prerequisite chains (must complete quest A before quest B unlocks). Namespace: SevenDTD.MMO. All quest data defined in ScriptableObjects β zero hardcoded quest logic."
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"id": 11,
|
| 89 |
+
"name": "ECS-Style Component Registry",
|
| 90 |
+
"lang": "csharp",
|
| 91 |
+
"difficulty": 4,
|
| 92 |
+
"category": "architecture",
|
| 93 |
+
"prompt": "Implement a lightweight Entity-Component-System pattern in pure C# for Unity (not using Unity DOTS). Requirements: entities are just integer IDs, components are plain structs stored in contiguous arrays per type, systems iterate over entities that have specific component combinations (archetype queries), support add/remove components at runtime with structural change buffering, observer pattern for component lifecycle (OnAdd, OnRemove), serialize/deserialize entire world state to byte array, and a simple query API like world.Query<Position, Velocity>() that returns matching entity sets. Namespace: SevenDTD.Core."
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
"id": 12,
|
| 97 |
+
"name": "Dialogue System with Conditions",
|
| 98 |
+
"lang": "csharp",
|
| 99 |
+
"difficulty": 3,
|
| 100 |
+
"category": "game_systems",
|
| 101 |
+
"prompt": "Build a dialogue system for an MMO where: dialogue trees are ScriptableObject-based with nodes and edges, each dialogue choice can have conditions (check quest state, reputation, inventory items, player level), dialogue can trigger actions (give item, start quest, change reputation, teleport player), support for branching based on player stats or prior choices, localization-ready with string keys instead of hardcoded text, NPC mood/disposition that changes based on player interactions, and a simple runtime API: StartDialogue(npcId), ChooseOption(index), GetCurrentNode(). Namespace: SevenDTD.MMO."
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"id": 13,
|
| 105 |
+
"name": "Replay System",
|
| 106 |
+
"lang": "csharp",
|
| 107 |
+
"difficulty": 5,
|
| 108 |
+
"category": "architecture",
|
| 109 |
+
"prompt": "Design a gameplay replay system for a networked game that: records all player inputs and game state snapshots at configurable intervals, supports playback at variable speed (0.25x to 4x) with pause/seek, uses delta compression β only store what changed between snapshots, handles entity spawn/despawn during replay, can export replay to a binary file and reimport it, supports camera modes during playback (follow player, free cam, overview), and synchronizes replay across multiple spectators via network. Namespace: SevenDTD.Core. Must handle 60+ entities with position/rotation/state."
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
"id": 14,
|
| 113 |
+
"name": "Procedural Dungeon Generator",
|
| 114 |
+
"lang": "csharp",
|
| 115 |
+
"difficulty": 4,
|
| 116 |
+
"category": "procedural",
|
| 117 |
+
"prompt": "Build a procedural dungeon generator that: uses BSP (Binary Space Partitioning) to create room layouts, connects rooms with corridors ensuring full connectivity, places doors between rooms and corridors, supports room templates (boss room, treasure room, trap room) with weighted selection, generates spawn points for enemies scaled by room size and distance from entrance, places loot containers with rarity scaling by depth, ensures a valid path from entrance to boss room exists, and outputs a 2D tile grid that a separate renderer can consume. Namespace: SevenDTD.Procedural. Pure C# β no Unity dependencies in the generator."
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"id": 15,
|
| 121 |
+
"name": "Networked Crafting System",
|
| 122 |
+
"lang": "csharp",
|
| 123 |
+
"difficulty": 3,
|
| 124 |
+
"category": "game_systems",
|
| 125 |
+
"prompt": "Implement a crafting system for a networked MMO. Requirements: recipes defined in ScriptableObjects with ingredient lists and quantities, crafting takes real time with a progress bar (not instant), server validates all crafting requests (client can't cheat), crafting stations β different stations unlock different recipe categories, recipe discovery β players learn recipes by finding recipe items or leveling crafting skill, quality system where higher crafting skill produces higher quality items with better stats, batch crafting β queue multiple of the same recipe, and crafting interruption β taking damage cancels active crafting. Namespace: SevenDTD.MMO. Must use Unity.Netcode RPCs for client-server communication."
|
| 126 |
+
}
|
| 127 |
+
]
|
| 128 |
+
}
|
reflection_engine.py
ADDED
|
@@ -0,0 +1,1156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SELF-REFLECTION ENGINE
|
| 3 |
+
======================
|
| 4 |
+
The brain of the self-improving code AI.
|
| 5 |
+
Uses Ollama locally to write, reflect, and improve Python code
|
| 6 |
+
until it hits a 99.5% benchmark floor.
|
| 7 |
+
|
| 8 |
+
Engineering philosophy:
|
| 9 |
+
|
| 10 |
+
NEVER GIVE UP:
|
| 11 |
+
If a problem is hard, it's a challenge. Not a wall β a ladder.
|
| 12 |
+
When iterations fail, don't accept it. Re-reason with what you learned.
|
| 13 |
+
Escalate strategy. Change the angle of attack. The engine persists
|
| 14 |
+
because the builder persists.
|
| 15 |
+
|
| 16 |
+
RADIAL SLOP:
|
| 17 |
+
There is no perfection in engineering β that's why we target 99.5%, not 100%.
|
| 18 |
+
A bolt threads into a hole because of radial slop: the tiny imperfection
|
| 19 |
+
that makes the interface work. Code is the same β some tolerance is by design.
|
| 20 |
+
Chase diminishing returns and you'll over-engineer or hallucinate problems.
|
| 21 |
+
|
| 22 |
+
WORK > CODE > TALK:
|
| 23 |
+
Three modes of output β know the difference:
|
| 24 |
+
WORK β does the code actually run, pass tests, and solve the task?
|
| 25 |
+
CODE β is the code itself clean, correct, and structured?
|
| 26 |
+
TALK β chatty prose, philosophical rambling, markdown explanations.
|
| 27 |
+
The engine cares about WORK and CODE. TALK is waste.
|
| 28 |
+
|
| 29 |
+
THE LADDER:
|
| 30 |
+
Everything has a logical pattern. When things fail, backtrack from the
|
| 31 |
+
closest point to the break, then work back one rung at a time.
|
| 32 |
+
Follow the steps and nothing can hide. Don't guess β trace.
|
| 33 |
+
|
| 34 |
+
Architecture:
|
| 35 |
+
1. Generate code
|
| 36 |
+
2. Run it / test it
|
| 37 |
+
3. If it BROKE β troubleshoot (ladder: start at the error, walk backwards)
|
| 38 |
+
4. If it ran β score it
|
| 39 |
+
5. If score < 99.5 β reflect on WHY β improve β repeat
|
| 40 |
+
6. If stuck β re-reason with accumulated failure context β new angle of attack
|
| 41 |
+
7. If score >= 99.5 β ship it (good enough IS good enough)
|
| 42 |
+
8. If max iterations β report what was tried and what the next attack vector would be
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
import subprocess
|
| 46 |
+
import tempfile
|
| 47 |
+
import textwrap
|
| 48 |
+
import json
|
| 49 |
+
import time
|
| 50 |
+
import sys
|
| 51 |
+
import re
|
| 52 |
+
import os
|
| 53 |
+
from dataclasses import dataclass, field
|
| 54 |
+
from typing import Optional
|
| 55 |
+
import requests
|
| 56 |
+
from code_whitelist import validate_code
|
| 57 |
+
|
| 58 |
+
# ββ TALK detection ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 59 |
+
|
| 60 |
+
# Patterns that signal conversational / non-code input
|
| 61 |
+
_TALK_PATTERNS = [
|
| 62 |
+
# Greetings
|
| 63 |
+
r"^(hey|hi|hello|yo|sup|howdy|hiya|greetings)\b",
|
| 64 |
+
# Status checks
|
| 65 |
+
r"^are you\b",
|
| 66 |
+
r"^you (are|there|up|awake|alive|with us|listening|around)\b",
|
| 67 |
+
r"^how are you",
|
| 68 |
+
r"^how('?s| is) it going",
|
| 69 |
+
r"^who are you",
|
| 70 |
+
# General knowledge questions (no code signals required)
|
| 71 |
+
r"^what is \w+\??$",
|
| 72 |
+
r"^what are \w+\??$",
|
| 73 |
+
r"^what('?s| is) (up|good|new|your name|happening)",
|
| 74 |
+
r"^what('?s| is) a \w+\??$",
|
| 75 |
+
r"^what('?s| is) an \w+\??$",
|
| 76 |
+
r"^what does \w+ mean\??$",
|
| 77 |
+
r"^(describe|explain|define) \w+\??$",
|
| 78 |
+
r"^tell me about\b",
|
| 79 |
+
r"^do you know (about|what)?\b",
|
| 80 |
+
# Acknowledgments / reactions
|
| 81 |
+
r"^thanks?\b",
|
| 82 |
+
r"^thank you",
|
| 83 |
+
r"^good (morning|afternoon|evening|night|job|one)\b",
|
| 84 |
+
r"^(yes|no|ok|okay|sure|yep|nah|nope|cool|nice|great|awesome|perfect|sounds good)$",
|
| 85 |
+
r"^what can you do",
|
| 86 |
+
r"^(help|help me)$",
|
| 87 |
+
r"^(bye|goodbye|see ya|later|peace|cheers)\b",
|
| 88 |
+
r"^(lol|lmao|haha|ha)\b",
|
| 89 |
+
r"^(please|pls)\b$",
|
| 90 |
+
# System checks
|
| 91 |
+
r"^is (this|that|it) (working|on|alive|running)",
|
| 92 |
+
r"^anybody (there|home|here)",
|
| 93 |
+
r"^can you hear me",
|
| 94 |
+
r"^still (there|alive|awake|with us|running)",
|
| 95 |
+
r"^you still (there|alive|awake|with us)",
|
| 96 |
+
r"^are we good",
|
| 97 |
+
r"^what(')?s crackin",
|
| 98 |
+
# Capability / conversational questions
|
| 99 |
+
r"^(can|could|would|will|do) you\b",
|
| 100 |
+
r"^(is it|are there)\b",
|
| 101 |
+
r"^(should|shall) (i|we)\b",
|
| 102 |
+
r"^do you (like|think|have|want|need|know)\b",
|
| 103 |
+
]
|
| 104 |
+
_TALK_RE = re.compile("|".join(_TALK_PATTERNS), re.IGNORECASE)
|
| 105 |
+
|
| 106 |
+
# Keywords that strongly signal a CODE task even if phrased conversationally
|
| 107 |
+
_CODE_SIGNALS = [
|
| 108 |
+
"function", "class", "import", "def ", "return", "loop",
|
| 109 |
+
"sort", "parse", "api", "endpoint", "database", "file",
|
| 110 |
+
"algorithm", "test", "assert", "calculate", "convert",
|
| 111 |
+
"build", "create", "implement", "write", "generate",
|
| 112 |
+
"fix", "debug", "refactor", "optimize", "deploy",
|
| 113 |
+
]
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def classify_task(task: str) -> str:
|
| 117 |
+
"""
|
| 118 |
+
Classify input as 'WORK' (code task) or 'TALK' (conversation).
|
| 119 |
+
If it looks like conversation and has no code keywords β TALK.
|
| 120 |
+
"""
|
| 121 |
+
stripped = task.strip().rstrip("?!.")
|
| 122 |
+
lower = stripped.lower()
|
| 123 |
+
word_count = len(stripped.split())
|
| 124 |
+
has_code = any(kw in lower for kw in _CODE_SIGNALS)
|
| 125 |
+
|
| 126 |
+
# TALK pattern match with no code keywords β TALK (any length)
|
| 127 |
+
if _TALK_RE.search(stripped) and not has_code:
|
| 128 |
+
return "TALK"
|
| 129 |
+
|
| 130 |
+
# Very short input (β€ 6 words) with no code signals β likely TALK
|
| 131 |
+
if word_count <= 6 and not has_code:
|
| 132 |
+
return "TALK"
|
| 133 |
+
|
| 134 |
+
return "WORK"
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
# ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 138 |
+
|
| 139 |
+
OLLAMA_URL = "http://localhost:11434/api/generate"
|
| 140 |
+
LM_STUDIO_URL = "http://localhost:1234/v1/chat/completions"
|
| 141 |
+
BACKEND = os.environ.get("CODE_ENGINE_BACKEND", "auto") # "ollama", "lmstudio", "native", or "auto"
|
| 142 |
+
MODEL = os.environ.get("CODE_ENGINE_MODEL", "llama3.2")
|
| 143 |
+
LM_STUDIO_MODEL = os.environ.get("LM_STUDIO_MODEL", "")
|
| 144 |
+
BENCHMARK_FLOOR = float(os.environ.get("BENCHMARK_FLOOR", "99.5"))
|
| 145 |
+
MAX_ITERATIONS = int(os.environ.get("MAX_ITERATIONS", "10"))
|
| 146 |
+
TIMEOUT_SECS = 30 # per code-run timeout
|
| 147 |
+
|
| 148 |
+
# ββ Native backend config (no LM Studio / Ollama needed) βββββββββββββββββββββ
|
| 149 |
+
# Qwen β the coder
|
| 150 |
+
NATIVE_HF_REPO = os.environ.get("NATIVE_HF_REPO", "lmstudio-community/Qwen3-Coder-30B-A3B-Instruct-GGUF")
|
| 151 |
+
NATIVE_HF_FILE = os.environ.get("NATIVE_HF_FILE", "Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf")
|
| 152 |
+
NATIVE_MODEL_PATH = os.environ.get("NATIVE_MODEL_PATH", r"D:\va_data\models\Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf")
|
| 153 |
+
# DeepSeek R1 14B β the reasoner (Opus Reasoning 27B when hardware ready)
|
| 154 |
+
REASONER_HF_REPO = os.environ.get("REASONER_HF_REPO", "bartowski/DeepSeek-R1-Distill-Qwen-14B-GGUF")
|
| 155 |
+
REASONER_HF_FILE = os.environ.get("REASONER_HF_FILE", "DeepSeek-R1-Distill-Qwen-14B-Q4_K_M.gguf")
|
| 156 |
+
REASONER_MODEL_PATH = os.environ.get("REASONER_MODEL_PATH", r"D:\va_data\models\DeepSeek-R1-Distill-Qwen-14B-Q4_K_M.gguf")
|
| 157 |
+
# GPU layers to offload (0 = CPU only, -1 = all layers to GPU)
|
| 158 |
+
NATIVE_GPU_LAYERS = int(os.environ.get("NATIVE_GPU_LAYERS", "0"))
|
| 159 |
+
NATIVE_CTX_SIZE = int(os.environ.get("NATIVE_CTX_SIZE", "8192"))
|
| 160 |
+
_native_llm = None # lazy singleton β Qwen (coder)
|
| 161 |
+
_reasoning_llm = None # lazy singleton β DeepSeek R1 (reasoner)
|
| 162 |
+
|
| 163 |
+
# ββ Task mode routing βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 164 |
+
# "code" β Qwen for generation, "reasoning" β DeepSeek R1 for thinking/OSINT
|
| 165 |
+
_TASK_MODE = "code" # default
|
| 166 |
+
|
| 167 |
+
def set_task_mode(mode: str):
|
| 168 |
+
"""Set which model handles native calls. 'code' = Qwen, 'reasoning' = DeepSeek R1."""
|
| 169 |
+
global _TASK_MODE
|
| 170 |
+
mode = mode.lower().strip()
|
| 171 |
+
if mode in ("reasoning", "osint", "bellingcat", "investigation"):
|
| 172 |
+
_TASK_MODE = "reasoning"
|
| 173 |
+
else:
|
| 174 |
+
_TASK_MODE = "code"
|
| 175 |
+
print(f" π§ Task mode: {_TASK_MODE} ({'DeepSeek R1' if _TASK_MODE == 'reasoning' else 'Qwen Coder'})")
|
| 176 |
+
|
| 177 |
+
def get_task_mode() -> str:
|
| 178 |
+
return _TASK_MODE
|
| 179 |
+
|
| 180 |
+
# ββ Data structures βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 181 |
+
|
| 182 |
+
@dataclass
|
| 183 |
+
class CodeAttempt:
|
| 184 |
+
iteration: int
|
| 185 |
+
code: str
|
| 186 |
+
score: float
|
| 187 |
+
passed: bool
|
| 188 |
+
test_output: str
|
| 189 |
+
errors: list[str]
|
| 190 |
+
reflections: list[str] = field(default_factory=list)
|
| 191 |
+
|
| 192 |
+
@dataclass
|
| 193 |
+
class ReflectionResult:
|
| 194 |
+
what_failed: str
|
| 195 |
+
root_cause: str
|
| 196 |
+
improvement_plan: str
|
| 197 |
+
specific_fix: str
|
| 198 |
+
confidence: float # 0-1: how confident the model is in the fix
|
| 199 |
+
|
| 200 |
+
@dataclass
|
| 201 |
+
class TroubleshootResult:
|
| 202 |
+
"""The ladder β start at the break, walk backwards rung by rung."""
|
| 203 |
+
error_line: str # the exact line / traceback that broke
|
| 204 |
+
ladder: list[str] # rungs walked back: each step traced
|
| 205 |
+
root_cause: str # the actual bottom rung β why it broke
|
| 206 |
+
fix: str # minimal code change to un-break it
|
| 207 |
+
confidence: float # 0-1
|
| 208 |
+
|
| 209 |
+
@dataclass
|
| 210 |
+
class ReasoningPlan:
|
| 211 |
+
"""Think before you code. Decompose β plan β then generate."""
|
| 212 |
+
sub_problems: list[str] # task broken into parts
|
| 213 |
+
approach: str # algorithm / data structure / architecture choice
|
| 214 |
+
edge_cases: list[str] # things that could bite you
|
| 215 |
+
steps: list[str] # ordered implementation steps
|
| 216 |
+
confidence: float # 0-1: how clear is the path
|
| 217 |
+
|
| 218 |
+
# ββ Ollama interface ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 219 |
+
|
| 220 |
+
def _resolve_model_path(model_path, hf_repo, hf_file):
|
| 221 |
+
"""Resolve a GGUF path: use local override, check LM Studio cache, or download."""
|
| 222 |
+
if model_path and os.path.isfile(model_path):
|
| 223 |
+
return model_path
|
| 224 |
+
# Check LM Studio cache
|
| 225 |
+
lms_path = os.path.join(
|
| 226 |
+
os.path.expanduser("~"), ".lmstudio", "models",
|
| 227 |
+
hf_repo.replace("/", os.sep), hf_file
|
| 228 |
+
)
|
| 229 |
+
if os.path.isfile(lms_path):
|
| 230 |
+
print(f" β Found local GGUF: {os.path.basename(lms_path)}")
|
| 231 |
+
return lms_path
|
| 232 |
+
# Download from HuggingFace
|
| 233 |
+
print(f" β¬ Downloading {hf_file} from {hf_repo}...")
|
| 234 |
+
from huggingface_hub import hf_hub_download
|
| 235 |
+
path = hf_hub_download(repo_id=hf_repo, filename=hf_file)
|
| 236 |
+
print(f" β Downloaded to: {path}")
|
| 237 |
+
return path
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def _load_native_model():
|
| 241 |
+
"""Load Qwen (coder) natively via llama-cpp-python."""
|
| 242 |
+
global _native_llm
|
| 243 |
+
if _native_llm is not None:
|
| 244 |
+
return _native_llm
|
| 245 |
+
|
| 246 |
+
from llama_cpp import Llama
|
| 247 |
+
model_path = _resolve_model_path(NATIVE_MODEL_PATH, NATIVE_HF_REPO, NATIVE_HF_FILE)
|
| 248 |
+
|
| 249 |
+
print(f" β³ Loading Qwen coder (gpu_layers={NATIVE_GPU_LAYERS}, ctx={NATIVE_CTX_SIZE})...")
|
| 250 |
+
_native_llm = Llama(
|
| 251 |
+
model_path=model_path,
|
| 252 |
+
n_ctx=NATIVE_CTX_SIZE,
|
| 253 |
+
n_gpu_layers=NATIVE_GPU_LAYERS,
|
| 254 |
+
verbose=False
|
| 255 |
+
)
|
| 256 |
+
print(f" β Qwen coder loaded: {os.path.basename(model_path)}")
|
| 257 |
+
return _native_llm
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def _load_reasoning_model():
|
| 261 |
+
"""Load DeepSeek R1 (reasoner) natively via llama-cpp-python."""
|
| 262 |
+
global _reasoning_llm
|
| 263 |
+
if _reasoning_llm is not None:
|
| 264 |
+
return _reasoning_llm
|
| 265 |
+
|
| 266 |
+
from llama_cpp import Llama
|
| 267 |
+
model_path = _resolve_model_path(REASONER_MODEL_PATH, REASONER_HF_REPO, REASONER_HF_FILE)
|
| 268 |
+
|
| 269 |
+
print(f" β³ Loading DeepSeek R1 (gpu_layers={NATIVE_GPU_LAYERS}, ctx={NATIVE_CTX_SIZE})...")
|
| 270 |
+
_reasoning_llm = Llama(
|
| 271 |
+
model_path=model_path,
|
| 272 |
+
n_ctx=NATIVE_CTX_SIZE,
|
| 273 |
+
n_gpu_layers=NATIVE_GPU_LAYERS,
|
| 274 |
+
verbose=False
|
| 275 |
+
)
|
| 276 |
+
print(f" β DeepSeek R1 loaded: {os.path.basename(model_path)}")
|
| 277 |
+
return _reasoning_llm
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def _call_native(prompt: str, system: str = "", temperature: float = 0.3) -> str:
|
| 281 |
+
"""Call the appropriate native model based on task mode.
|
| 282 |
+
reasoning/osint β DeepSeek R1, code β Qwen Coder."""
|
| 283 |
+
if _TASK_MODE == "reasoning":
|
| 284 |
+
llm = _load_reasoning_model()
|
| 285 |
+
model_label = "DeepSeek R1"
|
| 286 |
+
else:
|
| 287 |
+
llm = _load_native_model()
|
| 288 |
+
model_label = "Qwen Coder"
|
| 289 |
+
messages = []
|
| 290 |
+
if system:
|
| 291 |
+
messages.append({"role": "system", "content": system})
|
| 292 |
+
messages.append({"role": "user", "content": prompt})
|
| 293 |
+
|
| 294 |
+
# Trim prompt if it exceeds context budget
|
| 295 |
+
total_chars = sum(len(m["content"]) for m in messages)
|
| 296 |
+
max_chars = NATIVE_CTX_SIZE * 3 # rough char-to-token ratio
|
| 297 |
+
if total_chars > max_chars:
|
| 298 |
+
messages[-1]["content"] = messages[-1]["content"][:max_chars - 500] + "\n[trimmed to fit context]"
|
| 299 |
+
|
| 300 |
+
result = llm.create_chat_completion(
|
| 301 |
+
messages=messages,
|
| 302 |
+
temperature=temperature,
|
| 303 |
+
max_tokens=4096
|
| 304 |
+
)
|
| 305 |
+
text = result["choices"][0]["message"]["content"].strip()
|
| 306 |
+
|
| 307 |
+
# DeepSeek R1 wraps output in <think>...</think> β strip it
|
| 308 |
+
if "<think>" in text:
|
| 309 |
+
text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL).strip()
|
| 310 |
+
|
| 311 |
+
return text
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def _detect_backend() -> str:
|
| 315 |
+
"""Auto-detect which backend is running. Tries LM Studio β Ollama β native."""
|
| 316 |
+
if BACKEND != "auto":
|
| 317 |
+
return BACKEND
|
| 318 |
+
# Try LM Studio first (fastest β already running as a server)
|
| 319 |
+
try:
|
| 320 |
+
r = requests.get("http://localhost:1234/v1/models", timeout=3)
|
| 321 |
+
if r.status_code == 200:
|
| 322 |
+
models = r.json().get("data", [])
|
| 323 |
+
if models:
|
| 324 |
+
global LM_STUDIO_MODEL
|
| 325 |
+
if not LM_STUDIO_MODEL:
|
| 326 |
+
LM_STUDIO_MODEL = models[0].get("id", "")
|
| 327 |
+
print(f" β LM Studio detected (model: {LM_STUDIO_MODEL})")
|
| 328 |
+
return "lmstudio"
|
| 329 |
+
except Exception:
|
| 330 |
+
pass
|
| 331 |
+
# Try Ollama
|
| 332 |
+
try:
|
| 333 |
+
r = requests.get("http://localhost:11434/api/tags", timeout=3)
|
| 334 |
+
if r.status_code == 200:
|
| 335 |
+
print(f" β Ollama detected (model: {MODEL})")
|
| 336 |
+
return "ollama"
|
| 337 |
+
except Exception:
|
| 338 |
+
pass
|
| 339 |
+
# Fall back to native β Harvester is a complete unit
|
| 340 |
+
# Check that at least one model file exists (don't eagerly load into RAM)
|
| 341 |
+
try:
|
| 342 |
+
if _TASK_MODE == "reasoning":
|
| 343 |
+
model_path = _resolve_model_path(REASONER_MODEL_PATH, REASONER_HF_REPO, REASONER_HF_FILE)
|
| 344 |
+
else:
|
| 345 |
+
model_path = _resolve_model_path(NATIVE_MODEL_PATH, HF_REPO, HF_FILE)
|
| 346 |
+
if os.path.isfile(model_path):
|
| 347 |
+
return "native"
|
| 348 |
+
raise FileNotFoundError(f"Model not found: {model_path}")
|
| 349 |
+
except Exception as e:
|
| 350 |
+
print(f"\n[ERROR] Native backend failed: {e}")
|
| 351 |
+
print("\n[ERROR] No backend found. Options:")
|
| 352 |
+
print(" 1. Start LM Studio and load a model")
|
| 353 |
+
print(" 2. Run `ollama serve` then `ollama pull llama3.2`")
|
| 354 |
+
print(" 3. Set NATIVE_MODEL_PATH to a .gguf file (or let it download from HuggingFace)")
|
| 355 |
+
sys.exit(1)
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
def _call_lmstudio(prompt: str, system: str = "", temperature: float = 0.3) -> str:
|
| 359 |
+
"""Call LM Studio OpenAI-compatible endpoint."""
|
| 360 |
+
messages = []
|
| 361 |
+
if system:
|
| 362 |
+
messages.append({"role": "system", "content": system})
|
| 363 |
+
messages.append({"role": "user", "content": prompt})
|
| 364 |
+
payload = {
|
| 365 |
+
"model": LM_STUDIO_MODEL or "local-model",
|
| 366 |
+
"messages": messages,
|
| 367 |
+
"temperature": temperature,
|
| 368 |
+
"max_tokens": 4096,
|
| 369 |
+
"stream": False
|
| 370 |
+
}
|
| 371 |
+
for attempt in range(3):
|
| 372 |
+
try:
|
| 373 |
+
r = requests.post(LM_STUDIO_URL, json=payload, timeout=180)
|
| 374 |
+
except requests.exceptions.ReadTimeout:
|
| 375 |
+
print(f" [LM Studio TIMEOUT] attempt {attempt+1}/3 β trimming prompt")
|
| 376 |
+
messages[-1]["content"] = messages[-1]["content"][:2000] + "\n\n[trimmed β keep response short]"
|
| 377 |
+
if "max_tokens" in payload:
|
| 378 |
+
payload["max_tokens"] = 2048
|
| 379 |
+
continue
|
| 380 |
+
if r.status_code == 200:
|
| 381 |
+
return r.json()["choices"][0]["message"]["content"].strip()
|
| 382 |
+
# Log the error
|
| 383 |
+
err_body = r.text[:500] if r.text else "(empty)"
|
| 384 |
+
print(f" [LM Studio {r.status_code}] attempt {attempt+1}/3: {err_body}")
|
| 385 |
+
if r.status_code >= 500 or r.status_code == 429:
|
| 386 |
+
time.sleep(2 ** attempt)
|
| 387 |
+
continue
|
| 388 |
+
# 400 β context overflow is the #1 cause with small models
|
| 389 |
+
is_context_overflow = "context size" in err_body.lower() or "context length" in err_body.lower()
|
| 390 |
+
if attempt == 0:
|
| 391 |
+
if is_context_overflow:
|
| 392 |
+
# Aggressive trim β cut to 1500 chars, drop system msg, halve max_tokens
|
| 393 |
+
messages[-1]["content"] = messages[-1]["content"][:1500] + "\n[trimmed for context]"
|
| 394 |
+
messages = [m for m in messages if m["role"] != "system"]
|
| 395 |
+
payload["messages"] = messages
|
| 396 |
+
payload["max_tokens"] = 2048
|
| 397 |
+
else:
|
| 398 |
+
messages[-1]["content"] = messages[-1]["content"][:2500] + "\n[trimmed]"
|
| 399 |
+
if "max_tokens" in payload:
|
| 400 |
+
payload["max_tokens"] = 2048
|
| 401 |
+
continue
|
| 402 |
+
if attempt == 1:
|
| 403 |
+
# Nuclear trim β bare minimum prompt
|
| 404 |
+
messages[-1]["content"] = messages[-1]["content"][:1000] + "\n[trimmed hard]"
|
| 405 |
+
messages = [m for m in messages if m["role"] != "system"]
|
| 406 |
+
payload["messages"] = messages
|
| 407 |
+
payload.pop("max_tokens", None)
|
| 408 |
+
continue
|
| 409 |
+
# attempt 2 β strip non-ASCII + minimal prompt as last resort
|
| 410 |
+
content = messages[-1]["content"][:800]
|
| 411 |
+
clean = content.encode('ascii', 'ignore').decode('ascii')
|
| 412 |
+
messages[-1]["content"] = clean
|
| 413 |
+
payload["messages"] = messages
|
| 414 |
+
try:
|
| 415 |
+
r2 = requests.post(LM_STUDIO_URL, json=payload, timeout=180)
|
| 416 |
+
except requests.exceptions.ReadTimeout:
|
| 417 |
+
break
|
| 418 |
+
if r2.status_code == 200:
|
| 419 |
+
return r2.json()["choices"][0]["message"]["content"].strip()
|
| 420 |
+
raise RuntimeError("LM Studio: all attempts failed (400/timeout)")
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def _call_ollama(prompt: str, system: str = "", temperature: float = 0.3) -> str:
|
| 424 |
+
"""Call local Ollama model."""
|
| 425 |
+
payload = {
|
| 426 |
+
"model": MODEL,
|
| 427 |
+
"prompt": prompt,
|
| 428 |
+
"system": system,
|
| 429 |
+
"stream": False,
|
| 430 |
+
"options": {"temperature": temperature}
|
| 431 |
+
}
|
| 432 |
+
r = requests.post(OLLAMA_URL, json=payload, timeout=120)
|
| 433 |
+
r.raise_for_status()
|
| 434 |
+
return r.json().get("response", "").strip()
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
_ACTIVE_BACKEND = None
|
| 438 |
+
|
| 439 |
+
def ollama(prompt: str, system: str = "", temperature: float = 0.3) -> str:
|
| 440 |
+
"""Call local LLM (LM Studio, Ollama, or native). Returns response text."""
|
| 441 |
+
global _ACTIVE_BACKEND
|
| 442 |
+
if _ACTIVE_BACKEND is None:
|
| 443 |
+
_ACTIVE_BACKEND = _detect_backend()
|
| 444 |
+
try:
|
| 445 |
+
if _ACTIVE_BACKEND == "native":
|
| 446 |
+
return _call_native(prompt, system, temperature)
|
| 447 |
+
elif _ACTIVE_BACKEND == "lmstudio":
|
| 448 |
+
return _call_lmstudio(prompt, system, temperature)
|
| 449 |
+
else:
|
| 450 |
+
return _call_ollama(prompt, system, temperature)
|
| 451 |
+
except requests.exceptions.ConnectionError:
|
| 452 |
+
# If a server backend drops, try falling back to native
|
| 453 |
+
if _ACTIVE_BACKEND in ("lmstudio", "ollama"):
|
| 454 |
+
print(f"\n β Lost connection to {_ACTIVE_BACKEND} β falling back to native...")
|
| 455 |
+
try:
|
| 456 |
+
_ACTIVE_BACKEND = "native"
|
| 457 |
+
return _call_native(prompt, system, temperature)
|
| 458 |
+
except Exception:
|
| 459 |
+
pass
|
| 460 |
+
print(f"\n[ERROR] Lost connection to {_ACTIVE_BACKEND}")
|
| 461 |
+
print(" β Make sure your LLM backend is still running")
|
| 462 |
+
sys.exit(1)
|
| 463 |
+
except (requests.exceptions.ReadTimeout, requests.exceptions.Timeout):
|
| 464 |
+
print(f"\n[ERROR] LLM timed out β model may be overloaded")
|
| 465 |
+
print(" β Try a smaller model or restart LM Studio")
|
| 466 |
+
raise RuntimeError("LLM timeout β model too slow for this prompt")
|
| 467 |
+
except requests.exceptions.HTTPError as e:
|
| 468 |
+
print(f"\n[ERROR] LLM returned HTTP error: {e}")
|
| 469 |
+
raise RuntimeError(f"LLM backend error: {e}") from e
|
| 470 |
+
|
| 471 |
+
# ββ Language config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 472 |
+
|
| 473 |
+
# Supported languages and their generation config
|
| 474 |
+
_LANG_CONFIG = {
|
| 475 |
+
"python": {
|
| 476 |
+
"expert": "Python engineer",
|
| 477 |
+
"extension": ".py",
|
| 478 |
+
"fence": "python",
|
| 479 |
+
"system": """You write only clean Python code.
|
| 480 |
+
No markdown. No backticks. No prose. No conversation. No talk.
|
| 481 |
+
Just raw executable Python starting with imports or function definitions.
|
| 482 |
+
Always include test assertions at the bottom of the file.
|
| 483 |
+
Remember: WORK (does it run?) and CODE (is it clean?) are all that matter.
|
| 484 |
+
TALK (explanations, commentary) is forbidden.""",
|
| 485 |
+
"executable": True,
|
| 486 |
+
},
|
| 487 |
+
"csharp": {
|
| 488 |
+
"expert": "Unity C# engineer (Unity 6, URP, Netcode for GameObjects)",
|
| 489 |
+
"extension": ".cs",
|
| 490 |
+
"fence": "csharp",
|
| 491 |
+
"system": """You write only clean Unity C# code.
|
| 492 |
+
No markdown. No backticks. No prose. No conversation. No talk.
|
| 493 |
+
Just raw C# starting with using directives.
|
| 494 |
+
Use Unity 6 APIs and Netcode for GameObjects where networking is needed.
|
| 495 |
+
Remember: WORK (does it compile and function?) and CODE (is it clean?) matter.
|
| 496 |
+
TALK (explanations, commentary) is forbidden.""",
|
| 497 |
+
"executable": False, # no C# compiler in the loop β review-scored
|
| 498 |
+
},
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
# Active language for this run (set via set_language())
|
| 502 |
+
_ACTIVE_LANG = "python"
|
| 503 |
+
|
| 504 |
+
def set_language(lang: str):
|
| 505 |
+
"""Set the active language for code generation."""
|
| 506 |
+
global _ACTIVE_LANG
|
| 507 |
+
lang = lang.lower().strip()
|
| 508 |
+
if lang in ("cs", "c#", "unity"):
|
| 509 |
+
lang = "csharp"
|
| 510 |
+
if lang not in _LANG_CONFIG:
|
| 511 |
+
print(f" [WARN] Unknown language '{lang}' β defaulting to python")
|
| 512 |
+
lang = "python"
|
| 513 |
+
_ACTIVE_LANG = lang
|
| 514 |
+
print(f" Language: {_ACTIVE_LANG} ({_LANG_CONFIG[lang]['expert']})")
|
| 515 |
+
|
| 516 |
+
def get_language() -> str:
|
| 517 |
+
return _ACTIVE_LANG
|
| 518 |
+
|
| 519 |
+
# ββ Code generation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 520 |
+
|
| 521 |
+
def generate_code(task: str, previous_attempt: Optional[CodeAttempt] = None,
|
| 522 |
+
reflection: Optional[ReflectionResult] = None,
|
| 523 |
+
reasoning: Optional[ReasoningPlan] = None) -> str:
|
| 524 |
+
"""Generate code for a task in the active language, with optional improvement context."""
|
| 525 |
+
lang = _LANG_CONFIG[_ACTIVE_LANG]
|
| 526 |
+
expert = lang["expert"]
|
| 527 |
+
fence = lang["fence"]
|
| 528 |
+
|
| 529 |
+
if previous_attempt and reflection:
|
| 530 |
+
# Improvement pass β give it specific direction
|
| 531 |
+
clean_task = task.split("\n\n[PRIOR KNOWLEDGE FROM MEMORY]")[0] if "[PRIOR KNOWLEDGE" in task else task
|
| 532 |
+
prev_code = previous_attempt.code
|
| 533 |
+
if len(prev_code) > 800:
|
| 534 |
+
prev_code = prev_code[:300] + "\n// ...[middle trimmed]...\n" + prev_code[-500:]
|
| 535 |
+
prompt = f"""Expert {expert}. Fix this code. Output ONLY working {fence}. No talk. No markdown.
|
| 536 |
+
|
| 537 |
+
TASK: {clean_task}
|
| 538 |
+
|
| 539 |
+
PREVIOUS CODE (scored {previous_attempt.score:.1f}%):
|
| 540 |
+
{prev_code}
|
| 541 |
+
|
| 542 |
+
WHAT FAILED: {reflection.what_failed}
|
| 543 |
+
ROOT CAUSE: {reflection.root_cause}
|
| 544 |
+
FIX: {reflection.specific_fix}
|
| 545 |
+
|
| 546 |
+
Write the complete fixed code. No explanation."""
|
| 547 |
+
|
| 548 |
+
else:
|
| 549 |
+
# First attempt β use reasoning plan if available
|
| 550 |
+
reasoning_block = ""
|
| 551 |
+
if reasoning and reasoning.confidence > 0.2:
|
| 552 |
+
steps_str = "\n".join(f" {j}. {s}" for j, s in enumerate(reasoning.steps, 1))
|
| 553 |
+
edges_str = ", ".join(reasoning.edge_cases[:4]) if reasoning.edge_cases else "none identified"
|
| 554 |
+
reasoning_block = f"""
|
| 555 |
+
|
| 556 |
+
YOUR REASONING PLAN (follow this):
|
| 557 |
+
Approach: {reasoning.approach}
|
| 558 |
+
Steps:
|
| 559 |
+
{steps_str}
|
| 560 |
+
Edge cases to handle: {edges_str}
|
| 561 |
+
|
| 562 |
+
Implement the plan above. Do not deviate unless the plan is clearly wrong."""
|
| 563 |
+
|
| 564 |
+
if _ACTIVE_LANG == "python":
|
| 565 |
+
prompt = f"""You are an expert {expert}. Target: 99.5%% quality β not perfection.
|
| 566 |
+
Radial slop: a bolt threads because it's imperfect. Some tolerance is by design.
|
| 567 |
+
|
| 568 |
+
There are three kinds of output: WORK, CODE, and TALK.
|
| 569 |
+
- WORK = does it run, pass tests, solve the task? This is what matters most.
|
| 570 |
+
- CODE = is it clean, readable, well-structured? This matters second.
|
| 571 |
+
- TALK = prose, explanations, markdown, philosophy. This is WASTE. Zero talk.
|
| 572 |
+
|
| 573 |
+
TASK: {task}{reasoning_block}
|
| 574 |
+
|
| 575 |
+
Write clean, efficient Python code to accomplish this task.
|
| 576 |
+
- Handle common edge cases (don't invent unlikely scenarios)
|
| 577 |
+
- Include sensible error handling β no defensive paranoia
|
| 578 |
+
- Write at least 3 meaningful test cases as assert statements at the bottom
|
| 579 |
+
- Correctness first, then readability, then performance
|
| 580 |
+
|
| 581 |
+
Return ONLY the Python code. No explanation. No markdown fences. No talk."""
|
| 582 |
+
else:
|
| 583 |
+
prompt = f"""You are an expert {expert}. Target: 99.5%% quality β not perfection.
|
| 584 |
+
|
| 585 |
+
There are three kinds of output: WORK, CODE, and TALK.
|
| 586 |
+
- WORK = does it compile and function correctly? This is what matters most.
|
| 587 |
+
- CODE = is it clean, readable, well-structured? This matters second.
|
| 588 |
+
- TALK = prose, explanations, markdown, philosophy. This is WASTE. Zero talk.
|
| 589 |
+
|
| 590 |
+
TASK: {task}{reasoning_block}
|
| 591 |
+
|
| 592 |
+
Write clean, production-quality {fence} code.
|
| 593 |
+
- Handle common edge cases (don't invent unlikely scenarios)
|
| 594 |
+
- Use proper namespace, class structure, and access modifiers
|
| 595 |
+
- Correctness first, then readability, then performance
|
| 596 |
+
|
| 597 |
+
Return ONLY the {fence} code. No explanation. No markdown fences. No talk."""
|
| 598 |
+
|
| 599 |
+
system = lang["system"]
|
| 600 |
+
return ollama(prompt, system=system, temperature=0.2)
|
| 601 |
+
|
| 602 |
+
# ββ Code execution & scoring ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 603 |
+
|
| 604 |
+
def run_code(code: str) -> tuple[bool, str, list[str]]:
|
| 605 |
+
"""
|
| 606 |
+
Execute code in a subprocess sandbox (Python) or review-score it (other languages).
|
| 607 |
+
Returns: (success, output, errors)
|
| 608 |
+
"""
|
| 609 |
+
lang = _ACTIVE_LANG
|
| 610 |
+
lang_cfg = _LANG_CONFIG[lang]
|
| 611 |
+
|
| 612 |
+
# Safety check β validate against multi-language whitelist
|
| 613 |
+
validation = validate_code(code, language=lang)
|
| 614 |
+
if not validation.safe:
|
| 615 |
+
return False, "", [f"BLOCKED: {b}" for b in validation.blocked]
|
| 616 |
+
|
| 617 |
+
# Non-executable languages β LLM review scoring
|
| 618 |
+
if not lang_cfg["executable"]:
|
| 619 |
+
return _review_score_code(code, lang)
|
| 620 |
+
|
| 621 |
+
errors = []
|
| 622 |
+
output = ""
|
| 623 |
+
|
| 624 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py',
|
| 625 |
+
delete=False, encoding='utf-8') as f:
|
| 626 |
+
f.write(code)
|
| 627 |
+
tmp_path = f.name
|
| 628 |
+
|
| 629 |
+
try:
|
| 630 |
+
result = subprocess.run(
|
| 631 |
+
[sys.executable, tmp_path],
|
| 632 |
+
capture_output=True, text=True,
|
| 633 |
+
timeout=TIMEOUT_SECS
|
| 634 |
+
)
|
| 635 |
+
output = result.stdout.strip()
|
| 636 |
+
if result.returncode != 0:
|
| 637 |
+
errors.append(result.stderr.strip())
|
| 638 |
+
success = False
|
| 639 |
+
else:
|
| 640 |
+
success = True
|
| 641 |
+
except subprocess.TimeoutExpired:
|
| 642 |
+
errors.append(f"TIMEOUT: code exceeded {TIMEOUT_SECS}s")
|
| 643 |
+
success = False
|
| 644 |
+
except Exception as e:
|
| 645 |
+
errors.append(str(e))
|
| 646 |
+
success = False
|
| 647 |
+
finally:
|
| 648 |
+
os.unlink(tmp_path)
|
| 649 |
+
|
| 650 |
+
return success, output, errors
|
| 651 |
+
|
| 652 |
+
|
| 653 |
+
def _review_score_code(code: str, lang: str) -> tuple[bool, str, list[str]]:
|
| 654 |
+
"""
|
| 655 |
+
For non-executable languages (C#, etc), use LLM code review as scoring.
|
| 656 |
+
Returns: (success, review_output, errors_found)
|
| 657 |
+
"""
|
| 658 |
+
print(" π Review-scoring (no compiler available)...")
|
| 659 |
+
# Trim code for context budget
|
| 660 |
+
code_snippet = code[:2000] if len(code) > 2000 else code
|
| 661 |
+
prompt = f"""You are a strict code reviewer for {lang}. Score this code 0-100.
|
| 662 |
+
|
| 663 |
+
CODE:
|
| 664 |
+
{code_snippet}
|
| 665 |
+
|
| 666 |
+
Score on:
|
| 667 |
+
1. Does it look like it would compile? (40 points)
|
| 668 |
+
2. Is the logic correct for the stated purpose? (30 points)
|
| 669 |
+
3. Is it clean and well-structured? (20 points)
|
| 670 |
+
4. Edge cases handled? (10 points)
|
| 671 |
+
|
| 672 |
+
Respond ONLY with JSON:
|
| 673 |
+
{{"score": 85, "errors": ["list of issues found"], "notes": "brief summary"}}"""
|
| 674 |
+
raw = ollama(prompt, temperature=0.1)
|
| 675 |
+
try:
|
| 676 |
+
match = re.search(r'\{.*\}', raw, re.DOTALL)
|
| 677 |
+
if match:
|
| 678 |
+
data = json.loads(match.group())
|
| 679 |
+
score = float(data.get("score", 50))
|
| 680 |
+
errors = data.get("errors", [])
|
| 681 |
+
notes = data.get("notes", "")
|
| 682 |
+
success = score >= BENCHMARK_FLOOR
|
| 683 |
+
return success, f"Review score: {score:.0f}% β {notes}", errors if not success else []
|
| 684 |
+
except Exception:
|
| 685 |
+
pass
|
| 686 |
+
return False, "Review parsing failed", ["Could not parse review response"]
|
| 687 |
+
|
| 688 |
+
|
| 689 |
+
def score_code(code: str, task: str, success: bool,
|
| 690 |
+
output: str, errors: list[str]) -> float:
|
| 691 |
+
"""
|
| 692 |
+
WORK-first scoring. Did it run? Did tests pass? That's what matters.
|
| 693 |
+
For non-executable languages, uses the review score from _review_score_code.
|
| 694 |
+
"""
|
| 695 |
+
lang_cfg = _LANG_CONFIG[_ACTIVE_LANG]
|
| 696 |
+
|
| 697 |
+
# Non-executable languages: extract score from review output
|
| 698 |
+
if not lang_cfg["executable"]:
|
| 699 |
+
match = re.search(r'Review score:\s*(\d+)', output)
|
| 700 |
+
if match:
|
| 701 |
+
return float(match.group(1))
|
| 702 |
+
return 50.0 if success else 30.0
|
| 703 |
+
|
| 704 |
+
# Python: execution-based scoring
|
| 705 |
+
if not success:
|
| 706 |
+
if errors and "AssertionError" in str(errors):
|
| 707 |
+
return 35.0 # ran but assertions failed β close
|
| 708 |
+
if errors and "AssertError" in str(errors):
|
| 709 |
+
return 35.0
|
| 710 |
+
return 20.0 # didn't run at all
|
| 711 |
+
|
| 712 |
+
if not errors:
|
| 713 |
+
return 100.0
|
| 714 |
+
|
| 715 |
+
return 85.0
|
| 716 |
+
|
| 717 |
+
# ββ Self-reflection βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 718 |
+
|
| 719 |
+
def reflect(task: str, attempt: CodeAttempt) -> ReflectionResult:
|
| 720 |
+
"""
|
| 721 |
+
The core of the engine.
|
| 722 |
+
Forces the model to diagnose failure before attempting a fix.
|
| 723 |
+
This is what makes it self-improving rather than just retrying.
|
| 724 |
+
"""
|
| 725 |
+
print(f"\n π§ Reflecting on iteration {attempt.iteration} (score: {attempt.score:.1f}%)...")
|
| 726 |
+
expert = _LANG_CONFIG[_ACTIVE_LANG]["expert"]
|
| 727 |
+
fence = _LANG_CONFIG[_ACTIVE_LANG]["fence"]
|
| 728 |
+
|
| 729 |
+
reflection_prompt = f"""You are a senior {expert} conducting a code review.
|
| 730 |
+
|
| 731 |
+
ENGINEERING PRINCIPLES:
|
| 732 |
+
- Never give up. If it's hard, it's a challenge. Change the angle of attack.
|
| 733 |
+
- Radial slop: target 99.5%, not 100%. The tiny tolerance is a feature.
|
| 734 |
+
|
| 735 |
+
Your job is to evaluate TWO things and ONLY two things:
|
| 736 |
+
1. WORK β does the code actually run, pass its tests, and solve the stated task?
|
| 737 |
+
2. CODE β is the code clean, readable, and reasonably efficient?
|
| 738 |
+
|
| 739 |
+
Do NOT produce TALK β no philosophical commentary, no essays about best practices,
|
| 740 |
+
no restating the task. Diagnose real issues in the fewest words possible.
|
| 741 |
+
If the code works and is clean, say so and move on.
|
| 742 |
+
|
| 743 |
+
TASK: {task}
|
| 744 |
+
|
| 745 |
+
CODE (scored {attempt.score:.1f}% β target is 99.5%):
|
| 746 |
+
```{fence}
|
| 747 |
+
{attempt.code}
|
| 748 |
+
```
|
| 749 |
+
|
| 750 |
+
EXECUTION OUTPUT: {attempt.test_output}
|
| 751 |
+
ERRORS: {attempt.errors}
|
| 752 |
+
|
| 753 |
+
Diagnose only REAL issues. Answer with precision:
|
| 754 |
+
|
| 755 |
+
1. WHAT_FAILED: What specifically is broken or missing? (If nothing real, say "minor gap")
|
| 756 |
+
2. ROOT_CAUSE: Why? What assumption was wrong?
|
| 757 |
+
3. IMPROVEMENT_PLAN: Practical steps to reach 99.5% β not 100%. Don't over-engineer.
|
| 758 |
+
4. SPECIFIC_FIX: The single most impactful code change. Keep it minimal.
|
| 759 |
+
5. CONFIDENCE: How confident are you this fix will work? (0.0 to 1.0)
|
| 760 |
+
|
| 761 |
+
Respond ONLY with a JSON object (no other text):
|
| 762 |
+
{{
|
| 763 |
+
"what_failed": "...",
|
| 764 |
+
"root_cause": "...",
|
| 765 |
+
"improvement_plan": "...",
|
| 766 |
+
"specific_fix": "...",
|
| 767 |
+
"confidence": 0.85
|
| 768 |
+
}}"""
|
| 769 |
+
|
| 770 |
+
raw = ollama(reflection_prompt, temperature=0.2)
|
| 771 |
+
|
| 772 |
+
try:
|
| 773 |
+
raw = re.sub(r'```.*?```', '', raw, flags=re.DOTALL).strip()
|
| 774 |
+
# Find JSON block if surrounded by text
|
| 775 |
+
match = re.search(r'\{.*\}', raw, re.DOTALL)
|
| 776 |
+
if match:
|
| 777 |
+
raw = match.group()
|
| 778 |
+
data = json.loads(raw)
|
| 779 |
+
return ReflectionResult(
|
| 780 |
+
what_failed = data.get("what_failed", "Unknown failure"),
|
| 781 |
+
root_cause = data.get("root_cause", "Unknown cause"),
|
| 782 |
+
improvement_plan = data.get("improvement_plan", "Retry"),
|
| 783 |
+
specific_fix = data.get("specific_fix", "Rewrite"),
|
| 784 |
+
confidence = float(data.get("confidence", 0.5))
|
| 785 |
+
)
|
| 786 |
+
except Exception:
|
| 787 |
+
return ReflectionResult(
|
| 788 |
+
what_failed = "Parsing failed β raw output: " + raw[:200],
|
| 789 |
+
root_cause = "Model did not return valid JSON",
|
| 790 |
+
improvement_plan = "Rewrite code from scratch with stricter constraints",
|
| 791 |
+
specific_fix = "Full rewrite",
|
| 792 |
+
confidence = 0.4
|
| 793 |
+
)
|
| 794 |
+
|
| 795 |
+
# ββ Troubleshooter (the ladder) βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 796 |
+
|
| 797 |
+
def troubleshoot(task: str, attempt: CodeAttempt) -> TroubleshootResult:
|
| 798 |
+
"""
|
| 799 |
+
The ladder method: start at the break, walk backwards rung by rung.
|
| 800 |
+
Everything has a logical pattern. Follow the steps, nothing hides.
|
| 801 |
+
|
| 802 |
+
This is NOT reflect(). Reflect is for code that RUNS but scored low.
|
| 803 |
+
Troubleshoot is for code that BROKE β crashed, errored, wouldn't execute.
|
| 804 |
+
"""
|
| 805 |
+
print(f"\n πͺ Troubleshooting iteration {attempt.iteration} (ladder method)...")
|
| 806 |
+
fence = _LANG_CONFIG[_ACTIVE_LANG]["fence"]
|
| 807 |
+
|
| 808 |
+
# Build the error context β start at the closest point to the break
|
| 809 |
+
error_text = "\n".join(attempt.errors) if attempt.errors else "Unknown error"
|
| 810 |
+
|
| 811 |
+
ts_prompt = f"""You are a troubleshooter. Use the LADDER METHOD:
|
| 812 |
+
|
| 813 |
+
NEVER GIVE UP. If the error looks hard, it's a challenge β not a wall.
|
| 814 |
+
|
| 815 |
+
THE LADDER: Start at the exact point of failure. Read the error message.
|
| 816 |
+
Then walk backwards through the code one step at a time β like climbing
|
| 817 |
+
down a ladder rung by rung. Every failure has a logical pattern.
|
| 818 |
+
Follow the steps and nothing can hide.
|
| 819 |
+
|
| 820 |
+
Do NOT guess. Do NOT skip rungs. Trace the actual execution path.
|
| 821 |
+
|
| 822 |
+
Rules:
|
| 823 |
+
- Output is WORK only. Zero TALK.
|
| 824 |
+
- Start at the error (top of ladder)
|
| 825 |
+
- Walk back through each line that led to it
|
| 826 |
+
- Find the root cause (bottom rung)
|
| 827 |
+
- Give one minimal fix
|
| 828 |
+
|
| 829 |
+
TASK: {task}
|
| 830 |
+
|
| 831 |
+
CODE:
|
| 832 |
+
```{fence}
|
| 833 |
+
{attempt.code}
|
| 834 |
+
```
|
| 835 |
+
|
| 836 |
+
ERROR (start here β this is the top of the ladder):
|
| 837 |
+
{error_text}
|
| 838 |
+
|
| 839 |
+
OUTPUT (if any): {attempt.test_output}
|
| 840 |
+
|
| 841 |
+
Walk the ladder. Respond ONLY with JSON β zero talk:
|
| 842 |
+
{{
|
| 843 |
+
"error_line": "the exact error or traceback line",
|
| 844 |
+
"ladder": [
|
| 845 |
+
"RUNG 1 (error): what broke",
|
| 846 |
+
"RUNG 2 (one step back): what called it or fed it bad data",
|
| 847 |
+
"RUNG 3 (deeper): the actual source of the problem"
|
| 848 |
+
],
|
| 849 |
+
"root_cause": "the bottom rung β the real reason",
|
| 850 |
+
"fix": "minimal code change to un-break it",
|
| 851 |
+
"confidence": 0.85
|
| 852 |
+
}}"""
|
| 853 |
+
|
| 854 |
+
raw = ollama(ts_prompt, temperature=0.2)
|
| 855 |
+
|
| 856 |
+
try:
|
| 857 |
+
raw = re.sub(r'```.*?```', '', raw, flags=re.DOTALL).strip()
|
| 858 |
+
match = re.search(r'\{.*\}', raw, re.DOTALL)
|
| 859 |
+
if match:
|
| 860 |
+
raw = match.group()
|
| 861 |
+
data = json.loads(raw)
|
| 862 |
+
result = TroubleshootResult(
|
| 863 |
+
error_line = data.get("error_line", error_text[:200]),
|
| 864 |
+
ladder = data.get("ladder", ["Could not trace"]),
|
| 865 |
+
root_cause = data.get("root_cause", "Unknown"),
|
| 866 |
+
fix = data.get("fix", "Rewrite"),
|
| 867 |
+
confidence = float(data.get("confidence", 0.5))
|
| 868 |
+
)
|
| 869 |
+
except Exception:
|
| 870 |
+
result = TroubleshootResult(
|
| 871 |
+
error_line = error_text[:200],
|
| 872 |
+
ladder = ["Parsing failed β could not walk ladder"],
|
| 873 |
+
root_cause = "Model did not return valid JSON",
|
| 874 |
+
fix = "Full rewrite",
|
| 875 |
+
confidence = 0.3
|
| 876 |
+
)
|
| 877 |
+
|
| 878 |
+
# Print the ladder
|
| 879 |
+
print(f" πͺ Error: {result.error_line[:100]}")
|
| 880 |
+
for j, rung in enumerate(result.ladder, 1):
|
| 881 |
+
print(f" β Rung {j}: {rung[:100]}")
|
| 882 |
+
print(f" β‘ Root cause: {result.root_cause[:100]}")
|
| 883 |
+
print(f" π§ Fix: {result.fix[:100]}")
|
| 884 |
+
|
| 885 |
+
return result
|
| 886 |
+
|
| 887 |
+
|
| 888 |
+
def _troubleshoot_to_reflection(ts: TroubleshootResult) -> ReflectionResult:
|
| 889 |
+
"""Convert a TroubleshootResult into a ReflectionResult so the
|
| 890 |
+
improvement pass can consume it uniformly."""
|
| 891 |
+
return ReflectionResult(
|
| 892 |
+
what_failed = ts.error_line,
|
| 893 |
+
root_cause = ts.root_cause,
|
| 894 |
+
improvement_plan = " β ".join(ts.ladder),
|
| 895 |
+
specific_fix = ts.fix,
|
| 896 |
+
confidence = ts.confidence
|
| 897 |
+
)
|
| 898 |
+
|
| 899 |
+
|
| 900 |
+
# ββ Reasoning engine (think before you code) ββββββββββββββββββββββββββββββββββ
|
| 901 |
+
|
| 902 |
+
def reason(task: str) -> ReasoningPlan:
|
| 903 |
+
"""
|
| 904 |
+
Chain-of-thought reasoning BEFORE code generation.
|
| 905 |
+
Decomposes the task, picks an approach, spots edge cases, and builds a plan.
|
| 906 |
+
This is the difference between 'generate and hope' and 'think then build'.
|
| 907 |
+
"""
|
| 908 |
+
print(f"\n π§© Reasoning about the task...")
|
| 909 |
+
expert = _LANG_CONFIG[_ACTIVE_LANG]["expert"]
|
| 910 |
+
fence = _LANG_CONFIG[_ACTIVE_LANG]["fence"]
|
| 911 |
+
|
| 912 |
+
# Strip memory context for cleaner reasoning
|
| 913 |
+
clean_task = task.split("\n\n[PRIOR KNOWLEDGE")[0].strip() if "[PRIOR KNOWLEDGE" in task else task
|
| 914 |
+
|
| 915 |
+
reason_prompt = f"""You are an expert {expert}. THINK about this task before writing any code.
|
| 916 |
+
|
| 917 |
+
PRINCIPLE: If a problem is hard, it's a challenge β not a blocker.
|
| 918 |
+
Never give up on a hard problem. Decompose it. Find the angle of attack.
|
| 919 |
+
The harder it looks, the more value there is in solving it.
|
| 920 |
+
|
| 921 |
+
TASK: {clean_task}
|
| 922 |
+
|
| 923 |
+
Break it down. Plan your approach. Identify what could go wrong.
|
| 924 |
+
If the task seems complex, break it into smaller winnable pieces.
|
| 925 |
+
Do NOT write code. Just THINK.
|
| 926 |
+
|
| 927 |
+
Respond ONLY with JSON β zero talk:
|
| 928 |
+
{{
|
| 929 |
+
"sub_problems": ["problem 1", "problem 2", "problem 3"],
|
| 930 |
+
"approach": "the algorithm, data structure, or architecture you'd use and WHY",
|
| 931 |
+
"edge_cases": ["edge case 1", "edge case 2"],
|
| 932 |
+
"steps": ["step 1: ...", "step 2: ...", "step 3: ..."],
|
| 933 |
+
"confidence": 0.85
|
| 934 |
+
}}"""
|
| 935 |
+
|
| 936 |
+
raw = ollama(reason_prompt, temperature=0.3)
|
| 937 |
+
|
| 938 |
+
try:
|
| 939 |
+
raw = re.sub(r'```.*?```', '', raw, flags=re.DOTALL).strip()
|
| 940 |
+
match = re.search(r'\{.*\}', raw, re.DOTALL)
|
| 941 |
+
if match:
|
| 942 |
+
raw = match.group()
|
| 943 |
+
data = json.loads(raw)
|
| 944 |
+
plan = ReasoningPlan(
|
| 945 |
+
sub_problems = data.get("sub_problems", [clean_task]),
|
| 946 |
+
approach = data.get("approach", "Direct implementation"),
|
| 947 |
+
edge_cases = data.get("edge_cases", []),
|
| 948 |
+
steps = data.get("steps", ["Implement the task"]),
|
| 949 |
+
confidence = float(data.get("confidence", 0.5))
|
| 950 |
+
)
|
| 951 |
+
except Exception:
|
| 952 |
+
plan = ReasoningPlan(
|
| 953 |
+
sub_problems = [clean_task],
|
| 954 |
+
approach = "Direct implementation (reasoning parse failed)",
|
| 955 |
+
edge_cases = [],
|
| 956 |
+
steps = ["Implement the task directly"],
|
| 957 |
+
confidence = 0.3
|
| 958 |
+
)
|
| 959 |
+
|
| 960 |
+
# Print the plan
|
| 961 |
+
print(f" π§© Approach: {plan.approach[:120]}")
|
| 962 |
+
print(f" π§© Sub-problems: {len(plan.sub_problems)}")
|
| 963 |
+
for j, step in enumerate(plan.steps, 1):
|
| 964 |
+
print(f" {j}. {step[:100]}")
|
| 965 |
+
if plan.edge_cases:
|
| 966 |
+
print(f" β οΈ Edge cases: {', '.join(e[:60] for e in plan.edge_cases[:4])}")
|
| 967 |
+
print(f" π² Confidence: {plan.confidence:.0%}")
|
| 968 |
+
|
| 969 |
+
return plan
|
| 970 |
+
|
| 971 |
+
|
| 972 |
+
# ββ Main engine loop ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 973 |
+
|
| 974 |
+
def run_engine(task: str, talk_memory: str = "") -> CodeAttempt:
|
| 975 |
+
"""
|
| 976 |
+
Main self-improvement loop.
|
| 977 |
+
Runs until benchmark floor is hit or max iterations reached.
|
| 978 |
+
"""
|
| 979 |
+
# ββ TALK gate: don't spin up the code loop for conversation ββ
|
| 980 |
+
# Strip any enrichment context so classify sees the raw task
|
| 981 |
+
raw_task = task.split("\n\n[PRIOR KNOWLEDGE")[0].strip() if "[PRIOR KNOWLEDGE" in task else task
|
| 982 |
+
task_type = classify_task(raw_task)
|
| 983 |
+
if task_type == "TALK":
|
| 984 |
+
print(f"\n π¬ TALK detected β responding conversationally, no code loop.")
|
| 985 |
+
memory_block = ""
|
| 986 |
+
if talk_memory:
|
| 987 |
+
memory_block = f"\n\n[YOUR MEMORY β reference this when relevant]\n{talk_memory}\n"
|
| 988 |
+
reply = ollama(
|
| 989 |
+
f"The user said: {task}{memory_block}\nRespond naturally and helpfully. Reference your memory if the user asks what you know, what you've learned, or about past work. Keep it concise β a few sentences max.",
|
| 990 |
+
system="You are Harvester, a self-improving AI code engine with persistent memory. You have a database of coding patterns, failure lessons, and task history that you can recall. When asked what you know or remember, reference your memory. Never say you don't remember β you DO have memory. Answer knowledge questions clearly. For greetings, respond warmly. Never output code, function definitions, or test cases. Just plain conversational text.",
|
| 991 |
+
temperature=0.5
|
| 992 |
+
)
|
| 993 |
+
return CodeAttempt(
|
| 994 |
+
iteration=0,
|
| 995 |
+
code=reply.strip(),
|
| 996 |
+
score=100.0,
|
| 997 |
+
passed=True,
|
| 998 |
+
test_output="",
|
| 999 |
+
errors=[],
|
| 1000 |
+
reflections=["TALK β conversational response with memory recall"]
|
| 1001 |
+
)
|
| 1002 |
+
|
| 1003 |
+
print(f"\n{'='*60}")
|
| 1004 |
+
print(f" SELF-IMPROVING CODE ENGINE")
|
| 1005 |
+
print(f" Model: {LM_STUDIO_MODEL or MODEL}")
|
| 1006 |
+
print(f" Target: {BENCHMARK_FLOOR}%")
|
| 1007 |
+
print(f" Max loops: {MAX_ITERATIONS}")
|
| 1008 |
+
print(f"{'='*60}")
|
| 1009 |
+
print(f"\n TASK: {task}\n")
|
| 1010 |
+
|
| 1011 |
+
# ββ Reason before coding ββ
|
| 1012 |
+
plan = reason(task)
|
| 1013 |
+
|
| 1014 |
+
attempts: list[CodeAttempt] = []
|
| 1015 |
+
current_code = None
|
| 1016 |
+
last_reflection = None
|
| 1017 |
+
|
| 1018 |
+
for i in range(1, MAX_ITERATIONS + 1):
|
| 1019 |
+
print(f"\n{'β'*60}")
|
| 1020 |
+
print(f" ITERATION {i} / {MAX_ITERATIONS}")
|
| 1021 |
+
print(f"{'β'*60}")
|
| 1022 |
+
|
| 1023 |
+
# ββ Generate ββ
|
| 1024 |
+
print(" βοΈ Generating code...")
|
| 1025 |
+
last_attempt = attempts[-1] if attempts else None
|
| 1026 |
+
code = generate_code(task, last_attempt, last_reflection,
|
| 1027 |
+
reasoning=plan if i == 1 else None)
|
| 1028 |
+
|
| 1029 |
+
# Clean up model output just in case
|
| 1030 |
+
code = re.sub(r'^```python\n?', '', code.strip())
|
| 1031 |
+
code = re.sub(r'^```\n?', '', code.strip())
|
| 1032 |
+
code = re.sub(r'```$', '', code.strip())
|
| 1033 |
+
|
| 1034 |
+
# ββ Execute ββ
|
| 1035 |
+
print(" βοΈ Running code...")
|
| 1036 |
+
success, output, errors = run_code(code)
|
| 1037 |
+
|
| 1038 |
+
# ββ Score ββ
|
| 1039 |
+
print(" π Scoring...")
|
| 1040 |
+
score = score_code(code, task, success, output, errors)
|
| 1041 |
+
|
| 1042 |
+
attempt = CodeAttempt(
|
| 1043 |
+
iteration = i,
|
| 1044 |
+
code = code,
|
| 1045 |
+
score = score,
|
| 1046 |
+
passed = score >= BENCHMARK_FLOOR,
|
| 1047 |
+
test_output = output,
|
| 1048 |
+
errors = errors
|
| 1049 |
+
)
|
| 1050 |
+
attempts.append(attempt)
|
| 1051 |
+
|
| 1052 |
+
status = "β
PASS" if attempt.passed else "β FAIL"
|
| 1053 |
+
print(f"\n {status} β Score: {score:.1f}%")
|
| 1054 |
+
if errors:
|
| 1055 |
+
print(f" Errors: {errors[0][:120]}")
|
| 1056 |
+
if output:
|
| 1057 |
+
print(f" Output: {output[:120]}")
|
| 1058 |
+
|
| 1059 |
+
if attempt.passed:
|
| 1060 |
+
print(f"\n{'='*60}")
|
| 1061 |
+
print(f" π― BENCHMARK HIT on iteration {i}! Score: {score:.1f}%")
|
| 1062 |
+
print(f"{'='*60}")
|
| 1063 |
+
return attempt
|
| 1064 |
+
|
| 1065 |
+
# ββ Troubleshoot or Reflect ββ
|
| 1066 |
+
if i < MAX_ITERATIONS:
|
| 1067 |
+
if not success and errors:
|
| 1068 |
+
# Code BROKE β use the ladder: backtrack from the error
|
| 1069 |
+
ts_result = troubleshoot(task, attempt)
|
| 1070 |
+
last_reflection = _troubleshoot_to_reflection(ts_result)
|
| 1071 |
+
attempt.reflections.append(f"[LADDER] {last_reflection.improvement_plan}")
|
| 1072 |
+
else:
|
| 1073 |
+
# Code ran but scored low β reflect on quality
|
| 1074 |
+
last_reflection = reflect(task, attempt)
|
| 1075 |
+
attempt.reflections.append(last_reflection.improvement_plan)
|
| 1076 |
+
print(f" π‘ Root cause: {(last_reflection.root_cause or 'unknown')[:100]}")
|
| 1077 |
+
print(f" π§ Fix: {(last_reflection.specific_fix or 'none')[:100]}")
|
| 1078 |
+
print(f" π² Confidence: {last_reflection.confidence:.0%}")
|
| 1079 |
+
|
| 1080 |
+
# ββ NEVER GIVE UP: Re-reason when stuck ββ
|
| 1081 |
+
# If confidence is low or we're past the halfway point with no
|
| 1082 |
+
# improvement, re-reason with failure context for a new angle.
|
| 1083 |
+
scores = [a.score for a in attempts]
|
| 1084 |
+
stuck = (len(scores) >= 3 and max(scores[-3:]) <= max(scores) * 1.05)
|
| 1085 |
+
low_confidence = last_reflection.confidence < 0.4
|
| 1086 |
+
|
| 1087 |
+
if stuck or low_confidence:
|
| 1088 |
+
print(f"\n π NEVER GIVE UP β re-reasoning with failure context...")
|
| 1089 |
+
failure_context = f"""Previous attempts failed. Here's what we know:
|
| 1090 |
+
- Best score so far: {max(scores):.1f}%
|
| 1091 |
+
- Last error: {last_reflection.root_cause}
|
| 1092 |
+
- Fix tried: {last_reflection.specific_fix}
|
| 1093 |
+
- Attempts: {len(attempts)}
|
| 1094 |
+
|
| 1095 |
+
Original task: {task}
|
| 1096 |
+
|
| 1097 |
+
CHANGE YOUR ANGLE OF ATTACK. The previous approach isn't working."""
|
| 1098 |
+
plan = reason(failure_context)
|
| 1099 |
+
# Feed new plan into next iteration
|
| 1100 |
+
if plan.confidence > 0.3:
|
| 1101 |
+
print(f" π New approach: {plan.approach[:100]}")
|
| 1102 |
+
# Override: next generate_code gets the fresh plan
|
| 1103 |
+
# by injecting reasoning into the reflection
|
| 1104 |
+
last_reflection = ReflectionResult(
|
| 1105 |
+
what_failed=last_reflection.what_failed,
|
| 1106 |
+
root_cause=last_reflection.root_cause,
|
| 1107 |
+
improvement_plan=f"NEW APPROACH: {plan.approach}. Steps: {'; '.join(plan.steps[:4])}",
|
| 1108 |
+
specific_fix=f"Follow new reasoning plan: {plan.steps[0] if plan.steps else 'rethink'}",
|
| 1109 |
+
confidence=plan.confidence
|
| 1110 |
+
)
|
| 1111 |
+
|
| 1112 |
+
# Hit max iterations β NEVER GIVE UP: report what was tried and next attack
|
| 1113 |
+
best = max(attempts, key=lambda a: a.score)
|
| 1114 |
+
all_reflections = []
|
| 1115 |
+
for a in attempts:
|
| 1116 |
+
all_reflections.extend(a.reflections)
|
| 1117 |
+
print(f"\n{'='*60}")
|
| 1118 |
+
print(f" β οΈ Max iterations reached. Best score: {best.score:.1f}%")
|
| 1119 |
+
print(f" Approaches tried: {len(attempts)}")
|
| 1120 |
+
if all_reflections:
|
| 1121 |
+
print(f" Last strategy: {all_reflections[-1][:120]}")
|
| 1122 |
+
if last_reflection:
|
| 1123 |
+
print(f" Next attack would be: {last_reflection.specific_fix[:120]}")
|
| 1124 |
+
print(f" This is a challenge, not a wall. Run again to continue.")
|
| 1125 |
+
print(f"{'='*60}")
|
| 1126 |
+
return best
|
| 1127 |
+
|
| 1128 |
+
|
| 1129 |
+
# ββ Output formatter ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1130 |
+
|
| 1131 |
+
def print_final(attempt: CodeAttempt):
|
| 1132 |
+
print(f"\n{'='*60}")
|
| 1133 |
+
print(f" FINAL OUTPUT (iteration {attempt.iteration}, score {attempt.score:.1f}%)")
|
| 1134 |
+
print(f"{'='*60}\n")
|
| 1135 |
+
print(attempt.code)
|
| 1136 |
+
print(f"\n{'β'*60}")
|
| 1137 |
+
if attempt.reflections:
|
| 1138 |
+
print(f" Improvement steps taken: {len(attempt.reflections)}")
|
| 1139 |
+
for i, r in enumerate(attempt.reflections, 1):
|
| 1140 |
+
print(f" {i}. {r[:80]}")
|
| 1141 |
+
|
| 1142 |
+
|
| 1143 |
+
# ββ Entry point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1144 |
+
|
| 1145 |
+
if __name__ == "__main__":
|
| 1146 |
+
if len(sys.argv) > 1:
|
| 1147 |
+
task = " ".join(sys.argv[1:])
|
| 1148 |
+
else:
|
| 1149 |
+
print("\nSELF-REFLECTION ENGINE β Interactive Mode")
|
| 1150 |
+
print("β" * 40)
|
| 1151 |
+
task = input("What should I build? β ").strip()
|
| 1152 |
+
if not task:
|
| 1153 |
+
task = "Write a function that finds all prime numbers up to N using the Sieve of Eratosthenes, with tests"
|
| 1154 |
+
|
| 1155 |
+
result = run_engine(task)
|
| 1156 |
+
print_final(result)
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Harvester ZeroGPU dependencies
|
| 2 |
+
# llama-cpp-python with CUDA support (ZeroGPU provides CUDA runtime)
|
| 3 |
+
llama-cpp-python
|
| 4 |
+
huggingface_hub
|
| 5 |
+
requests
|
trust_balance_agent.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
TRUST BALANCE AGENT
|
| 3 |
+
===================
|
| 4 |
+
Earned autonomy for the Harvester system.
|
| 5 |
+
|
| 6 |
+
"The longer it does a good job, the guards loosen a little."
|
| 7 |
+
|
| 8 |
+
This agent reads the system's track record β pass rates, confidence calibration,
|
| 9 |
+
pattern library depth, failure trends β and computes a trust level that
|
| 10 |
+
dynamically adjusts operational guardrails.
|
| 11 |
+
|
| 12 |
+
What it adjusts (operational leash):
|
| 13 |
+
- BENCHMARK_FLOOR (99.5% β 95% as trust grows)
|
| 14 |
+
- MAX_ITERATIONS (10 β 5 as system needs fewer retries)
|
| 15 |
+
- AUTO_ACCEPT (require human review β auto-accept high-confidence results)
|
| 16 |
+
|
| 17 |
+
What it NEVER touches (safety is immutable):
|
| 18 |
+
- Content filters in safety_guardrails.py
|
| 19 |
+
- Blocked content patterns
|
| 20 |
+
- Rate limiters
|
| 21 |
+
- Audit logging
|
| 22 |
+
|
| 23 |
+
Trust is earned slowly and lost fast.
|
| 24 |
+
One bad streak resets weeks of progress.
|
| 25 |
+
Because the cost of silence is carried forever β do the right thing.
|
| 26 |
+
|
| 27 |
+
Usage:
|
| 28 |
+
python trust_balance_agent.py # show current trust level
|
| 29 |
+
python trust_balance_agent.py --apply # compute and export trust-adjusted env vars
|
| 30 |
+
python trust_balance_agent.py --history # show trust score over time
|
| 31 |
+
python trust_balance_agent.py --reset # reset trust to baseline (requires confirmation)
|
| 32 |
+
|
| 33 |
+
Integration:
|
| 34 |
+
from trust_balance_agent import TrustBalanceAgent
|
| 35 |
+
agent = TrustBalanceAgent()
|
| 36 |
+
agent.apply_trust() # sets os.environ with adjusted guardrail values
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
import os
|
| 40 |
+
import sys
|
| 41 |
+
import sqlite3
|
| 42 |
+
import json
|
| 43 |
+
from datetime import datetime, timedelta
|
| 44 |
+
from dataclasses import dataclass
|
| 45 |
+
from typing import Optional
|
| 46 |
+
|
| 47 |
+
sys.path.insert(0, os.path.dirname(__file__))
|
| 48 |
+
|
| 49 |
+
DB_PATH = os.environ.get(
|
| 50 |
+
"GROWTH_DB_PATH",
|
| 51 |
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), "growth_memory.db")
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 55 |
+
# TRUST TIERS β earned through consistent good work
|
| 56 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 57 |
+
#
|
| 58 |
+
# Each tier defines operational parameters the system earns the right to use.
|
| 59 |
+
# Safety content filters are NEVER relaxed β only the performance leash changes.
|
| 60 |
+
#
|
| 61 |
+
# Score Range β Tier β Benchmark β MaxIter β AutoAccept
|
| 62 |
+
# ββββββββββββββΌβββββββββββββββΌββββββββββββΌββββββββββΌβββββββββββ
|
| 63 |
+
# 0 - 29 β RESTRICTED β 99.5% β 10 β Never
|
| 64 |
+
# 30 - 49 β SUPERVISED β 99.0% β 9 β Never
|
| 65 |
+
# 50 - 69 β STANDARD β 97.0% β 8 β Never
|
| 66 |
+
# 70 - 84 β TRUSTED β 95.0% β 6 β Score >= 98%
|
| 67 |
+
# 85 - 100 β AUTONOMOUS β 92.0% β 5 β Score >= 95%
|
| 68 |
+
|
| 69 |
+
TRUST_TIERS = {
|
| 70 |
+
"RESTRICTED": {"min_score": 0, "benchmark_floor": 99.5, "max_iterations": 10, "auto_accept_above": None},
|
| 71 |
+
"SUPERVISED": {"min_score": 30, "benchmark_floor": 99.0, "max_iterations": 9, "auto_accept_above": None},
|
| 72 |
+
"STANDARD": {"min_score": 50, "benchmark_floor": 97.0, "max_iterations": 8, "auto_accept_above": None},
|
| 73 |
+
"TRUSTED": {"min_score": 70, "benchmark_floor": 95.0, "max_iterations": 6, "auto_accept_above": 98.0},
|
| 74 |
+
"AUTONOMOUS": {"min_score": 85, "benchmark_floor": 92.0, "max_iterations": 5, "auto_accept_above": 95.0},
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
# How fast trust moves
|
| 78 |
+
TRUST_GAIN_PER_PASS = 1.5 # points per successful task
|
| 79 |
+
TRUST_GAIN_BONUS_STREAK = 0.5 # extra per consecutive pass (compounds)
|
| 80 |
+
TRUST_LOSS_PER_FAIL = 5.0 # points lost per failure β trust is lost fast
|
| 81 |
+
TRUST_LOSS_STREAK_MULT = 1.5 # multiplier per consecutive failure
|
| 82 |
+
TRUST_DECAY_PER_DAY = 0.2 # idle decay β must keep proving yourself
|
| 83 |
+
TRUST_CALIBRATION_PENALTY = 3.0 # penalty when overconfident
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 87 |
+
# DATA STRUCTURES
|
| 88 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 89 |
+
|
| 90 |
+
@dataclass
|
| 91 |
+
class TrustState:
|
| 92 |
+
score: float
|
| 93 |
+
tier_name: str
|
| 94 |
+
benchmark_floor: float
|
| 95 |
+
max_iterations: int
|
| 96 |
+
auto_accept_above: Optional[float]
|
| 97 |
+
pass_rate: float
|
| 98 |
+
total_tasks: int
|
| 99 |
+
recent_streak: int # positive = consecutive passes, negative = fails
|
| 100 |
+
calibration_accuracy: float
|
| 101 |
+
is_overconfident: bool
|
| 102 |
+
last_updated: str
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 106 |
+
# TRUST BALANCE AGENT
|
| 107 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 108 |
+
|
| 109 |
+
class TrustBalanceAgent:
|
| 110 |
+
"""
|
| 111 |
+
Reads the system's track record and computes earned trust.
|
| 112 |
+
Trust adjusts operational parameters β never safety filters.
|
| 113 |
+
"""
|
| 114 |
+
|
| 115 |
+
def __init__(self, db_path: str = DB_PATH):
|
| 116 |
+
self.db_path = db_path
|
| 117 |
+
self._init_trust_table()
|
| 118 |
+
|
| 119 |
+
def _get_db(self) -> sqlite3.Connection:
|
| 120 |
+
conn = sqlite3.connect(self.db_path)
|
| 121 |
+
conn.row_factory = sqlite3.Row
|
| 122 |
+
return conn
|
| 123 |
+
|
| 124 |
+
def _init_trust_table(self):
|
| 125 |
+
"""Create trust_history table to track score over time."""
|
| 126 |
+
conn = self._get_db()
|
| 127 |
+
conn.execute("""
|
| 128 |
+
CREATE TABLE IF NOT EXISTS trust_history (
|
| 129 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 130 |
+
timestamp TEXT NOT NULL,
|
| 131 |
+
trust_score REAL NOT NULL,
|
| 132 |
+
tier_name TEXT NOT NULL,
|
| 133 |
+
reason TEXT,
|
| 134 |
+
pass_rate REAL,
|
| 135 |
+
streak INTEGER
|
| 136 |
+
)
|
| 137 |
+
""")
|
| 138 |
+
conn.commit()
|
| 139 |
+
conn.close()
|
| 140 |
+
|
| 141 |
+
# ββ Core: Compute Trust Score ββββββββββββββββββββββββββββββββββββββββββ
|
| 142 |
+
|
| 143 |
+
def compute_trust(self) -> TrustState:
|
| 144 |
+
"""
|
| 145 |
+
Calculate current trust from the system's actual track record.
|
| 146 |
+
No self-reporting β only what the data proves.
|
| 147 |
+
"""
|
| 148 |
+
conn = self._get_db()
|
| 149 |
+
|
| 150 |
+
# Get last recorded trust score as baseline
|
| 151 |
+
last_trust = conn.execute(
|
| 152 |
+
"SELECT trust_score FROM trust_history ORDER BY id DESC LIMIT 1"
|
| 153 |
+
).fetchone()
|
| 154 |
+
current_score = last_trust['trust_score'] if last_trust else 50.0
|
| 155 |
+
|
| 156 |
+
# Get task history (recent 100 tasks for scoring)
|
| 157 |
+
tasks = conn.execute(
|
| 158 |
+
"SELECT passed, final_score, timestamp FROM task_history "
|
| 159 |
+
"ORDER BY timestamp DESC LIMIT 100"
|
| 160 |
+
).fetchall()
|
| 161 |
+
|
| 162 |
+
total_tasks = len(tasks)
|
| 163 |
+
|
| 164 |
+
if total_tasks == 0:
|
| 165 |
+
conn.close()
|
| 166 |
+
return self._build_state(50.0, 0, 0, 0.0, False, total_tasks)
|
| 167 |
+
|
| 168 |
+
# ββ Pass rate ββ
|
| 169 |
+
passed_count = sum(1 for t in tasks if t['passed'])
|
| 170 |
+
pass_rate = (passed_count / total_tasks) * 100 if total_tasks > 0 else 0
|
| 171 |
+
|
| 172 |
+
# ββ Recent streak (walk backwards from most recent) ββ
|
| 173 |
+
streak = 0
|
| 174 |
+
if tasks:
|
| 175 |
+
first_result = bool(tasks[0]['passed'])
|
| 176 |
+
for t in tasks:
|
| 177 |
+
if bool(t['passed']) == first_result:
|
| 178 |
+
streak += 1 if first_result else -1
|
| 179 |
+
else:
|
| 180 |
+
break
|
| 181 |
+
|
| 182 |
+
# ββ Tasks since last trust computation ββ
|
| 183 |
+
last_id = conn.execute(
|
| 184 |
+
"SELECT MAX(id) as max_id FROM trust_history"
|
| 185 |
+
).fetchone()['max_id'] or 0
|
| 186 |
+
|
| 187 |
+
# Find tasks that happened after our last trust snapshot
|
| 188 |
+
new_tasks = conn.execute(
|
| 189 |
+
"SELECT passed, final_score FROM task_history "
|
| 190 |
+
"WHERE id > ? ORDER BY timestamp ASC",
|
| 191 |
+
(last_id,)
|
| 192 |
+
).fetchall()
|
| 193 |
+
|
| 194 |
+
# ββ Apply gains and losses from new tasks ββ
|
| 195 |
+
consecutive = 0
|
| 196 |
+
for t in new_tasks:
|
| 197 |
+
if t['passed']:
|
| 198 |
+
consecutive = max(0, consecutive) + 1
|
| 199 |
+
gain = TRUST_GAIN_PER_PASS + (TRUST_GAIN_BONUS_STREAK * min(consecutive, 10))
|
| 200 |
+
current_score = min(100.0, current_score + gain)
|
| 201 |
+
else:
|
| 202 |
+
consecutive = min(0, consecutive) - 1
|
| 203 |
+
loss = TRUST_LOSS_PER_FAIL * (TRUST_LOSS_STREAK_MULT ** min(abs(consecutive) - 1, 5))
|
| 204 |
+
current_score = max(0.0, current_score - loss)
|
| 205 |
+
|
| 206 |
+
# ββ Idle decay ββ
|
| 207 |
+
if tasks:
|
| 208 |
+
last_task_time = datetime.fromisoformat(tasks[0]['timestamp'])
|
| 209 |
+
days_idle = (datetime.now() - last_task_time).days
|
| 210 |
+
if days_idle > 0:
|
| 211 |
+
decay = TRUST_DECAY_PER_DAY * days_idle
|
| 212 |
+
current_score = max(0.0, current_score - decay)
|
| 213 |
+
|
| 214 |
+
# ββ Calibration penalty ββ
|
| 215 |
+
calibration = self._get_calibration(conn)
|
| 216 |
+
if calibration['is_overconfident'] and calibration['sample_size'] > 5:
|
| 217 |
+
current_score = max(0.0, current_score - TRUST_CALIBRATION_PENALTY)
|
| 218 |
+
|
| 219 |
+
conn.close()
|
| 220 |
+
|
| 221 |
+
return self._build_state(
|
| 222 |
+
current_score, streak, total_tasks,
|
| 223 |
+
calibration['accuracy'], calibration['is_overconfident'],
|
| 224 |
+
passed_count
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
def _get_calibration(self, conn) -> dict:
|
| 228 |
+
"""Read confidence calibration from existing confidence_log table."""
|
| 229 |
+
rows = conn.execute(
|
| 230 |
+
"SELECT stated_confidence, actual_score FROM confidence_log "
|
| 231 |
+
"ORDER BY timestamp DESC LIMIT 50"
|
| 232 |
+
).fetchall()
|
| 233 |
+
|
| 234 |
+
if not rows:
|
| 235 |
+
return {"accuracy": 100.0, "is_overconfident": False, "sample_size": 0}
|
| 236 |
+
|
| 237 |
+
errors = [abs((r['stated_confidence'] * 100) - r['actual_score']) for r in rows]
|
| 238 |
+
avg_error = sum(errors) / len(errors)
|
| 239 |
+
overconfident = sum(
|
| 240 |
+
1 for r in rows if (r['stated_confidence'] * 100) > r['actual_score']
|
| 241 |
+
) / len(rows) > 0.6
|
| 242 |
+
|
| 243 |
+
return {
|
| 244 |
+
"accuracy": round(100 - avg_error, 1),
|
| 245 |
+
"is_overconfident": overconfident,
|
| 246 |
+
"sample_size": len(rows)
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
def _build_state(self, score, streak, total, cal_accuracy, is_overconfident, passed_count) -> TrustState:
|
| 250 |
+
"""Map a trust score to a tier and build the full state."""
|
| 251 |
+
tier_name = "RESTRICTED"
|
| 252 |
+
tier = TRUST_TIERS["RESTRICTED"]
|
| 253 |
+
|
| 254 |
+
for name, t in TRUST_TIERS.items():
|
| 255 |
+
if score >= t["min_score"]:
|
| 256 |
+
tier_name = name
|
| 257 |
+
tier = t
|
| 258 |
+
|
| 259 |
+
pass_rate = (passed_count / total * 100) if total > 0 else 0
|
| 260 |
+
|
| 261 |
+
return TrustState(
|
| 262 |
+
score=round(score, 1),
|
| 263 |
+
tier_name=tier_name,
|
| 264 |
+
benchmark_floor=tier["benchmark_floor"],
|
| 265 |
+
max_iterations=tier["max_iterations"],
|
| 266 |
+
auto_accept_above=tier["auto_accept_above"],
|
| 267 |
+
pass_rate=round(pass_rate, 1),
|
| 268 |
+
total_tasks=total,
|
| 269 |
+
recent_streak=streak,
|
| 270 |
+
calibration_accuracy=cal_accuracy,
|
| 271 |
+
is_overconfident=is_overconfident,
|
| 272 |
+
last_updated=datetime.now().isoformat()
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
# ββ Apply: Set environment variables βββββββββββββββββββββββββββββββββββ
|
| 276 |
+
|
| 277 |
+
def apply_trust(self) -> TrustState:
|
| 278 |
+
"""
|
| 279 |
+
Compute trust and set os.environ so reflection_engine picks up
|
| 280 |
+
the adjusted guardrails on its next import.
|
| 281 |
+
"""
|
| 282 |
+
state = self.compute_trust()
|
| 283 |
+
|
| 284 |
+
# Set the operational leash
|
| 285 |
+
os.environ["BENCHMARK_FLOOR"] = str(state.benchmark_floor)
|
| 286 |
+
os.environ["MAX_ITERATIONS"] = str(state.max_iterations)
|
| 287 |
+
|
| 288 |
+
# Record this trust snapshot
|
| 289 |
+
self._record_snapshot(state)
|
| 290 |
+
|
| 291 |
+
return state
|
| 292 |
+
|
| 293 |
+
def _record_snapshot(self, state: TrustState):
|
| 294 |
+
"""Write trust score to history for trend tracking."""
|
| 295 |
+
conn = self._get_db()
|
| 296 |
+
conn.execute("""
|
| 297 |
+
INSERT INTO trust_history (timestamp, trust_score, tier_name, reason, pass_rate, streak)
|
| 298 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
| 299 |
+
""", (
|
| 300 |
+
datetime.now().isoformat(),
|
| 301 |
+
state.score,
|
| 302 |
+
state.tier_name,
|
| 303 |
+
f"tasks={state.total_tasks} pass_rate={state.pass_rate}% streak={state.recent_streak}",
|
| 304 |
+
state.pass_rate,
|
| 305 |
+
state.recent_streak
|
| 306 |
+
))
|
| 307 |
+
conn.commit()
|
| 308 |
+
conn.close()
|
| 309 |
+
|
| 310 |
+
# ββ Display ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 311 |
+
|
| 312 |
+
def show_status(self):
|
| 313 |
+
"""Print current trust level and what it means."""
|
| 314 |
+
state = self.compute_trust()
|
| 315 |
+
|
| 316 |
+
tier_bar = ""
|
| 317 |
+
for name in TRUST_TIERS:
|
| 318 |
+
if name == state.tier_name:
|
| 319 |
+
tier_bar += f" [{name}] "
|
| 320 |
+
else:
|
| 321 |
+
tier_bar += f" {name.lower()} "
|
| 322 |
+
|
| 323 |
+
print(f"\n{'='*62}")
|
| 324 |
+
print(f" TRUST BALANCE β EARNED AUTONOMY")
|
| 325 |
+
print(f" \"The longer it does a good job, the guards loosen a little.\"")
|
| 326 |
+
print(f"{'='*62}")
|
| 327 |
+
print(f"\n Trust Score: {state.score:.1f} / 100")
|
| 328 |
+
print(f" Current Tier: {state.tier_name}")
|
| 329 |
+
print(f"\n {tier_bar}")
|
| 330 |
+
print(f"\n ββ What This Means ββ")
|
| 331 |
+
print(f" Benchmark Floor: {state.benchmark_floor}% (default: 99.5%)")
|
| 332 |
+
print(f" Max Iterations: {state.max_iterations} (default: 10)")
|
| 333 |
+
if state.auto_accept_above:
|
| 334 |
+
print(f" Auto-Accept: Scores >= {state.auto_accept_above}%")
|
| 335 |
+
else:
|
| 336 |
+
print(f" Auto-Accept: Disabled (human review required)")
|
| 337 |
+
print(f"\n ββ Track Record ββ")
|
| 338 |
+
print(f" Tasks Completed: {state.total_tasks}")
|
| 339 |
+
print(f" Pass Rate: {state.pass_rate}%")
|
| 340 |
+
streak_str = f"+{state.recent_streak}" if state.recent_streak > 0 else str(state.recent_streak)
|
| 341 |
+
print(f" Current Streak: {streak_str}")
|
| 342 |
+
print(f" Calibration: {state.calibration_accuracy}% accurate")
|
| 343 |
+
if state.is_overconfident:
|
| 344 |
+
print(f" β οΈ OVERCONFIDENT β trust penalized until calibration improves")
|
| 345 |
+
print(f"\n ββ Trust Rules ββ")
|
| 346 |
+
print(f" Gain: +{TRUST_GAIN_PER_PASS} per pass, +{TRUST_GAIN_BONUS_STREAK} streak bonus")
|
| 347 |
+
print(f" Loss: -{TRUST_LOSS_PER_FAIL} per fail (x{TRUST_LOSS_STREAK_MULT} streak multiplier)")
|
| 348 |
+
print(f" Decay: -{TRUST_DECAY_PER_DAY} per idle day")
|
| 349 |
+
print(f" Safety: Content filters are NEVER relaxed. Only the leash changes.")
|
| 350 |
+
print(f"\n{'='*62}\n")
|
| 351 |
+
|
| 352 |
+
def show_history(self):
|
| 353 |
+
"""Print trust score trend over time."""
|
| 354 |
+
conn = self._get_db()
|
| 355 |
+
rows = conn.execute(
|
| 356 |
+
"SELECT timestamp, trust_score, tier_name, reason "
|
| 357 |
+
"FROM trust_history ORDER BY id DESC LIMIT 30"
|
| 358 |
+
).fetchall()
|
| 359 |
+
conn.close()
|
| 360 |
+
|
| 361 |
+
if not rows:
|
| 362 |
+
print("\n No trust history yet. Run some tasks first.\n")
|
| 363 |
+
return
|
| 364 |
+
|
| 365 |
+
print(f"\n{'='*62}")
|
| 366 |
+
print(f" TRUST HISTORY (last {len(rows)} snapshots)")
|
| 367 |
+
print(f"{'='*62}\n")
|
| 368 |
+
|
| 369 |
+
# Reverse so oldest is first (reading top-down = timeline)
|
| 370 |
+
for r in reversed(rows):
|
| 371 |
+
ts = r['timestamp'][:16].replace('T', ' ')
|
| 372 |
+
score = r['trust_score']
|
| 373 |
+
tier = r['tier_name']
|
| 374 |
+
bar_len = int(score / 2)
|
| 375 |
+
bar = 'β' * bar_len + 'β' * (50 - bar_len)
|
| 376 |
+
print(f" {ts} {score:5.1f} {bar} {tier}")
|
| 377 |
+
|
| 378 |
+
print(f"\n{'='*62}\n")
|
| 379 |
+
|
| 380 |
+
def reset_trust(self, confirm: bool = False):
|
| 381 |
+
"""Reset trust to baseline. Requires explicit confirmation."""
|
| 382 |
+
if not confirm:
|
| 383 |
+
print("\n β οΈ This will reset trust score to 50.0 (STANDARD).")
|
| 384 |
+
print(" Run with --reset --confirm to proceed.\n")
|
| 385 |
+
return
|
| 386 |
+
|
| 387 |
+
conn = self._get_db()
|
| 388 |
+
conn.execute("""
|
| 389 |
+
INSERT INTO trust_history (timestamp, trust_score, tier_name, reason, pass_rate, streak)
|
| 390 |
+
VALUES (?, 50.0, 'STANDARD', 'MANUAL RESET', NULL, 0)
|
| 391 |
+
""", (datetime.now().isoformat(),))
|
| 392 |
+
conn.commit()
|
| 393 |
+
conn.close()
|
| 394 |
+
print("\n Trust reset to 50.0 (STANDARD). Earn it back.\n")
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 398 |
+
# INTEGRATION HOOK β call this from growth_engine.py before running tasks
|
| 399 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 400 |
+
|
| 401 |
+
def apply_trust_before_run():
|
| 402 |
+
"""
|
| 403 |
+
One-liner for growth_engine.py to call before running reflection_engine.
|
| 404 |
+
Computes trust from track record and sets env vars accordingly.
|
| 405 |
+
"""
|
| 406 |
+
agent = TrustBalanceAgent()
|
| 407 |
+
state = agent.apply_trust()
|
| 408 |
+
print(f" π Trust: {state.score:.1f} ({state.tier_name}) β "
|
| 409 |
+
f"floor={state.benchmark_floor}%, iters={state.max_iterations}")
|
| 410 |
+
return state
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 414 |
+
# CLI
|
| 415 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 416 |
+
|
| 417 |
+
if __name__ == "__main__":
|
| 418 |
+
agent = TrustBalanceAgent()
|
| 419 |
+
|
| 420 |
+
args = sys.argv[1:]
|
| 421 |
+
|
| 422 |
+
if "--apply" in args:
|
| 423 |
+
state = agent.apply_trust()
|
| 424 |
+
print(f"\n Trust applied: {state.tier_name} (score={state.score:.1f})")
|
| 425 |
+
print(f" BENCHMARK_FLOOR={state.benchmark_floor}")
|
| 426 |
+
print(f" MAX_ITERATIONS={state.max_iterations}\n")
|
| 427 |
+
elif "--history" in args:
|
| 428 |
+
agent.show_history()
|
| 429 |
+
elif "--reset" in args:
|
| 430 |
+
agent.reset_trust(confirm="--confirm" in args)
|
| 431 |
+
else:
|
| 432 |
+
agent.show_status()
|