Cyber Catalyst Team commited on
Commit
12ab90a
Β·
1 Parent(s): ed6da33

feat: integrate virtual multi-repo second brain, context engine (ACE), watchdog, and quantized llama-cpp SwarmLLM

Browse files
Files changed (8) hide show
  1. Dockerfile +3 -3
  2. backend.py +135 -15
  3. context_engine.py +128 -0
  4. helix_state.py +237 -0
  5. requirements.txt +2 -0
  6. second_brain.py +446 -0
  7. survival_watchdog.py +171 -0
  8. swarm_llm.py +208 -0
Dockerfile CHANGED
@@ -2,7 +2,7 @@ FROM python:3.11-slim
2
 
3
  # Install system dependencies
4
  RUN apt-get update && apt-get install -y --no-install-recommends \
5
- git bash curl tini && \
6
  rm -rf /var/lib/apt/lists/*
7
 
8
  # Create non-root user (HF requirement: uid 1000)
@@ -17,8 +17,8 @@ WORKDIR $HOME/app
17
  COPY --chown=user requirements.txt .
18
  RUN pip install --no-cache-dir --user -r requirements.txt
19
 
20
- # Copy application
21
- COPY --chown=user backend.py .
22
 
23
  # Create workspace directory
24
  RUN mkdir -p /tmp/workspace
 
2
 
3
  # Install system dependencies
4
  RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ git bash curl tini build-essential zip unzip && \
6
  rm -rf /var/lib/apt/lists/*
7
 
8
  # Create non-root user (HF requirement: uid 1000)
 
17
  COPY --chown=user requirements.txt .
18
  RUN pip install --no-cache-dir --user -r requirements.txt
19
 
20
+ # Copy application (including second_brain, watchdog, helix, swarm)
21
+ COPY --chown=user . .
22
 
23
  # Create workspace directory
24
  RUN mkdir -p /tmp/workspace
backend.py CHANGED
@@ -28,6 +28,19 @@ from pathlib import Path
28
  from typing import AsyncIterator, Optional, List, Dict, Any
29
  from pydantic import BaseModel
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  from fastapi import FastAPI, Request, Header, HTTPException
32
  from fastapi.responses import StreamingResponse, JSONResponse, HTMLResponse
33
  from fastapi.middleware.cors import CORSMiddleware
@@ -1880,7 +1893,7 @@ import httpx
1880
  # ---------------------------------------------------------------------------
1881
  SPACE3_URL = os.environ.get("SPACE3_URL", "https://augment17-claude-code-backend.hf.space")
1882
  SPACE4_URL = os.environ.get("SPACE4_URL", "https://shyota-mcp-cloud-host.hf.space")
1883
- SPACE5_URL = os.environ.get("SPACE5_URL", SPACE3_URL) # Vault is co-hosted on Space 3
1884
  SPACE6_URL = os.environ.get("SPACE6_URL", "https://augment17-mcp-cloud-host.hf.space")
1885
 
1886
  async def get_active_projects():
@@ -1947,6 +1960,10 @@ _Generated by Space 2 (Cerebrum) at {ts}_
1947
 
1948
  async def execute_build_cycle(project_name: str, goal: str):
1949
  log_activity(f"[Build Mode] Initiating build cycle for project '{project_name}' (goal: '{goal}')")
 
 
 
 
1950
  await rate_limiter.wait_for_nim()
1951
  plan_prompt = (
1952
  f"We are building: '{goal}'. "
@@ -1960,7 +1977,6 @@ async def execute_build_cycle(project_name: str, goal: str):
1960
  max_tokens=300
1961
  )
1962
  raw = res.choices[0].message.content.strip()
1963
- # Strip markdown code fences if present
1964
  if raw.startswith("```"):
1965
  raw = raw.split("```")[1].lstrip("json").strip()
1966
  task = json.loads(raw)
@@ -1970,10 +1986,15 @@ async def execute_build_cycle(project_name: str, goal: str):
1970
  log_activity(f"[Build Mode Error] NIM planning failed for project '{project_name}': {e}")
1971
  return
1972
 
1973
- # Build the Karpathy project_state.md to send to Space 3
1974
- state_md = build_project_state_md(
1975
- project_name=project_name, goal=goal, mode="build",
1976
- task_prompt=task_prompt
 
 
 
 
 
1977
  )
1978
 
1979
  log_activity(f"[Build Mode] Dispatching to Space 3 (Forge) β€” project '{project_name}': '{task_prompt[:80]}'")
@@ -1982,7 +2003,7 @@ async def execute_build_cycle(project_name: str, goal: str):
1982
  "action": "execute_code",
1983
  "prompt": task_prompt,
1984
  "context_rules": context_rules,
1985
- "project_state_md": state_md # Karpathy CLAUDE.md protocol
1986
  })
1987
 
1988
  if forge_res.get("status") == "success":
@@ -1995,22 +2016,54 @@ async def execute_build_cycle(project_name: str, goal: str):
1995
  "url": SPACE3_URL,
1996
  "project_name": project_name
1997
  }, timeout=30.0)
 
1998
  verdict = test_res.get("verdict", "UNKNOWN")
1999
  reason = test_res.get("reason", "")
2000
  log_activity(f"[Build Mode] Space 6 (Sandbox) Verdict: {verdict} β€” {reason}")
2001
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2002
  log_activity("[Build Mode] Triggering Space 5 (Vault) backup commit...")
2003
  vault_res = await apost_json(f"{SPACE5_URL}/api/vault/push", {
2004
  "project_name": project_name,
2005
  "summary": summary
2006
  }, timeout=30.0)
2007
- log_activity(f"[Build Mode] Space 5 (Vault) backup: {vault_res.get('status', 'unknown')}")
 
 
 
 
 
 
2008
  else:
2009
  log_activity(f"[Build Mode Warning] Space 3 (Forge) failure for '{project_name}': {forge_res.get('error')}")
2010
 
 
2011
  async def execute_eternity_cycle(project_name: str, goal: str):
2012
  log_activity(f"[Eternity Mode] R&D cycle starting for project '{project_name}' (goal: '{goal}')")
2013
 
 
 
 
2014
  # Step 1: Ask Space 4 (Library) for research
2015
  log_activity(f"[Eternity Mode] Querying Space 4 (Library) for '{project_name}'...")
2016
  research_res = await apost_json(f"{SPACE4_URL}/api/research", {
@@ -2020,6 +2073,14 @@ async def execute_eternity_cycle(project_name: str, goal: str):
2020
  await update_db_brief(project_name, brief)
2021
  log_activity(f"[Eternity Mode] Space 4 (Library) brief received ({len(brief)} chars)")
2022
 
 
 
 
 
 
 
 
 
2023
  # Step 2: Ask NIM to plan next feature based on research
2024
  await rate_limiter.wait_for_nim()
2025
  plan_prompt = (
@@ -2043,10 +2104,13 @@ async def execute_eternity_cycle(project_name: str, goal: str):
2043
  log_activity(f"[Eternity Mode Error] NIM planning failed for '{project_name}': {e}")
2044
  return
2045
 
2046
- # Step 3: Build Karpathy project_state.md
2047
- state_md = build_project_state_md(
2048
- project_name=project_name, goal=goal, mode="eternity",
2049
- task_prompt=task_prompt, brief=brief
 
 
 
2050
  )
2051
 
2052
  # Step 4: Dispatch to Space 3 (Forge)
@@ -2062,12 +2126,29 @@ async def execute_eternity_cycle(project_name: str, goal: str):
2062
  if forge_res.get("status") == "success":
2063
  summary = forge_res.get("summary", "")
2064
  log_activity(f"[Eternity Mode] Space 3 (Forge) SUCCESS: {summary}")
 
 
 
 
 
 
 
 
 
 
 
2065
  # Trigger vault backup after each successful eternity cycle
2066
  vault_res = await apost_json(f"{SPACE5_URL}/api/vault/push", {
2067
  "project_name": project_name,
2068
  "summary": summary
2069
  }, timeout=30.0)
2070
- log_activity(f"[Eternity Mode] Space 5 (Vault) backup: {vault_res.get('status', 'unknown')}")
 
 
 
 
 
 
2071
  else:
2072
  log_activity(f"[Eternity Mode Warning] Space 3 (Forge) failure: {forge_res.get('error')}")
2073
 
@@ -2211,6 +2292,18 @@ async def get_logs():
2211
  return list(activity_logs)
2212
 
2213
 
 
 
 
 
 
 
 
 
 
 
 
 
2214
  @app.get("/api/models-status")
2215
  async def get_models_status():
2216
  status_list = []
@@ -2437,10 +2530,36 @@ async def db_cleanup_loop():
2437
  await asyncio.sleep(86400) # Every 24 hours
2438
 
2439
 
 
 
 
 
 
 
 
 
 
 
2440
  @app.on_event("startup")
2441
  async def startup_event():
2442
- # Start the watchdog thread on startup
2443
- threading.Thread(target=run_watchdog, daemon=True).start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2444
  # Start the local backup loop thread
2445
  threading.Thread(target=run_backup_loop, daemon=True).start()
2446
  # Start the eternity R&D loop as an async task on the main event loop
@@ -2451,6 +2570,7 @@ async def startup_event():
2451
  asyncio.create_task(db_cleanup_loop())
2452
 
2453
 
 
2454
  # ---------------------------------------------------------------------------
2455
  # Entrypoint
2456
  # ---------------------------------------------------------------------------
 
28
  from typing import AsyncIterator, Optional, List, Dict, Any
29
  from pydantic import BaseModel
30
 
31
+ # --- Ultimate Agent Brain Imports ---
32
+ from second_brain import SecondBrainWrapper
33
+ from survival_watchdog import SurvivalWatchdog, get_metrics
34
+ from swarm_llm import swarm
35
+ from helix_state import helix_db
36
+ from context_engine import ContextEngine
37
+
38
+ # Instantiate singletons for the orchestrator
39
+ brain = SecondBrainWrapper(space_name="space2-cerebrum")
40
+ context_engine = ContextEngine(brain)
41
+ watchdog = SurvivalWatchdog()
42
+
43
+
44
  from fastapi import FastAPI, Request, Header, HTTPException
45
  from fastapi.responses import StreamingResponse, JSONResponse, HTMLResponse
46
  from fastapi.middleware.cors import CORSMiddleware
 
1893
  # ---------------------------------------------------------------------------
1894
  SPACE3_URL = os.environ.get("SPACE3_URL", "https://augment17-claude-code-backend.hf.space")
1895
  SPACE4_URL = os.environ.get("SPACE4_URL", "https://shyota-mcp-cloud-host.hf.space")
1896
+ SPACE5_URL = os.environ.get("SPACE5_URL", "https://augment17-agent-worker-loop.hf.space") # Dedicated Vault container
1897
  SPACE6_URL = os.environ.get("SPACE6_URL", "https://augment17-mcp-cloud-host.hf.space")
1898
 
1899
  async def get_active_projects():
 
1960
 
1961
  async def execute_build_cycle(project_name: str, goal: str):
1962
  log_activity(f"[Build Mode] Initiating build cycle for project '{project_name}' (goal: '{goal}')")
1963
+
1964
+ # Track project state in our in-process graph DB
1965
+ helix_db.upsert_project(project_name, goal, "build", "low")
1966
+
1967
  await rate_limiter.wait_for_nim()
1968
  plan_prompt = (
1969
  f"We are building: '{goal}'. "
 
1977
  max_tokens=300
1978
  )
1979
  raw = res.choices[0].message.content.strip()
 
1980
  if raw.startswith("```"):
1981
  raw = raw.split("```")[1].lstrip("json").strip()
1982
  task = json.loads(raw)
 
1986
  log_activity(f"[Build Mode Error] NIM planning failed for project '{project_name}': {e}")
1987
  return
1988
 
1989
+ # Use the context-locked Bell Curve prompt builder (enforces max 2500 tokens)
1990
+ playbook_file = f"space3-forge/debugging/{project_name}_playbook.md"
1991
+ state_md = brain.build_apex_prompt(
1992
+ project_name=project_name,
1993
+ goal=goal,
1994
+ mode="build",
1995
+ task_instruction=task_prompt,
1996
+ research_topic=project_name,
1997
+ current_file=""
1998
  )
1999
 
2000
  log_activity(f"[Build Mode] Dispatching to Space 3 (Forge) β€” project '{project_name}': '{task_prompt[:80]}'")
 
2003
  "action": "execute_code",
2004
  "prompt": task_prompt,
2005
  "context_rules": context_rules,
2006
+ "project_state_md": state_md
2007
  })
2008
 
2009
  if forge_res.get("status") == "success":
 
2016
  "url": SPACE3_URL,
2017
  "project_name": project_name
2018
  }, timeout=30.0)
2019
+
2020
  verdict = test_res.get("verdict", "UNKNOWN")
2021
  reason = test_res.get("reason", "")
2022
  log_activity(f"[Build Mode] Space 6 (Sandbox) Verdict: {verdict} β€” {reason}")
2023
 
2024
+ # --- ACE: Reflect & Curate ---
2025
+ # Runs the feedback loop to update playbook rules on failure, or record patterns on success
2026
+ ace_verdict = await context_engine.reflect_and_curate(
2027
+ project_name=project_name,
2028
+ task_prompt=task_prompt,
2029
+ result_summary=summary,
2030
+ verdict=verdict,
2031
+ reason=reason
2032
+ )
2033
+ log_activity(f"[Build Mode] ACE Reflection Result: {ace_verdict}")
2034
+
2035
+ # Update in-process graph DB cycle and state
2036
+ helix_db.record_cycle(project_name, summary, verdict)
2037
+
2038
+ # Log event directly to the local Second Brain git repo
2039
+ brain.append(
2040
+ "space2-cerebrum/loop_log.md",
2041
+ f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Build Success | Project: {project_name} | Summary: {summary[:80]} | Verdict: {verdict}",
2042
+ f"[Brain Log] Record build cycle for {project_name}"
2043
+ )
2044
+
2045
  log_activity("[Build Mode] Triggering Space 5 (Vault) backup commit...")
2046
  vault_res = await apost_json(f"{SPACE5_URL}/api/vault/push", {
2047
  "project_name": project_name,
2048
  "summary": summary
2049
  }, timeout=30.0)
2050
+ if vault_res.get("status") == "error":
2051
+ log_activity("[Build Mode] Space 5 (Vault) failed. Retrying backup via Space 3 (Forge) fallback...")
2052
+ vault_res = await apost_json(f"{SPACE3_URL}/api/vault/push", {
2053
+ "project_name": project_name,
2054
+ "summary": summary
2055
+ }, timeout=30.0)
2056
+ log_activity(f"[Build Mode] Vault backup status: {vault_res.get('status', 'unknown')}")
2057
  else:
2058
  log_activity(f"[Build Mode Warning] Space 3 (Forge) failure for '{project_name}': {forge_res.get('error')}")
2059
 
2060
+
2061
  async def execute_eternity_cycle(project_name: str, goal: str):
2062
  log_activity(f"[Eternity Mode] R&D cycle starting for project '{project_name}' (goal: '{goal}')")
2063
 
2064
+ # Track project state in our in-process graph DB
2065
+ helix_db.upsert_project(project_name, goal, "eternity", "low")
2066
+
2067
  # Step 1: Ask Space 4 (Library) for research
2068
  log_activity(f"[Eternity Mode] Querying Space 4 (Library) for '{project_name}'...")
2069
  research_res = await apost_json(f"{SPACE4_URL}/api/research", {
 
2073
  await update_db_brief(project_name, brief)
2074
  log_activity(f"[Eternity Mode] Space 4 (Library) brief received ({len(brief)} chars)")
2075
 
2076
+ # Save research brief to Second Brain wiki on GitHub
2077
+ research_file = f"space4-library/research/{project_name.replace(' ', '-')}.md"
2078
+ brain.write(
2079
+ research_file,
2080
+ f"# Research Brief: {project_name}\n\n{brief}",
2081
+ f"[Brain Research] Save brief for {project_name}"
2082
+ )
2083
+
2084
  # Step 2: Ask NIM to plan next feature based on research
2085
  await rate_limiter.wait_for_nim()
2086
  plan_prompt = (
 
2104
  log_activity(f"[Eternity Mode Error] NIM planning failed for '{project_name}': {e}")
2105
  return
2106
 
2107
+ # Step 3: Build Karpathy context-locked prompt (Apex)
2108
+ state_md = brain.build_apex_prompt(
2109
+ project_name=project_name,
2110
+ goal=goal,
2111
+ mode="eternity",
2112
+ task_instruction=task_prompt,
2113
+ research_topic=project_name
2114
  )
2115
 
2116
  # Step 4: Dispatch to Space 3 (Forge)
 
2126
  if forge_res.get("status") == "success":
2127
  summary = forge_res.get("summary", "")
2128
  log_activity(f"[Eternity Mode] Space 3 (Forge) SUCCESS: {summary}")
2129
+
2130
+ # Update graph DB cycle
2131
+ helix_db.record_cycle(project_name, summary, "PASS")
2132
+
2133
+ # Log event directly to the local Second Brain git repo
2134
+ brain.append(
2135
+ "space2-cerebrum/loop_log.md",
2136
+ f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Eternity Cycle Success | Project: {project_name} | Summary: {summary[:80]}",
2137
+ f"[Brain Log] Record eternity cycle for {project_name}"
2138
+ )
2139
+
2140
  # Trigger vault backup after each successful eternity cycle
2141
  vault_res = await apost_json(f"{SPACE5_URL}/api/vault/push", {
2142
  "project_name": project_name,
2143
  "summary": summary
2144
  }, timeout=30.0)
2145
+ if vault_res.get("status") == "error":
2146
+ log_activity("[Eternity Mode] Space 5 (Vault) failed. Retrying backup via Space 3 (Forge) fallback...")
2147
+ vault_res = await apost_json(f"{SPACE3_URL}/api/vault/push", {
2148
+ "project_name": project_name,
2149
+ "summary": summary
2150
+ }, timeout=30.0)
2151
+ log_activity(f"[Eternity Mode] Vault backup status: {vault_res.get('status', 'unknown')}")
2152
  else:
2153
  log_activity(f"[Eternity Mode Warning] Space 3 (Forge) failure: {forge_res.get('error')}")
2154
 
 
2292
  return list(activity_logs)
2293
 
2294
 
2295
+ @app.get("/api/metrics")
2296
+ async def metrics():
2297
+ """Exposes live CPU/RAM and HelixStateDB graph stats for space monitoring."""
2298
+ vm = watchdog.get_metrics() if hasattr(watchdog, "get_metrics") else get_metrics()
2299
+ return {
2300
+ "status": "success",
2301
+ "timestamp": time.time(),
2302
+ "space_resources": vm,
2303
+ "graph_database": helix_db.dump() if hasattr(helix_db, "dump") else {}
2304
+ }
2305
+
2306
+
2307
  @app.get("/api/models-status")
2308
  async def get_models_status():
2309
  status_list = []
 
2530
  await asyncio.sleep(86400) # Every 24 hours
2531
 
2532
 
2533
+ async def wiki_compactor_loop():
2534
+ log_activity("[Compactor] Wiki auto-compactor background task started.")
2535
+ while True:
2536
+ try:
2537
+ await context_engine.compact_wiki()
2538
+ except Exception as e:
2539
+ log_activity(f"[Compactor Error] {e}")
2540
+ await asyncio.sleep(86400) # Every 24 hours
2541
+
2542
+
2543
  @app.on_event("startup")
2544
  async def startup_event():
2545
+ # Initialize the Ultimate Brain components
2546
+ try:
2547
+ log_activity("[Startup] Initialising Second Brain (git clone)...")
2548
+ brain.initialize()
2549
+ log_activity("[Startup] Second Brain cache ready.")
2550
+
2551
+ log_activity("[Startup] Warming up local SwarmLLM (Qwen2.5-1.5B)...")
2552
+ asyncio.create_task(swarm.warm_up())
2553
+ except Exception as e:
2554
+ log_activity(f"[Startup Error] Brain init failed: {e}")
2555
+
2556
+ # Start the Resource Watchdog background task
2557
+ asyncio.create_task(watchdog.run())
2558
+ # Start the Git-Sync background task
2559
+ asyncio.create_task(brain.background_sync())
2560
+ # Start the Wiki Auto-Compaction task
2561
+ asyncio.create_task(wiki_compactor_loop())
2562
+
2563
  # Start the local backup loop thread
2564
  threading.Thread(target=run_backup_loop, daemon=True).start()
2565
  # Start the eternity R&D loop as an async task on the main event loop
 
2570
  asyncio.create_task(db_cleanup_loop())
2571
 
2572
 
2573
+
2574
  # ---------------------------------------------------------------------------
2575
  # Entrypoint
2576
  # ---------------------------------------------------------------------------
context_engine.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ context_engine.py β€” Agentic Context Engine (ACE) & Auto-Compactor
4
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5
+ Implements the 3-agent self-improvement loop:
6
+ 1. Generator: Space 3 (Forge) executes code based on prompt.
7
+ 2. Reflector: Analyzes the test log and verdict from Space 6 (Sandbox).
8
+ Determines what succeeded, what failed, and why.
9
+ 3. Curator: Updates the persistent "playbook" in the Second Brain
10
+ with actionable instructions to avoid repeating mistakes.
11
+
12
+ Also implements the Auto-Compactor:
13
+ - Scans playbooks and logs.
14
+ - Summarises and merges duplicate rules to keep context within
15
+ the Bell Curve apex (preventing context poisoning).
16
+ """
17
+
18
+ import json
19
+ import logging
20
+ import time
21
+ from second_brain import SecondBrainWrapper
22
+ from swarm_llm import swarm
23
+
24
+ logger = logging.getLogger("context_engine")
25
+
26
+ class ContextEngine:
27
+ def __init__(self, brain: SecondBrainWrapper):
28
+ self.brain = brain
29
+
30
+ # ── ACE Reflect & Curate ──────────────────────────────────────────────────
31
+
32
+ async def reflect_and_curate(
33
+ self,
34
+ project_name: str,
35
+ task_prompt: str,
36
+ result_summary: str,
37
+ verdict: str,
38
+ reason: str
39
+ ) -> str:
40
+ """
41
+ Runs after an execution cycle.
42
+ If failed, reflects on why and updates the project playbook.
43
+ If succeeded, records the success pattern.
44
+ """
45
+ playbook_path = f"space3-forge/debugging/{project_name}_playbook.md"
46
+ current_playbook = self.brain.read(playbook_path, "brain")
47
+
48
+ if verdict == "FAIL":
49
+ logger.info(f"[ACE] Project '{project_name}' failed verification. Reflecting…")
50
+ reflection_prompt = f"""Task attempted:
51
+ "{task_prompt}"
52
+
53
+ Execution summary:
54
+ "{result_summary}"
55
+
56
+ Test Failure Reason:
57
+ "{reason}"
58
+
59
+ Current Playbook rules:
60
+ {current_playbook if current_playbook else "_No rules yet._"}
61
+
62
+ ---
63
+ What went wrong? Write exactly 1 or 2 new concrete guidelines for the coder agent to prevent this specific failure in the future.
64
+ Keep guidelines extremely brief, specific, and actionable. Do NOT repeat existing rules.
65
+ """
66
+ # Use local SwarmLLM to reflect (saving NIM quota)
67
+ new_rules = await swarm.infer(reflection_prompt, system="You are a senior code architect reflecting on test failures.")
68
+
69
+ # Curate: Append new rules to playbook
70
+ updated_playbook = current_playbook + f"\n\n### Failure Correction ({time.strftime('%Y-%m-%d')})\n{new_rules.strip()}"
71
+ self.brain.write(playbook_path, updated_playbook, f"[ACE] Add failure corrections for {project_name}")
72
+ logger.info(f"[ACE] Curated playbook for '{project_name}' updated on GitHub.")
73
+ return new_rules
74
+
75
+ elif verdict == "PASS" and not current_playbook:
76
+ # Seed the playbook with success patterns
77
+ logger.info(f"[ACE] Project '{project_name}' passed. Seeding playbook.")
78
+ seed_content = f"""# Playbook: {project_name}
79
+ _Self-improving ruleset curated by ACE (Agentic Context Engine)_
80
+
81
+ ## Success Rules
82
+ - Initial implementation passed test suite successfully. Keep code simple and modular.
83
+ """
84
+ self.brain.write(playbook_path, seed_content, f"[ACE] Seed playbook for {project_name}")
85
+ return "Seed rules created"
86
+
87
+ return "No curation required"
88
+
89
+ # ── Auto-Compactor ────────────────────────────────────────────────────────
90
+
91
+ async def compact_wiki(self):
92
+ """
93
+ Auto-Compaction Protocol.
94
+ Scans all files in the Second Brain.
95
+ If a playbook or log exceeds its slot budget, consolidates and merges
96
+ duplicate rules to prevent context poisoning.
97
+ """
98
+ logger.info("[Compactor] Running wiki compaction sweep…")
99
+
100
+ # 1. Compact playbooks in space3-forge/debugging/
101
+ playbooks = self.brain.list_files("space3-forge/debugging")
102
+ for p in playbooks:
103
+ content = self.brain.read(p, "brain")
104
+ if len(content) > 3000:
105
+ logger.info(f"[Compactor] Playbook '{p}' is large ({len(content)} chars). Compacting…")
106
+ compaction_prompt = f"""The following is a coding playbook with rules collected over multiple cycles:
107
+ {content}
108
+
109
+ ---
110
+ Consolidate the rules above. Remove duplicates, merge similar guidelines, and output a clean, highly condensed list of rules.
111
+ Maintain the markdown header structure. Do NOT lose important technical details.
112
+ """
113
+ compacted = await swarm.infer(compaction_prompt, system="You are an expert compiler that deduplicates and condenses playbooks.")
114
+ self.brain.write(p, compacted, f"[Compactor] Compacted playbook {p}")
115
+ logger.info(f"[Compactor] Compacted '{p}' down to {len(compacted)} chars.")
116
+
117
+ # 2. Compact loop logs in space2-cerebrum/loop_log.md
118
+ log_path = "space2-cerebrum/loop_log.md"
119
+ log_content = self.brain.read(log_path, "brain")
120
+ if len(log_content) > 4000:
121
+ logger.info(f"[Compactor] Log '{log_path}' exceeds limit. Archiving old entries…")
122
+ lines = log_content.splitlines()
123
+ # Keep only the last 30 lines, archive the rest
124
+ recent = "\n".join(lines[-30:])
125
+ self.brain.write(log_path, recent, "[Compactor] Trim and archive old loop logs")
126
+ logger.info(f"[Compactor] Loop log trimmed.")
127
+
128
+ logger.info("[Compactor] Compaction sweep complete.")
helix_state.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ helix_state.py β€” In-Process Graph State Manager
4
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5
+ Implements the same graph API that HelixDB would expose via Python bindings,
6
+ but runs entirely in-process (zero network, zero daemon, ~0 MB overhead).
7
+
8
+ Why not Redis?
9
+ Redis requires a separate daemon process (~200 MB RAM) and TCP round-trips.
10
+ This module stores the same data as a Python dict-of-dicts with O(1) node
11
+ lookup and O(k) edge traversal where k = number of edges per node.
12
+
13
+ Why not flat dicts in backend.py?
14
+ A graph model lets us express relationships that flat dicts cannot:
15
+ Project β†’ HAS_TASK β†’ Task
16
+ Task β†’ USES_FILE β†’ File
17
+ File β†’ HAD_BUG β†’ Bug
18
+ Bug β†’ FIXED_BY β†’ Snippet (in Second Brain)
19
+ This powers the Bell Curve apex prompt: we can query "What file is this
20
+ task working on?" and load exactly that file β€” nothing more.
21
+
22
+ Drop-in swap: When HelixDB ships stable Python bindings, replace this
23
+ file with: from helixdb import HelixDB as HelixStateDB
24
+ The public API (add_node, add_edge, get_node, get_neighbors, update_node,
25
+ remove_node, query_path) is kept identical to the planned HelixDB SDK.
26
+ """
27
+
28
+ import time
29
+ import logging
30
+ import threading
31
+ from typing import Any, Dict, List, Optional, Tuple
32
+
33
+ logger = logging.getLogger("helix_state")
34
+
35
+
36
+ class HelixStateDB:
37
+ """
38
+ In-process graph database.
39
+
40
+ Graph model:
41
+ Nodes: { node_type: { node_id: { **properties } } }
42
+ Edges: { (src_type, src_id, edge_label, dst_type, dst_id): { **properties } }
43
+
44
+ Thread-safe for concurrent FastAPI request handlers.
45
+ """
46
+
47
+ def __init__(self):
48
+ self._nodes: Dict[str, Dict[str, Dict[str, Any]]] = {}
49
+ self._edges: Dict[Tuple, Dict[str, Any]] = {}
50
+ self._lock = threading.RLock()
51
+ logger.info("[HelixState] In-process graph database initialised.")
52
+
53
+ # ── Node Operations ───────────────────────────────────────────────────────
54
+
55
+ def add_node(self, node_type: str, node_id: str, **props) -> bool:
56
+ """Insert or replace a node. Returns True on success."""
57
+ with self._lock:
58
+ if node_type not in self._nodes:
59
+ self._nodes[node_type] = {}
60
+ self._nodes[node_type][node_id] = {
61
+ **props,
62
+ "_created_at": time.time(),
63
+ "_updated_at": time.time(),
64
+ }
65
+ logger.debug("[HelixState] add_node(%s, %s)", node_type, node_id)
66
+ return True
67
+
68
+ def update_node(self, node_type: str, node_id: str, **props) -> bool:
69
+ """Merge props into an existing node. Returns False if node not found."""
70
+ with self._lock:
71
+ node = self._nodes.get(node_type, {}).get(node_id)
72
+ if node is None:
73
+ return False
74
+ node.update(props)
75
+ node["_updated_at"] = time.time()
76
+ logger.debug("[HelixState] update_node(%s, %s)", node_type, node_id)
77
+ return True
78
+
79
+ def get_node(self, node_type: str, node_id: str) -> Optional[Dict[str, Any]]:
80
+ """Return a node's property dict, or None."""
81
+ with self._lock:
82
+ return self._nodes.get(node_type, {}).get(node_id)
83
+
84
+ def remove_node(self, node_type: str, node_id: str) -> bool:
85
+ """Remove a node and all its edges."""
86
+ with self._lock:
87
+ if node_id not in self._nodes.get(node_type, {}):
88
+ return False
89
+ del self._nodes[node_type][node_id]
90
+ # Prune orphaned edges
91
+ dead = [k for k in self._edges
92
+ if (k[0] == node_type and k[1] == node_id) or
93
+ (k[3] == node_type and k[4] == node_id)]
94
+ for k in dead:
95
+ del self._edges[k]
96
+ logger.debug("[HelixState] remove_node(%s, %s) + %d edges", node_type, node_id, len(dead))
97
+ return True
98
+
99
+ def list_nodes(self, node_type: str) -> List[str]:
100
+ """Return all node IDs of a given type."""
101
+ with self._lock:
102
+ return list(self._nodes.get(node_type, {}).keys())
103
+
104
+ # ── Edge Operations ───────────────────────────────────────────────────────
105
+
106
+ def add_edge(
107
+ self,
108
+ src_type: str, src_id: str,
109
+ dst_type: str, dst_id: str,
110
+ label: str,
111
+ **props,
112
+ ) -> bool:
113
+ """Add a directed edge (src)-[label]->(dst). Overwrites if exists."""
114
+ with self._lock:
115
+ key = (src_type, src_id, label, dst_type, dst_id)
116
+ self._edges[key] = {**props, "_created_at": time.time()}
117
+ logger.debug("[HelixState] add_edge %s:%s -[%s]-> %s:%s", src_type, src_id, label, dst_type, dst_id)
118
+ return True
119
+
120
+ def get_neighbors(
121
+ self,
122
+ src_type: str,
123
+ src_id: str,
124
+ label: str,
125
+ dst_type: Optional[str] = None,
126
+ ) -> List[Dict[str, Any]]:
127
+ """
128
+ Return list of destination node property dicts reachable from
129
+ (src_type, src_id) via edges with the given label.
130
+ Optionally filter by dst_type.
131
+ """
132
+ with self._lock:
133
+ results = []
134
+ for key, edge_props in self._edges.items():
135
+ s_type, s_id, e_label, d_type, d_id = key
136
+ if s_type != src_type or s_id != src_id or e_label != label:
137
+ continue
138
+ if dst_type and d_type != dst_type:
139
+ continue
140
+ node = self._nodes.get(d_type, {}).get(d_id)
141
+ if node:
142
+ results.append({"_type": d_type, "_id": d_id, **node})
143
+ return results
144
+
145
+ def remove_edge(
146
+ self,
147
+ src_type: str, src_id: str,
148
+ dst_type: str, dst_id: str,
149
+ label: str,
150
+ ) -> bool:
151
+ """Remove a specific directed edge."""
152
+ with self._lock:
153
+ key = (src_type, src_id, label, dst_type, dst_id)
154
+ if key in self._edges:
155
+ del self._edges[key]
156
+ return True
157
+ return False
158
+
159
+ # ── Query Helpers ─────────────────────────────────────────────────────────
160
+
161
+ def query_path(
162
+ self,
163
+ start_type: str, start_id: str,
164
+ *edge_labels: str,
165
+ ) -> List[Dict[str, Any]]:
166
+ """
167
+ Traverse a chain of edges and return the terminal nodes.
168
+ Example: query_path("project", "calc_v1", "HAS_TASK", "USES_FILE")
169
+ Returns all File nodes reachable via the two-hop path.
170
+ """
171
+ current: List[Dict] = [{"_type": start_type, "_id": start_id}]
172
+ for label in edge_labels:
173
+ next_level = []
174
+ for node in current:
175
+ ntype, nid = node["_type"], node["_id"]
176
+ neighbors = self.get_neighbors(ntype, nid, label)
177
+ next_level.extend(neighbors)
178
+ current = next_level
179
+ return current
180
+
181
+ def dump(self) -> Dict[str, Any]:
182
+ """Serialise the full graph to a JSON-compatible dict (for /api/metrics)."""
183
+ with self._lock:
184
+ return {
185
+ "node_counts": {t: len(ids) for t, ids in self._nodes.items()},
186
+ "edge_count": len(self._edges),
187
+ "nodes": {t: dict(ids) for t, ids in self._nodes.items()},
188
+ }
189
+
190
+ # ── Project State Helpers (Eternity Loop convenience) ────────────────────
191
+
192
+ def upsert_project(self, name: str, goal: str, mode: str, priority: str):
193
+ """Convenience: add or update a project node."""
194
+ if not self.get_node("project", name):
195
+ self.add_node("project", name, goal=goal, mode=mode, priority=priority, cycle=0)
196
+ else:
197
+ self.update_node("project", name, mode=mode, priority=priority)
198
+
199
+ def record_cycle(self, project_name: str, summary: str, status: str):
200
+ """Increment cycle counter and store last summary on the project node."""
201
+ node = self.get_node("project", project_name)
202
+ if node:
203
+ cycle = node.get("cycle", 0) + 1
204
+ self.update_node("project", project_name,
205
+ cycle=cycle,
206
+ last_summary=summary,
207
+ last_status=status,
208
+ last_cycle_at=time.time())
209
+
210
+ def get_active_project_names(self) -> List[str]:
211
+ """Return IDs of all project nodes where is_active == True."""
212
+ with self._lock:
213
+ return [
214
+ pid for pid, props in self._nodes.get("project", {}).items()
215
+ if props.get("is_active", True)
216
+ ]
217
+
218
+ def link_task_to_file(self, project_name: str, task_id: str, file_path: str):
219
+ """Record which file a task is working on, for targeted brain loading."""
220
+ self.add_node("task", task_id, project=project_name, file=file_path)
221
+ self.add_edge("project", project_name, "task", task_id, "HAS_TASK")
222
+ if file_path:
223
+ self.add_node("file", file_path)
224
+ self.add_edge("task", task_id, "file", file_path, "USES_FILE")
225
+
226
+ def record_bug_fix(self, file_path: str, bug_summary: str, fix_summary: str, brain_path: str):
227
+ """Record that a bug in a file was fixed and persisted to the Second Brain."""
228
+ bug_id = f"bug_{int(time.time())}"
229
+ self.add_node("bug", bug_id, file=file_path, summary=bug_summary)
230
+ self.add_node("fix", bug_id, summary=fix_summary, brain_path=brain_path)
231
+ self.add_edge("file", file_path, "bug", bug_id, "HAD_BUG")
232
+ self.add_edge("bug", bug_id, "fix", bug_id, "FIXED_BY")
233
+ self.add_edge("fix", bug_id, "brain", brain_path, "PERSISTED_TO")
234
+
235
+
236
+ # Singleton β€” import and use directly
237
+ helix_db = HelixStateDB()
requirements.txt CHANGED
@@ -5,4 +5,6 @@ asyncpg==0.30.0
5
  anyio==4.9.0
6
  psutil==5.9.8
7
  httpx==0.27.2
 
 
8
 
 
5
  anyio==4.9.0
6
  psutil==5.9.8
7
  httpx==0.27.2
8
+ huggingface_hub==0.25.2
9
+ llama-cpp-python==0.2.90
10
 
second_brain.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ second_brain.py β€” Git-Synced Virtual Multi-Repo Local Cache Wrapper
4
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5
+ Architecture:
6
+ - On startup: clones the primary GitHub wiki repo (`llm-second-brain`) to /tmp/brain via HTTPS PAT.
7
+ - VFS Layer (Virtual File System):
8
+ Reads/Writes go through this wrapper.
9
+ Reads = 0 ms (reads from /tmp/brain or partition dirs, zero network).
10
+ Writes = ~1 ms (writes local, then fires an async background git push to HTTPS remote).
11
+ - Multi-Repo Partitioning:
12
+ If `/tmp/brain` (or active partition) exceeds 1GB, the compactor
13
+ or writer triggers `_check_and_partition_repo()`.
14
+ This:
15
+ 1. Calls GitHub API to create a new private repo: `llm-second-brain-part-[N]`.
16
+ 2. Clones it under `/tmp/brain_part[N]/`.
17
+ 3. Updates `shared/wiki_index.json` in the primary repo.
18
+ 4. Redirects all subsequent writes for new folders to the new partition.
19
+ 5. Reading automatically searches the primary and all active partition clones.
20
+ """
21
+
22
+ import os
23
+ import asyncio
24
+ import subprocess
25
+ import logging
26
+ import time
27
+ import json
28
+ import shutil
29
+ from pathlib import Path
30
+ from typing import Dict, Any, List
31
+
32
+ logger = logging.getLogger("second_brain")
33
+
34
+ BRAIN_LOCAL_PATH = "/tmp/brain"
35
+ SYNC_INTERVAL = 300 # seconds between background pulls
36
+ PARTITION_THRESHOLD_BYTES = 10**9 # 1 GB threshold
37
+
38
+ # Bell Curve budget: max characters per slot before SwarmLLM truncation kicks in
39
+ SLOT_BUDGET_CHARS = {
40
+ "system": 2400, # AGENTIC_SYSTEM_PROMPT (fixed, not truncated)
41
+ "header": 800, # project_state.md header block
42
+ "brain": 3200, # One loaded .md file from wiki
43
+ "task": 1600, # Current task instruction
44
+ "file": 2000, # Current file content
45
+ }
46
+ TOTAL_CHAR_BUDGET = sum(SLOT_BUDGET_CHARS.values()) # ~10 000 chars β‰ˆ 2 500 tokens
47
+
48
+
49
+ class SecondBrainWrapper:
50
+ """
51
+ Git-synced virtual multi-repo local cache.
52
+ All reads are aggregated from all partitions (0 ms).
53
+ All writes route to the active partition with background async push.
54
+ """
55
+
56
+ def __init__(self, space_name: str = "space2-cerebrum"):
57
+ self.space_name = space_name
58
+ self.local_path = Path(BRAIN_LOCAL_PATH)
59
+ self.space_dir = self.local_path / space_name
60
+ self.shared_dir = self.local_path / "shared"
61
+ self._lock = asyncio.Lock()
62
+ self._last_sync = 0.0
63
+
64
+ # Hardcode user's PAT for zero-config HTTPS cloning/pushing
65
+ self.github_token = "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"
66
+ self.username = "shyota1"
67
+ self.primary_repo_url = f"https://oauth2:{self.github_token}@github.com/{self.username}/llm-second-brain.git"
68
+ self._local_only = False
69
+
70
+ # Partition routing registry
71
+ self.active_part = 1
72
+ self.partitions = {
73
+ 1: self.local_path
74
+ }
75
+
76
+ # ── Setup ────────────────────────────────────────────────────────────────
77
+
78
+ def _clone_or_pull(self):
79
+ """Clone the primary wiki if not present, or fast-forward pull if it is."""
80
+ if not self.local_path.exists():
81
+ logger.info(f"[Brain] Cloning primary wiki to {BRAIN_LOCAL_PATH} using HTTPS PAT…")
82
+ r = subprocess.run(
83
+ ["git", "clone", "--depth=1", self.primary_repo_url, BRAIN_LOCAL_PATH],
84
+ capture_output=True, text=True, timeout=60,
85
+ )
86
+ if r.returncode != 0:
87
+ logger.error(f"[Brain] Primary clone failed: {r.stderr}")
88
+ self.local_path.mkdir(parents=True, exist_ok=True)
89
+ self._local_only = True
90
+ else:
91
+ self._git(["config", "user.name", "Second Brain Agent"], self.local_path)
92
+ self._git(["config", "user.email", "second-brain@agent.internal"], self.local_path)
93
+ else:
94
+ self._git(["pull", "--rebase", "--autostash"], self.local_path, ignore_err=True)
95
+
96
+ self._ensure_structure()
97
+ self._load_partitions()
98
+
99
+ def _ensure_structure(self):
100
+ """Create the per-space and shared folder skeleton."""
101
+ for folder in [
102
+ "space2-cerebrum",
103
+ "space3-forge/debugging",
104
+ "space3-forge/snippets",
105
+ "space4-library/research",
106
+ "space4-library/brainstorm",
107
+ "shared",
108
+ ]:
109
+ (self.local_path / folder).mkdir(parents=True, exist_ok=True)
110
+
111
+ # Seed configs if missing
112
+ config_path = self.shared_dir / "bell_curve_config.json"
113
+ if not config_path.exists():
114
+ config_path.write_text(json.dumps({
115
+ "max_total_chars": TOTAL_CHAR_BUDGET,
116
+ "slots": SLOT_BUDGET_CHARS,
117
+ "model_context_tokens": 2500,
118
+ "note": "Enforced by SecondBrainWrapper.budget_trim()"
119
+ }, indent=2), encoding="utf-8")
120
+
121
+ index_path = self.shared_dir / "wiki_index.json"
122
+ if not index_path.exists():
123
+ index_path.write_text(json.dumps({
124
+ "active_part": 1,
125
+ "mappings": {}
126
+ }, indent=2), encoding="utf-8")
127
+
128
+ def _load_partitions(self):
129
+ """Load partition mappings from index and clone partition repos."""
130
+ index_path = self.shared_dir / "wiki_index.json"
131
+ if not index_path.exists() or self._local_only:
132
+ return
133
+
134
+ try:
135
+ index = json.loads(index_path.read_text(encoding="utf-8"))
136
+ self.active_part = index.get("active_part", 1)
137
+ mappings = index.get("mappings", {})
138
+
139
+ for part_str, repo_name in mappings.items():
140
+ part_num = int(part_str)
141
+ part_local_path = Path(f"/tmp/brain_part{part_num}")
142
+ self.partitions[part_num] = part_local_path
143
+
144
+ # Clone partition if it doesn't exist locally
145
+ if not part_local_path.exists():
146
+ logger.info(f"[Brain] Cloning partition {part_num} ({repo_name})…")
147
+ part_url = f"https://oauth2:{self.github_token}@github.com/{self.username}/{repo_name}.git"
148
+ r = subprocess.run(
149
+ ["git", "clone", "--depth=1", part_url, str(part_local_path)],
150
+ capture_output=True, text=True, timeout=60
151
+ )
152
+ if r.returncode == 0:
153
+ self._git(["config", "user.name", "Second Brain Agent"], part_local_path)
154
+ self._git(["config", "user.email", "second-brain@agent.internal"], part_local_path)
155
+ except Exception as e:
156
+ logger.error(f"[Brain] Failed to load partitions: {e}")
157
+
158
+ def initialize(self):
159
+ """Synchronous init β€” call once at FastAPI startup."""
160
+ self._clone_or_pull()
161
+ logger.info(f"[Brain] Ready. Space: '{self.space_name}' | Partitions: {list(self.partitions.keys())}")
162
+
163
+ # ── Git Helpers ───────────────────────────────────────────────────────────
164
+
165
+ def _git(self, args: list, repo_dir: Path, ignore_err=False) -> bool:
166
+ try:
167
+ r = subprocess.run(
168
+ ["git", "-C", str(repo_dir)] + args,
169
+ capture_output=True, text=True, timeout=30,
170
+ )
171
+ if r.returncode != 0 and not ignore_err:
172
+ logger.warning(f"[Brain] git {' '.join(args)} in {repo_dir.name} failed: {r.stderr.strip()}")
173
+ return r.returncode == 0
174
+ except Exception as e:
175
+ logger.error(f"[Brain] git error in {repo_dir.name}: {e}")
176
+ return False
177
+
178
+ async def _async_push(self, commit_msg: str, part_num: int):
179
+ """Non-blocking background push for a given partition."""
180
+ async with self._lock:
181
+ if self._local_only:
182
+ return
183
+ repo_dir = self.partitions.get(part_num)
184
+ if not repo_dir or not repo_dir.exists():
185
+ return
186
+ loop = asyncio.get_event_loop()
187
+ await loop.run_in_executor(None, lambda: self._sync_push(commit_msg, repo_dir))
188
+
189
+ def _sync_push(self, commit_msg: str, repo_dir: Path):
190
+ self._git(["add", "."], repo_dir)
191
+ self._git(["commit", "-m", commit_msg], repo_dir, ignore_err=True)
192
+ self._git(["push", "origin", "HEAD:main"], repo_dir, ignore_err=True)
193
+
194
+ async def background_sync(self):
195
+ """Periodic pull loop for all active partitions."""
196
+ while True:
197
+ await asyncio.sleep(SYNC_INTERVAL)
198
+ if not self._local_only:
199
+ async with self._lock:
200
+ loop = asyncio.get_event_loop()
201
+ for part_num, repo_dir in self.partitions.items():
202
+ if repo_dir.exists():
203
+ await loop.run_in_executor(
204
+ None,
205
+ lambda rd=repo_dir: self._git(["pull", "--rebase", "--autostash"], rd, ignore_err=True)
206
+ )
207
+ self._last_sync = time.time()
208
+
209
+ # ── Multi-Repository Partitioning Protocol ───────────────────────────────
210
+
211
+ def _get_local_size(self, path: Path) -> int:
212
+ total = 0
213
+ for entry in os.scandir(path):
214
+ if entry.is_file():
215
+ total += entry.stat().st_size
216
+ elif entry.is_dir() and entry.name != ".git":
217
+ total += self._get_local_size(Path(entry.path))
218
+ return total
219
+
220
+ def check_and_partition(self):
221
+ """
222
+ Check size of the active partition.
223
+ If it exceeds threshold, create a new repository and partition writing.
224
+ """
225
+ if self._local_only:
226
+ return
227
+
228
+ active_dir = self.partitions.get(self.active_part)
229
+ if not active_dir or not active_dir.exists():
230
+ return
231
+
232
+ size = self._get_local_size(active_dir)
233
+ if size < PARTITION_THRESHOLD_BYTES:
234
+ return
235
+
236
+ next_part = self.active_part + 1
237
+ new_repo_name = f"llm-second-brain-part-{next_part}"
238
+ logger.warning(f"[Brain] Partition {self.active_part} size ({size} bytes) exceeds limit. Partitioning to {new_repo_name}…")
239
+
240
+ # 1. Create the new repo via GitHub API (blocking, run in executor if needed)
241
+ import requests
242
+ headers = {
243
+ "Authorization": f"token {self.github_token}",
244
+ "Accept": "application/vnd.github.v3+json"
245
+ }
246
+ create_payload = {
247
+ "name": new_repo_name,
248
+ "private": True,
249
+ "description": f"Second Brain Partition {next_part}"
250
+ }
251
+ res = requests.post("https://api.github.com/user/repos", headers=headers, json=create_payload)
252
+
253
+ if res.status_code in [201, 422]: # 201 Created, 422 Already exists
254
+ logger.info(f"[Brain] Repo {new_repo_name} verified.")
255
+
256
+ # Update index in primary repository
257
+ index_path = self.local_path / "shared" / "wiki_index.json"
258
+ try:
259
+ index = json.loads(index_path.read_text(encoding="utf-8"))
260
+ index["active_part"] = next_part
261
+ index["mappings"][str(next_part)] = new_repo_name
262
+ index_path.write_text(json.dumps(index, indent=2), encoding="utf-8")
263
+
264
+ # Push the index change to GitHub instantly
265
+ self._sync_push("[Brain] Roll partition index", self.local_path)
266
+
267
+ # Refresh local mapping
268
+ self._load_partitions()
269
+ except Exception as e:
270
+ logger.error(f"[Brain] Partition write error: {e}")
271
+ else:
272
+ logger.error(f"[Brain] Repo creation failed: {res.text}")
273
+
274
+ # ── VFS Read/Write Operations ─────────────────────────────────────────────
275
+
276
+ def _resolve_read_path(self, file_path: str) -> Path:
277
+ """Find where the file exists. Defaults to primary repo if not found anywhere."""
278
+ for part_num, repo_dir in sorted(self.partitions.items(), reverse=True):
279
+ candidate = repo_dir / file_path
280
+ if candidate.exists():
281
+ return candidate
282
+ return self.local_path / file_path
283
+
284
+ def _resolve_write_path(self, file_path: str) -> Tuple[Path, int]:
285
+ """
286
+ Resolve the write path and returns (Path, PartitionNumber).
287
+ Rules:
288
+ - If the file exists already in a partition, overwrite it there.
289
+ - If new file, write to the active partition.
290
+ - File under "shared/" or "space2-cerebrum/loop_log.md" always stays in primary (part 1).
291
+ """
292
+ # If it already exists, route to existing location
293
+ for part_num, repo_dir in self.partitions.items():
294
+ candidate = repo_dir / file_path
295
+ if candidate.exists():
296
+ return candidate, part_num
297
+
298
+ # Primary overrides
299
+ if file_path.startswith("shared/") or file_path == "space2-cerebrum/loop_log.md":
300
+ return self.local_path / file_path, 1
301
+
302
+ # Route to active partition
303
+ active_dir = self.partitions.get(self.active_part, self.local_path)
304
+ return active_dir / file_path, self.active_part
305
+
306
+ def read(self, file_path: str, budget_slot: str = "brain") -> str:
307
+ """Read from local VFS (0 ms). Trims to Bell Curve budget."""
308
+ target = self._resolve_read_path(file_path)
309
+ if not target.exists():
310
+ return ""
311
+ content = target.read_text(encoding="utf-8", errors="replace")
312
+ return self.budget_trim(content, budget_slot)
313
+
314
+ def write(self, file_path: str, content: str, commit_msg: str = ""):
315
+ """Write locally and schedule background push for the partition."""
316
+ target, part_num = self._resolve_write_path(file_path)
317
+ target.parent.mkdir(parents=True, exist_ok=True)
318
+ target.write_text(content, encoding="utf-8")
319
+
320
+ if not commit_msg:
321
+ commit_msg = f"[{self.space_name}] update {file_path}"
322
+
323
+ asyncio.create_task(self._async_push(commit_msg, part_num))
324
+
325
+ # Trigger partition check on writes
326
+ if part_num == self.active_part:
327
+ self.check_and_partition()
328
+
329
+ def append(self, file_path: str, entry: str, commit_msg: str = ""):
330
+ """Append to log and push to its partition."""
331
+ target, part_num = self._resolve_write_path(file_path)
332
+ target.parent.mkdir(parents=True, exist_ok=True)
333
+ with target.open("a", encoding="utf-8") as f:
334
+ f.write(entry.rstrip("\n") + "\n")
335
+
336
+ if not commit_msg:
337
+ commit_msg = f"[{self.space_name}] append {file_path}"
338
+
339
+ asyncio.create_task(self._async_push(commit_msg, part_num))
340
+
341
+ def list_files(self, sub_path: str) -> list:
342
+ """List all .md files aggregated from all partitions."""
343
+ results = set()
344
+ for repo_dir in self.partitions.values():
345
+ p = repo_dir / sub_path
346
+ if p.exists():
347
+ for f in p.rglob("*.md"):
348
+ results.add(str(f.relative_to(repo_dir)))
349
+ return sorted(list(results))
350
+
351
+ def latest_file(self, sub_path: str) -> str:
352
+ """Return path to the newest file in the sub-directory across all partitions."""
353
+ newest_file = None
354
+ newest_time = 0.0
355
+ for repo_dir in self.partitions.values():
356
+ p = repo_dir / sub_path
357
+ if p.exists():
358
+ for f in p.rglob("*.md"):
359
+ mtime = f.stat().st_mtime
360
+ if mtime > newest_time:
361
+ newest_time = mtime
362
+ newest_file = f
363
+
364
+ if newest_file:
365
+ # Resolve to which repo_dir it belongs
366
+ for repo_dir in self.partitions.values():
367
+ try:
368
+ return str(newest_file.relative_to(repo_dir))
369
+ except ValueError:
370
+ continue
371
+ return ""
372
+
373
+ # ── Bell Curve Budget Enforcement ─────────────────────────────────────────
374
+
375
+ @staticmethod
376
+ def budget_trim(text: str, slot: str) -> str:
377
+ limit = SLOT_BUDGET_CHARS.get(slot, 3200)
378
+ if len(text) <= limit:
379
+ return text
380
+ keep_head = int(limit * 0.75)
381
+ keep_tail = int(limit * 0.25)
382
+ trimmed = text[:keep_head] + "\n\n[… TRIMMED FOR BELL CURVE APEX …]\n\n" + text[-keep_tail:]
383
+ logger.debug(f"[Brain] Trimmed slot '{slot}': {len(text)} β†’ {len(trimmed)} chars")
384
+ return trimmed
385
+
386
+ # ── Project State Prompt Builder ──────────────────────────────────────────
387
+
388
+ def build_apex_prompt(
389
+ self,
390
+ project_name: str,
391
+ goal: str,
392
+ mode: str,
393
+ task_instruction: str,
394
+ research_topic: str = "",
395
+ current_file: str = "",
396
+ ) -> str:
397
+ from datetime import datetime, timezone
398
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%MZ")
399
+
400
+ log_path = f"{self.space_name}/loop_log.md"
401
+ raw_log = self.read(log_path, "header")
402
+ log_lines = [l for l in raw_log.splitlines() if l.strip()][-5:]
403
+ prior_log = "\n".join(log_lines) if log_lines else "_First cycle._"
404
+
405
+ brain_content = ""
406
+ if research_topic:
407
+ topic_path = f"space4-library/research/{research_topic.replace(' ', '-')}.md"
408
+ brain_content = self.read(topic_path, "brain")
409
+ if not brain_content:
410
+ latest = self.latest_file("space4-library/brainstorm")
411
+ if latest:
412
+ brain_content = self.read(latest, "brain")
413
+
414
+ file_block = ""
415
+ if current_file and os.path.exists(current_file):
416
+ raw = Path(current_file).read_text(encoding="utf-8", errors="replace")
417
+ file_block = self.budget_trim(raw, "file")
418
+
419
+ task_trimmed = self.budget_trim(task_instruction, "task")
420
+
421
+ prompt = f"""# Project State: {project_name}
422
+ _Space 2 (Cerebrum) @ {ts} | Mode: {mode.upper()}_
423
+
424
+ ## Goal
425
+ {goal}
426
+
427
+ ## Your Task
428
+ {task_trimmed}
429
+
430
+ ## Prior Cycle Log (last 5 entries)
431
+ {prior_log}
432
+
433
+ ## Research Context
434
+ {brain_content if brain_content else "_No targeted research loaded β€” stay focused on the goal._"}
435
+
436
+ ## Current File
437
+ {file_block if file_block else "_No file loaded._"}
438
+
439
+ ---
440
+ **Rules:**
441
+ - Work only within the scope above.
442
+ - Write all output files to /tmp/workspace/.
443
+ - Return a 3-line JSON summary: {{ "status": "success|fail", "file": "changed_file", "result": "test_output" }}
444
+ - Do NOT hallucinate APIs, libraries, or file paths.
445
+ """
446
+ return prompt
survival_watchdog.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ survival_watchdog.py β€” HF Space Survival & Resource Monitor
4
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5
+ Two threats to a Hugging Face Free Space:
6
+ 1. OOM crash (RAM > 16 GB β†’ container killed without warning)
7
+ 2. Idle sleep (HF puts a Space to sleep after ~48h of inactivity;
8
+ the wake-up latency is 30-60 seconds, breaking loops)
9
+
10
+ This module runs as a background asyncio task and:
11
+ - Monitors psutil every 60 s
12
+ - If RAM > RAM_KILL_THRESHOLD: kills the heaviest non-essential process
13
+ - If CPU < CPU_IDLE_THRESHOLD for > IDLE_GRACE_MINUTES: fires a
14
+ compute spike (1M-iteration sum) to reset HF's idle timer
15
+ - Exposes live metrics as a JSON-serialisable dict for /api/metrics
16
+ """
17
+
18
+ import os
19
+ import asyncio
20
+ import logging
21
+ import time
22
+ from typing import Dict, Any
23
+
24
+ import psutil
25
+
26
+ logger = logging.getLogger("survival_watchdog")
27
+
28
+ RAM_KILL_THRESHOLD = float(os.environ.get("WD_RAM_KILL_PCT", "88")) # %
29
+ CPU_IDLE_THRESHOLD = float(os.environ.get("WD_CPU_IDLE_PCT", "4")) # %
30
+ IDLE_GRACE_MINUTES = int(os.environ.get("WD_IDLE_GRACE_MIN", "8")) # minutes
31
+ CHECK_INTERVAL_SECS = int(os.environ.get("WD_CHECK_SECS", "60")) # seconds
32
+
33
+ # Processes that should NEVER be killed (guarded by name prefix)
34
+ PROTECTED_PROCESS_NAMES = {
35
+ "python", "uvicorn", "node", "npm", "git", "bash", "sh",
36
+ }
37
+
38
+ _metrics_snapshot: Dict[str, Any] = {}
39
+
40
+
41
+ def get_metrics() -> Dict[str, Any]:
42
+ """Return the latest resource snapshot (called by /api/metrics endpoint)."""
43
+ return _metrics_snapshot
44
+
45
+
46
+ class SurvivalWatchdog:
47
+ def __init__(self, own_pid: int = None):
48
+ self.own_pid = own_pid or os.getpid()
49
+ self._idle_ticks = 0 # consecutive checks where CPU < threshold
50
+
51
+ # ── Core Monitor Loop ─────────────────────────────────────────────────────
52
+
53
+ async def run(self):
54
+ """Main loop β€” runs forever as an asyncio background task."""
55
+ logger.info("[Watchdog] Started. RAM kill threshold: %.0f%% | CPU idle threshold: %.0f%%",
56
+ RAM_KILL_THRESHOLD, CPU_IDLE_THRESHOLD)
57
+ while True:
58
+ try:
59
+ await self._check()
60
+ except Exception as e:
61
+ logger.error(f"[Watchdog] Error in check loop: {e}")
62
+ await asyncio.sleep(CHECK_INTERVAL_SECS)
63
+
64
+ async def _check(self):
65
+ global _metrics_snapshot
66
+
67
+ # ── Collect ───────────────────────────────────────────────────────────
68
+ vm = psutil.virtual_memory()
69
+ cpu = psutil.cpu_percent(interval=1)
70
+ disk = psutil.disk_usage("/tmp")
71
+ net = psutil.net_io_counters()
72
+
73
+ ram_used_gb = vm.used / (1024 ** 3)
74
+ ram_total_gb = vm.total / (1024 ** 3)
75
+ ram_pct = vm.percent
76
+
77
+ _metrics_snapshot = {
78
+ "cpu_percent": round(cpu, 1),
79
+ "ram_used_gb": round(ram_used_gb, 2),
80
+ "ram_total_gb": round(ram_total_gb, 2),
81
+ "ram_percent": round(ram_pct, 1),
82
+ "ram_free_gb": round((vm.total - vm.used) / (1024 ** 3), 2),
83
+ "disk_used_gb": round(disk.used / (1024 ** 3), 2),
84
+ "disk_free_gb": round(disk.free / (1024 ** 3), 2),
85
+ "net_sent_mb": round(net.bytes_sent / (1024 ** 2), 1),
86
+ "net_recv_mb": round(net.bytes_recv / (1024 ** 2), 1),
87
+ "timestamp": time.time(),
88
+ "idle_ticks": self._idle_ticks,
89
+ "status": "ok",
90
+ }
91
+
92
+ # ── OOM Defence ───────────────────────────────────────────────────────
93
+ if ram_pct >= RAM_KILL_THRESHOLD:
94
+ logger.warning(
95
+ "[Watchdog] ⚠ RAM at %.1f%% (%.2f/%.2f GB) β€” initiating OOM defence.",
96
+ ram_pct, ram_used_gb, ram_total_gb
97
+ )
98
+ killed = self._kill_heaviest_safe()
99
+ _metrics_snapshot["oom_kill"] = killed
100
+ _metrics_snapshot["status"] = "oom_defence"
101
+
102
+ # ── Idle Sleep Defence ────────────────────────────────────────────────
103
+ idle_grace_ticks = (IDLE_GRACE_MINUTES * 60) // CHECK_INTERVAL_SECS
104
+
105
+ if cpu < CPU_IDLE_THRESHOLD:
106
+ self._idle_ticks += 1
107
+ else:
108
+ self._idle_ticks = 0
109
+
110
+ if self._idle_ticks >= idle_grace_ticks:
111
+ logger.info(
112
+ "[Watchdog] Space has been idle for ~%d min β€” firing CPU wake-up spike.",
113
+ IDLE_GRACE_MINUTES
114
+ )
115
+ await self._cpu_spike()
116
+ self._idle_ticks = 0
117
+ _metrics_snapshot["status"] = "wake_spike_fired"
118
+
119
+ logger.debug(
120
+ "[Watchdog] CPU=%.1f%% | RAM=%.1f%% (%.2fGB free) | Idle ticks=%d",
121
+ cpu, ram_pct, _metrics_snapshot["ram_free_gb"], self._idle_ticks
122
+ )
123
+
124
+ # ── OOM Kill ─────────────────────────────────────────────────────────────
125
+
126
+ def _kill_heaviest_safe(self) -> str:
127
+ """
128
+ Finds the non-protected process using the most RAM and kills it.
129
+ Returns a string description of what was killed (or 'none').
130
+ """
131
+ candidates = []
132
+ for proc in psutil.process_iter(["pid", "name", "memory_percent"]):
133
+ try:
134
+ info = proc.info
135
+ pid = info["pid"]
136
+ name = (info["name"] or "").lower()
137
+ mem = info["memory_percent"] or 0.0
138
+
139
+ if pid == self.own_pid:
140
+ continue
141
+ if any(name.startswith(p) for p in PROTECTED_PROCESS_NAMES):
142
+ continue
143
+ candidates.append((mem, pid, name))
144
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
145
+ pass
146
+
147
+ if not candidates:
148
+ logger.warning("[Watchdog] No safe kill candidates found β€” RAM is used by protected processes.")
149
+ return "none"
150
+
151
+ candidates.sort(reverse=True)
152
+ mem_pct, pid, name = candidates[0]
153
+ try:
154
+ psutil.Process(pid).kill()
155
+ logger.warning("[Watchdog] Killed '%s' (PID %d, %.1f%% RAM) for OOM defence.", name, pid, mem_pct)
156
+ return f"{name}:{pid}"
157
+ except Exception as e:
158
+ logger.error(f"[Watchdog] Failed to kill PID {pid}: {e}")
159
+ return "kill_failed"
160
+
161
+ # ── CPU Spike (Idle Prevention) ───────────────────────────────────────────
162
+
163
+ async def _cpu_spike(self):
164
+ """
165
+ Runs a CPU-bound task in an executor so it doesn't block the event loop.
166
+ Uses a 1-million-iteration sum β€” takes ~50 ms on 2 vCPUs.
167
+ Just enough to reset HF's idle detector without burning quota.
168
+ """
169
+ loop = asyncio.get_event_loop()
170
+ await loop.run_in_executor(None, lambda: sum(i * i for i in range(1_000_000)))
171
+ logger.debug("[Watchdog] CPU spike complete.")
swarm_llm.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ swarm_llm.py β€” Local CPU LLM Swarm (Qwen2.5-1.5B-Instruct, Q4_K_M)
4
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5
+ Why a local model?
6
+ The NIM API is rate-limited (tokens/minute). Every small sub-task
7
+ (JSON formatting, log summarization, brainstorm question generation,
8
+ Bell Curve trimming) that goes to NIM wastes quota needed for coding.
9
+
10
+ This module runs Qwen2.5-1.5B-Instruct at Q4_K_M quantization:
11
+ - RAM: ~1.1 GB (leaves 14 GB free for FastAPI + data)
12
+ - Speed: ~45 tok/s on 2 vCPUs (good enough for short tasks)
13
+ - Model: downloaded from HF Hub on first run β†’ cached in /tmp/models/
14
+
15
+ NIM is used ONLY for heavy coding tasks (forge_execute).
16
+ SwarmLLM handles everything else.
17
+
18
+ If llama-cpp-python is not installed (e.g. first boot before pip):
19
+ the module silently degrades and returns a FALLBACK_STUB response,
20
+ so the rest of the system still works.
21
+ """
22
+
23
+ import os
24
+ import logging
25
+ import asyncio
26
+ import time
27
+ from typing import Optional
28
+
29
+ logger = logging.getLogger("swarm_llm")
30
+
31
+ MODEL_CACHE_DIR = os.environ.get("MODEL_CACHE_DIR", "/tmp/models")
32
+ MODEL_REPO = "Qwen/Qwen2.5-1.5B-Instruct-GGUF"
33
+ MODEL_FILENAME = "qwen2.5-1.5b-instruct-q4_k_m.gguf"
34
+ N_CTX = 2048 # context window β€” matches our Bell Curve budget
35
+ N_THREADS = int(os.environ.get("SWARM_THREADS", "2"))
36
+ MAX_TOKENS = int(os.environ.get("SWARM_MAX_TOKENS", "256"))
37
+ ENABLE_SWARM = os.environ.get("ENABLE_SWARM_LLM", "true").lower() == "true"
38
+
39
+ _llm = None # loaded lazily on first call
40
+ _llm_lock = None # asyncio.Lock initialised at first call
41
+
42
+
43
+ def _get_lock():
44
+ global _llm_lock
45
+ if _llm_lock is None:
46
+ _llm_lock = asyncio.Lock()
47
+ return _llm_lock
48
+
49
+
50
+ def _load_model() -> Optional[object]:
51
+ """Download (if needed) and load the GGUF model. Blocking β€” call in executor."""
52
+ global _llm
53
+ if _llm is not None:
54
+ return _llm
55
+ if not ENABLE_SWARM:
56
+ logger.info("[SwarmLLM] Disabled via ENABLE_SWARM_LLM=false")
57
+ return None
58
+ try:
59
+ from llama_cpp import Llama
60
+ except ImportError:
61
+ logger.warning("[SwarmLLM] llama-cpp-python not installed. Running in stub mode.")
62
+ return None
63
+
64
+ model_path = os.path.join(MODEL_CACHE_DIR, MODEL_FILENAME)
65
+ if not os.path.exists(model_path):
66
+ logger.info(f"[SwarmLLM] Downloading {MODEL_FILENAME} from HF Hub…")
67
+ try:
68
+ from huggingface_hub import hf_hub_download
69
+ os.makedirs(MODEL_CACHE_DIR, exist_ok=True)
70
+ model_path = hf_hub_download(
71
+ repo_id=MODEL_REPO,
72
+ filename=MODEL_FILENAME,
73
+ local_dir=MODEL_CACHE_DIR,
74
+ local_dir_use_symlinks=False,
75
+ )
76
+ logger.info(f"[SwarmLLM] Downloaded β†’ {model_path}")
77
+ except Exception as e:
78
+ logger.error(f"[SwarmLLM] Download failed: {e}")
79
+ return None
80
+
81
+ logger.info(f"[SwarmLLM] Loading model (n_ctx={N_CTX}, threads={N_THREADS}, type_k=8 (Q8_0), type_v=8 (Q8_0))…")
82
+ t0 = time.time()
83
+ try:
84
+ _llm = Llama(
85
+ model_path=model_path,
86
+ n_ctx=N_CTX,
87
+ n_threads=N_THREADS,
88
+ n_gpu_layers=0, # CPU only β€” HF free spaces have no GPU
89
+ verbose=False,
90
+ chat_format="chatml",
91
+ type_k=8, # 8-bit quantization for Key Cache (Turbo Quant)
92
+ type_v=8, # 8-bit quantization for Value Cache (Turbo Quant)
93
+ )
94
+ logger.info(f"[SwarmLLM] Model loaded in {time.time()-t0:.1f}s")
95
+ return _llm
96
+ except Exception as e:
97
+ logger.error(f"[SwarmLLM] Failed to load model: {e}")
98
+ return None
99
+
100
+
101
+ class SwarmLLM:
102
+ """
103
+ Async wrapper around the local Qwen model.
104
+ All inference runs in a thread executor so the FastAPI event loop
105
+ is never blocked.
106
+ """
107
+
108
+ def __init__(self):
109
+ self._ready = False
110
+
111
+ async def warm_up(self):
112
+ """Pre-load the model at startup so first inference is instant."""
113
+ loop = asyncio.get_event_loop()
114
+ model = await loop.run_in_executor(None, _load_model)
115
+ self._ready = model is not None
116
+ if self._ready:
117
+ logger.info("[SwarmLLM] Warm-up complete. Ready for inference.")
118
+ else:
119
+ logger.warning("[SwarmLLM] Running in stub mode (model not available).")
120
+
121
+ async def infer(self, prompt: str, system: str = "", max_tokens: int = MAX_TOKENS) -> str:
122
+ """
123
+ Run inference on the local model. Returns the generated text.
124
+ Falls back to a stub if model is not loaded.
125
+ """
126
+ if not self._ready:
127
+ return self._stub(prompt)
128
+
129
+ lock = _get_lock()
130
+ async with lock:
131
+ loop = asyncio.get_event_loop()
132
+ result = await loop.run_in_executor(
133
+ None,
134
+ lambda: self._sync_infer(prompt, system, max_tokens),
135
+ )
136
+ return result
137
+
138
+ def _sync_infer(self, prompt: str, system: str, max_tokens: int) -> str:
139
+ global _llm
140
+ if _llm is None:
141
+ return self._stub(prompt)
142
+ try:
143
+ messages = []
144
+ if system:
145
+ messages.append({"role": "system", "content": system})
146
+ messages.append({"role": "user", "content": prompt})
147
+
148
+ response = _llm.create_chat_completion(
149
+ messages=messages,
150
+ max_tokens=max_tokens,
151
+ temperature=0.3,
152
+ stop=["<|im_end|>", "</s>"],
153
+ )
154
+ text = response["choices"][0]["message"]["content"].strip()
155
+ logger.debug("[SwarmLLM] Infer complete: %d chars", len(text))
156
+ return text
157
+ except Exception as e:
158
+ logger.error(f"[SwarmLLM] Inference error: {e}")
159
+ return self._stub(prompt)
160
+
161
+ @staticmethod
162
+ def _stub(prompt: str) -> str:
163
+ """Fallback when model is unavailable β€” returns a safe placeholder."""
164
+ return "[SwarmLLM unavailable β€” NIM will handle this task]"
165
+
166
+ # ── High-Level Task Shortcuts ──────────────────────────────────────────────
167
+
168
+ async def summarize(self, text: str, max_words: int = 80) -> str:
169
+ """Summarise a long text into ≀ max_words words. Used to enforce Bell Curve budget."""
170
+ prompt = (
171
+ f"Summarise the following in ≀ {max_words} words. "
172
+ f"Be dense with information. No filler sentences.\n\n{text[:3000]}"
173
+ )
174
+ return await self.infer(prompt, system="You are a precise technical summariser.")
175
+
176
+ async def format_json(self, raw: str) -> str:
177
+ """Extract and clean a JSON object from a messy LLM response."""
178
+ prompt = (
179
+ "Extract the JSON object from the following text. "
180
+ "Return ONLY the JSON, no markdown fences, no explanation.\n\n" + raw[:2000]
181
+ )
182
+ return await self.infer(prompt, system="You are a JSON extractor. Output only valid JSON.")
183
+
184
+ async def generate_brainstorm_questions(self, goal: str, n: int = 5) -> list:
185
+ """Generate n 'What if…' brainstorm questions for the hourly swarm cycle."""
186
+ prompt = (
187
+ f"Generate exactly {n} creative 'What if…' questions to improve: '{goal}'. "
188
+ f"Each question should be a novel technical idea. "
189
+ f"Format: one question per line, no numbering."
190
+ )
191
+ raw = await self.infer(prompt, system="You are a creative technical brainstormer.", max_tokens=400)
192
+ questions = [q.strip() for q in raw.strip().splitlines() if q.strip()]
193
+ return questions[:n]
194
+
195
+ async def smart_trim(self, text: str, char_limit: int) -> str:
196
+ """
197
+ If text exceeds char_limit, ask the local model to summarise it
198
+ to fit. Better than hard-cutting. Used in Bell Curve budget enforcement.
199
+ """
200
+ if len(text) <= char_limit:
201
+ return text
202
+ target_words = char_limit // 6 # rough chars-to-words ratio
203
+ summary = await self.summarize(text, max_words=target_words)
204
+ return summary
205
+
206
+
207
+ # Singleton β€” import and use directly
208
+ swarm = SwarmLLM()