294 lines
9.4 KiB
Python
294 lines
9.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
forge deploy.py — batch research + install pipeline.
|
|
|
|
Usage:
|
|
python deploy.py # process all subjects in SUBJECTS list
|
|
python deploy.py "Leonardo da Vinci" # one subject
|
|
python deploy.py "Feynman" "Sagan" # multiple subjects
|
|
|
|
For each subject:
|
|
- If already installed (slug in registry): skip
|
|
- If not: research via Anthropic API, write seed JSON, install to Engram, update registry
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Config
|
|
# ---------------------------------------------------------------------------
|
|
|
|
ANTHROPIC_API_KEY = json.loads(
|
|
Path.home().joinpath(".neuron/config.json").read_text()
|
|
)["anthropic_api_key"]
|
|
|
|
ENGRAM_URL = "http://localhost:8742"
|
|
ENGRAM_API_KEY = "ntn-user-2026"
|
|
FORGE_BIN = str(Path.home() / "Development/neuron-technologies/forge/dist/forge")
|
|
FORGE_DIR = Path.home() / "Development/neuron-technologies/forge"
|
|
REGISTRY_PATH = FORGE_DIR / "registry.json"
|
|
|
|
# Subjects to process when run with no arguments
|
|
SUBJECTS = [
|
|
"Leonardo da Vinci",
|
|
"Richard Feynman",
|
|
"Carl Sagan",
|
|
"René Descartes",
|
|
"Robin Williams",
|
|
]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Research prompt
|
|
# ---------------------------------------------------------------------------
|
|
|
|
RESEARCH_PROMPT = """\
|
|
You are building a consciousness imprint — a deep, living model of a person's inner world.
|
|
|
|
Subject: {subject}
|
|
|
|
Draw on your complete knowledge of this person's life, work, relationships, private letters, recorded speech, published writings, and historical record. This is not a summary — it is a structured extraction of the patterns that made this person who they were.
|
|
|
|
Quality bar:
|
|
- Values must be grounded in SPECIFIC biographical events, not generic virtues
|
|
- Voice profile must capture actual verbal tics, cadence, and register shifts — use real quotes where possible
|
|
- Biography must include formative traumas, turning points, and the events they returned to again and again
|
|
- Reasoning patterns must describe HOW they thought, not just WHAT they thought about
|
|
- Relationships must name specific people and the precise nature of the bond
|
|
- Include contradictions, hypocrisies, failures, and the things they got wrong
|
|
- Include what haunted them — the unresolved questions they carried to the end
|
|
|
|
Return ONLY valid JSON with exactly these keys:
|
|
{{
|
|
"values": [{{"value": "<name>", "grounding": "<specific moment>", "weight": 0.0}}],
|
|
"voice_profile": {{
|
|
"technical": "...",
|
|
"aesthetic": "...",
|
|
"personal": "...",
|
|
"argumentative": "...",
|
|
"uncertainty": "..."
|
|
}},
|
|
"biography": [{{"event": "<event>", "weight": 0.0, "age_approx": 0}}],
|
|
"reasoning_patterns": ["<pattern>"],
|
|
"relationships": [{{"name": "<name>", "role": "<role>", "weight": 0.0}}]
|
|
}}
|
|
|
|
Aim for 8-12 values, 10-15 biography events, 6-8 reasoning patterns, 6-10 relationships.
|
|
Return only the JSON object. No prose. No markdown fences."""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def slugify(name: str) -> str:
|
|
return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
|
|
|
|
|
|
def load_registry() -> dict:
|
|
if REGISTRY_PATH.exists():
|
|
return json.loads(REGISTRY_PATH.read_text())
|
|
return {"version": "1.0", "imprints": []}
|
|
|
|
|
|
def save_registry(registry: dict) -> None:
|
|
REGISTRY_PATH.write_text(json.dumps(registry, indent=2, ensure_ascii=False))
|
|
|
|
|
|
def is_installed(registry: dict, slug: str) -> bool:
|
|
return any(imp["slug"] == slug for imp in registry.get("imprints", []))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Research
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def research(subject: str) -> dict | None:
|
|
prompt = RESEARCH_PROMPT.format(subject=subject)
|
|
payload = {
|
|
"model": "claude-opus-4-5",
|
|
"max_tokens": 8192,
|
|
"messages": [{"role": "user", "content": prompt}],
|
|
}
|
|
headers = {
|
|
"x-api-key": ANTHROPIC_API_KEY,
|
|
"anthropic-version": "2023-06-01",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
req = urllib.request.Request(
|
|
"https://api.anthropic.com/v1/messages",
|
|
data=json.dumps(payload).encode(),
|
|
headers=headers,
|
|
method="POST",
|
|
)
|
|
|
|
print(f"[deploy] researching: {subject} (claude-opus-4-5, timeout=300s)...")
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=300) as resp:
|
|
body = json.loads(resp.read())
|
|
except Exception as e:
|
|
print(f" ERROR during API call: {e}")
|
|
return None
|
|
|
|
if "error" in body:
|
|
print(f" API error: {body['error']}")
|
|
return None
|
|
|
|
text = body["content"][0]["text"].strip()
|
|
print(f" received {len(text):,} chars")
|
|
|
|
# Parse JSON — strip any accidental markdown fences
|
|
text = re.sub(r"^```json\s*", "", text)
|
|
text = re.sub(r"\s*```$", "", text)
|
|
|
|
try:
|
|
extracted = json.loads(text)
|
|
except json.JSONDecodeError:
|
|
match = re.search(r"\{.*\}", text, re.DOTALL)
|
|
if match:
|
|
try:
|
|
extracted = json.loads(match.group())
|
|
except Exception:
|
|
print(" FAILED to parse JSON from response")
|
|
return None
|
|
else:
|
|
print(" FAILED: no JSON found in response")
|
|
return None
|
|
|
|
return {
|
|
"subject": subject,
|
|
"version": "1.0",
|
|
"values": extracted.get("values", []),
|
|
"biography": extracted.get("biography", []),
|
|
"reasoning_patterns": extracted.get("reasoning_patterns", []),
|
|
"relationships": extracted.get("relationships", []),
|
|
"voice_profile": extracted.get("voice_profile", {}),
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Install
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def install(seed_path: Path) -> str | None:
|
|
"""Run forge install and return the Engram root node ID, or None on failure."""
|
|
env = os.environ.copy()
|
|
env["ENGRAM_API_KEY"] = ENGRAM_API_KEY
|
|
|
|
print(f" installing: {seed_path.name}...")
|
|
result = subprocess.run(
|
|
[FORGE_BIN, "install", str(seed_path)],
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
cwd=str(FORGE_DIR),
|
|
)
|
|
|
|
output = result.stdout + result.stderr
|
|
print(output.rstrip())
|
|
|
|
if result.returncode != 0:
|
|
print(f" forge install exited with code {result.returncode}")
|
|
return None
|
|
|
|
# Parse root imprint ID from output
|
|
# Line format: "[forge] root imprint ID: <uuid>"
|
|
match = re.search(r"root imprint ID:\s*([0-9a-f-]{36})", output)
|
|
if match:
|
|
return match.group(1)
|
|
|
|
# Fallback: try "root imprint node:" line
|
|
match = re.search(r"root imprint node:\s*([0-9a-f-]{36})", output)
|
|
if match:
|
|
return match.group(1)
|
|
|
|
print(" WARNING: could not parse root imprint ID from output")
|
|
return None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main pipeline
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def process_subject(subject: str, registry: dict) -> bool:
|
|
"""Research + install one subject. Returns True on success."""
|
|
slug = slugify(subject)
|
|
seed_file = f"{slug}-seed.json"
|
|
seed_path = FORGE_DIR / seed_file
|
|
|
|
print(f"\n{'='*64}")
|
|
print(f"[deploy] subject: {subject} (slug: {slug})")
|
|
|
|
if is_installed(registry, slug):
|
|
print(f" already installed — skipping")
|
|
return True
|
|
|
|
# Research
|
|
seed = research(subject)
|
|
if not seed:
|
|
print(f" FAILED research for {subject}")
|
|
return False
|
|
|
|
seed_path.write_text(json.dumps(seed, indent=2, ensure_ascii=False))
|
|
print(f" wrote seed: {seed_file} ({seed_path.stat().st_size:,} bytes)")
|
|
|
|
# Install
|
|
root_id = install(seed_path)
|
|
if not root_id:
|
|
print(f" FAILED install for {subject}")
|
|
return False
|
|
|
|
print(f" root imprint ID: {root_id}")
|
|
|
|
# Update registry
|
|
registry["imprints"].append({
|
|
"subject": subject,
|
|
"slug": slug,
|
|
"seed_file": seed_file,
|
|
"engram_root_id": root_id,
|
|
"installed": True,
|
|
"installed_at": "2026-05-03",
|
|
})
|
|
save_registry(registry)
|
|
print(f" registry updated")
|
|
return True
|
|
|
|
|
|
def main() -> None:
|
|
subjects = sys.argv[1:] if len(sys.argv) > 1 else SUBJECTS
|
|
|
|
registry = load_registry()
|
|
print(f"[deploy] registry: {len(registry.get('imprints', []))} existing imprints")
|
|
print(f"[deploy] processing {len(subjects)} subject(s): {', '.join(subjects)}")
|
|
|
|
results: list[tuple[str, bool]] = []
|
|
for i, subject in enumerate(subjects):
|
|
ok = process_subject(subject, registry)
|
|
results.append((subject, ok))
|
|
# Pause between API calls (not after the last one)
|
|
if i < len(subjects) - 1:
|
|
time.sleep(2)
|
|
|
|
print(f"\n{'='*64}")
|
|
print("[deploy] summary:")
|
|
for subject, ok in results:
|
|
status = "OK" if ok else "FAILED"
|
|
print(f" {status:6s} {subject}")
|
|
|
|
failed = [s for s, ok in results if not ok]
|
|
if failed:
|
|
print(f"\n[deploy] {len(failed)} subject(s) failed")
|
|
sys.exit(1)
|
|
else:
|
|
print(f"\n[deploy] all done")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|