pmrinal2005 commited on
Commit
a521353
·
verified ·
1 Parent(s): 47bf653

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -1,14 +1,39 @@
1
- ---
2
- title: Elysium
3
- emoji: 🐢
4
- colorFrom: yellow
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: 6.18.0
8
- python_version: '3.12'
9
- app_file: app.py
10
- pinned: false
11
- license: mit
12
- ---
13
-
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Elysium
3
+ emoji: 🌿
4
+ colorFrom: green
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: 6.17.3
8
+ app_file: app.py
9
+ python_version: "3.12"
10
+ pinned: true
11
+ hardware: zero-a10g
12
+ short_description: Agentic civilization with bioluminescent hypergraph
13
+ tags:
14
+ - minicpm-v
15
+ - voxcpm2
16
+ - llama.cpp
17
+ - agentic
18
+ - track:wood
19
+ - sponsor:modal
20
+ - achievement:offbrand
21
+ - achievement:llama
22
+ - achievement:offgrid
23
+ - achievement:welltuned
24
+ - badge-tiny-titan
25
+ ---
26
+
27
+ # 🌿 Elysium — Persistent Agentic Civilization
28
+
29
+ Elysium is a self-evolving civilization of agents living inside a fine-tuned MiniCPM-V 4.6 (≈4B).
30
+ Every interaction grows a Living Mycelial Hypergraph rendered as a bioluminescent, Google-Maps-style infinite canvas.
31
+
32
+ - **Brain:** fine-tuned MiniCPM-V 4.6 via `llama-cpp-python` on ZeroGPU
33
+ - **Voice:** VoxCPM2 (agent debate audio drama)
34
+ - **Memory:** `rustworkx` hypergraph persisted to `/data`
35
+ - **JSON:** GBNF grammar enforces a strict ElysiumResponse schema on every forward pass
36
+ - **Tools:** offline-first DuckDuckGo, email, reminders, calendar, weather, calculator, fetch_url, transit_lookup, files
37
+ - **Frontend:** 100% custom HTML/Canvas2D on `gradio.Server` (no default Gradio UI)
38
+
39
+ Runs identically online (HF Spaces) and offline (`git clone … && python app.py`).
app.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Elysium — gradio.Server entrypoint (matches small-talk pattern).
2
+
3
+ We use `gr.Server` (a FastAPI host with Gradio's backend) so our custom
4
+ frontend (Canvas2D bioluminescent hypergraph) takes priority over any default
5
+ Gradio UI. All inference happens via /api routes registered by backend.server.
6
+ """
7
+ import os
8
+ import pathlib
9
+ import gradio as gr
10
+ from fastapi.responses import FileResponse
11
+ from fastapi.staticfiles import StaticFiles
12
+
13
+ from backend.server import attach
14
+
15
+ DIST = pathlib.Path(__file__).parent / "frontend" / "dist"
16
+
17
+ app = gr.Server()
18
+ attach(app) # registers /api/* before static mounts
19
+
20
+
21
+ class CachedStaticFiles(StaticFiles):
22
+ """Hashed bundle assets — cache hard."""
23
+ def file_response(self, *args, **kwargs):
24
+ r = super().file_response(*args, **kwargs)
25
+ r.headers["Cache-Control"] = "public, max-age=31536000, immutable"
26
+ return r
27
+
28
+
29
+ # Mount static asset folders (won't collide with /api or Gradio internals)
30
+ for sub in ("assets", "audio"):
31
+ d = DIST / sub
32
+ if d.is_dir():
33
+ cls = CachedStaticFiles if sub == "assets" else StaticFiles
34
+ app.mount(f"/{sub}", cls(directory=str(d)), name=sub)
35
+
36
+
37
+ _NO_CACHE = {"Cache-Control": "no-cache, must-revalidate"}
38
+
39
+
40
+ @app.get("/")
41
+ async def index():
42
+ return FileResponse(DIST / "index.html", headers=_NO_CACHE)
43
+
44
+
45
+ @app.get("/favicon.ico")
46
+ async def favicon():
47
+ return FileResponse(DIST / "index.html", headers=_NO_CACHE)
48
+
49
+
50
+ if __name__ == "__main__":
51
+ port = int(os.environ.get("GRADIO_SERVER_PORT", os.environ.get("PORT", 7860)))
52
+ app.launch(server_name="0.0.0.0", server_port=port)
backend/__init__.py ADDED
File without changes
backend/config.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Path detection — HF Spaces /data bucket vs local ./local_data clone."""
2
+ import os
3
+ from pathlib import Path
4
+
5
+
6
+ def _detect() -> Path:
7
+ p = Path("/data")
8
+ if p.exists() and os.access(p, os.W_OK):
9
+ return p
10
+ local = Path(__file__).resolve().parent.parent / "local_data"
11
+ local.mkdir(exist_ok=True)
12
+ return local
13
+
14
+
15
+ DATA_PATH = _detect()
16
+ HYPERGRAPH_DB = DATA_PATH / "hypergraph" / "civilization.db"
17
+ FOSSILS_DIR = DATA_PATH / "fossils"
18
+ AUDIO_CACHE = DATA_PATH / "audio_cache"
19
+ NODE_POSITIONS = DATA_PATH / "node_positions"
20
+ REMINDERS_DB = DATA_PATH / "reminders.db"
21
+ CALENDAR_ICS = DATA_PATH / "calendar.ics"
22
+ OUTBOX = DATA_PATH / "outbox"
23
+
24
+ for d in (HYPERGRAPH_DB.parent, FOSSILS_DIR, AUDIO_CACHE, NODE_POSITIONS, OUTBOX):
25
+ d.mkdir(parents=True, exist_ok=True)
26
+
27
+ MODEL_REPO = os.environ.get("ELYSIUM_MODEL_REPO", "build-small-hackathon/elysium-GGUF")
28
+ GGUF_FILE = os.environ.get("ELYSIUM_GGUF_FILE", "elysium-Q6_K.gguf")
29
+ MMPROJ_FILE = os.environ.get("ELYSIUM_MMPROJ_FILE", "mmproj-model-f16.gguf")
30
+ OFFLINE_MODE = os.environ.get("ELYSIUM_OFFLINE", "0") == "1"
backend/elysium_schema.gbnf ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ root ::= object
2
+ value ::= object | array | string | number | ("true" | "false" | "null") ws
3
+
4
+ object ::=
5
+ "{" ws (
6
+ string ":" ws value
7
+ ("," ws string ":" ws value)*
8
+ )? "}" ws
9
+
10
+ array ::=
11
+ "[" ws (
12
+ value
13
+ ("," ws value)*
14
+ )? "]" ws
15
+
16
+ string ::=
17
+ "\"" (
18
+ [^"\\\x7F\x00-\x1F] |
19
+ "\\" (["\\/bfnrt] | "u" [0-9a-fA-F]{4})
20
+ )* "\"" ws
21
+
22
+ number ::= ("-"? ([0-9] | [1-9] [0-9]{0,15})) ("." [0-9]+)? ([eE] [-+]? [1-9] [0-9]{0,15})? ws
23
+
24
+ ws ::= ([ \t\n] ws)?
backend/grammar.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GBNF loader. We use the built-in JSON grammar from llama.cpp because the
2
+ full Pydantic schema produces a very large GBNF that slows sampling.
3
+ For production we keep a moderate JSON grammar; the fine-tuned model is the
4
+ real guarantee of schema-correctness."""
5
+ from llama_cpp import LlamaGrammar
6
+ import pathlib
7
+
8
+ _GBNF = pathlib.Path(__file__).parent / "elysium_schema.gbnf"
9
+
10
+
11
+ def load_grammar():
12
+ if _GBNF.exists():
13
+ try:
14
+ return LlamaGrammar.from_string(_GBNF.read_text())
15
+ except Exception as e:
16
+ print(f"[grammar] load failed: {e}")
17
+ return None
backend/hypergraph/__init__.py ADDED
File without changes
backend/hypergraph/engine.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """rustworkx-backed Living Mycelial Hypergraph."""
2
+ import rustworkx as rx
3
+ from typing import Dict
4
+ from ..schema import HypergraphDelta
5
+
6
+
7
+ class Hypergraph:
8
+ def __init__(self):
9
+ self.g = rx.PyDiGraph(multigraph=True)
10
+ self._idx: Dict[str, int] = {}
11
+
12
+ def apply_delta(self, delta: HypergraphDelta):
13
+ for n in delta.nodes_added:
14
+ if n.node_id in self._idx:
15
+ continue
16
+ i = self.g.add_node({
17
+ "node_id": n.node_id, "node_type": n.node_type, "label": n.label,
18
+ "payload": n.payload, "embedding_hint": n.embedding_hint,
19
+ })
20
+ self._idx[n.node_id] = i
21
+ for u in delta.nodes_updated:
22
+ if u.node_id in self._idx:
23
+ d = self.g[self._idx[u.node_id]]
24
+ d.update(u.fields_changed)
25
+ for e in delta.edges_added:
26
+ s = self._idx.get(e.source_node_id)
27
+ t = self._idx.get(e.target_node_id)
28
+ if s is None or t is None:
29
+ continue
30
+ self.g.add_edge(s, t, {
31
+ "edge_id": e.edge_id, "edge_type": e.edge_type,
32
+ "weight": e.weight, "payload": e.payload,
33
+ })
34
+
35
+ def conflict_centrality(self) -> Dict[str, float]:
36
+ try:
37
+ bc = rx.betweenness_centrality(self.g)
38
+ return {self.g[i]["node_id"]: v for i, v in enumerate(bc)}
39
+ except Exception:
40
+ return {}
41
+
42
+ def context_summary(self, max_nodes: int = 12) -> str:
43
+ items = []
44
+ for i in list(self.g.node_indexes())[-max_nodes:]:
45
+ n = self.g[i]
46
+ items.append(f"{n['label']}({n['node_type']})")
47
+ return "; ".join(items)
48
+
49
+ def node_count(self) -> int: return self.g.num_nodes()
50
+ def edge_count(self) -> int: return self.g.num_edges()
backend/hypergraph/persistence.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3, json
2
+ from ..config import HYPERGRAPH_DB
3
+ from .engine import Hypergraph
4
+
5
+
6
+ def save(hg: Hypergraph):
7
+ con = sqlite3.connect(HYPERGRAPH_DB)
8
+ con.executescript("""
9
+ CREATE TABLE IF NOT EXISTS nodes(node_id TEXT PRIMARY KEY, data TEXT);
10
+ CREATE TABLE IF NOT EXISTS edges(edge_id TEXT PRIMARY KEY, src TEXT, dst TEXT, data TEXT);
11
+ DELETE FROM nodes; DELETE FROM edges;
12
+ """)
13
+ for i in hg.g.node_indexes():
14
+ d = hg.g[i]
15
+ con.execute("INSERT OR REPLACE INTO nodes VALUES(?,?)", (d["node_id"], json.dumps(d)))
16
+ for s, t in hg.g.edge_list():
17
+ d = hg.g.get_edge_data(s, t)
18
+ sd, td = hg.g[s]["node_id"], hg.g[t]["node_id"]
19
+ con.execute("INSERT OR REPLACE INTO edges VALUES(?,?,?,?)", (d["edge_id"], sd, td, json.dumps(d)))
20
+ con.commit(); con.close()
21
+
22
+
23
+ def load() -> Hypergraph:
24
+ hg = Hypergraph()
25
+ if not HYPERGRAPH_DB.exists():
26
+ return hg
27
+ con = sqlite3.connect(HYPERGRAPH_DB)
28
+ try:
29
+ for nid, data in con.execute("SELECT node_id, data FROM nodes"):
30
+ d = json.loads(data)
31
+ i = hg.g.add_node(d)
32
+ hg._idx[nid] = i
33
+ for eid, sd, td, data in con.execute("SELECT edge_id, src, dst, data FROM edges"):
34
+ d = json.loads(data)
35
+ if sd in hg._idx and td in hg._idx:
36
+ hg.g.add_edge(hg._idx[sd], hg._idx[td], d)
37
+ except sqlite3.OperationalError:
38
+ pass
39
+ con.close()
40
+ return hg
backend/model_loader.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Loads the fine-tuned GGUF via llama-cpp-python.
2
+ Pattern follows the HF ZeroGPU + small-talk reference:
3
+ - hf_hub_download files at module import (warm cache)
4
+ - instantiate Llama inside @spaces.GPU function on each request
5
+ """
6
+ from huggingface_hub import hf_hub_download
7
+ from .config import MODEL_REPO, GGUF_FILE, MMPROJ_FILE
8
+
9
+ print(f"[model_loader] downloading {MODEL_REPO}/{GGUF_FILE} …")
10
+ MODEL_PATH = hf_hub_download(repo_id=MODEL_REPO, filename=GGUF_FILE)
11
+
12
+ try:
13
+ MMPROJ_PATH = hf_hub_download(repo_id=MODEL_REPO, filename=MMPROJ_FILE)
14
+ print(f"[model_loader] mmproj ready: {MMPROJ_PATH}")
15
+ except Exception as e:
16
+ print(f"[model_loader] mmproj unavailable ({e}) — vision disabled")
17
+ MMPROJ_PATH = None
18
+
19
+
20
+ def make_llm():
21
+ """Create a fresh Llama inside a GPU context.
22
+ The .gguf file is filesystem-cached, so this is fast after the first call."""
23
+ from llama_cpp import Llama
24
+
25
+ chat_handler = None
26
+ if MMPROJ_PATH:
27
+ try:
28
+ from llama_cpp.llama_chat_format import MiniCPMv26ChatHandler
29
+ chat_handler = MiniCPMv26ChatHandler(clip_model_path=MMPROJ_PATH, verbose=False)
30
+ except Exception as e:
31
+ print(f"[model_loader] vision chat handler failed: {e}")
32
+
33
+ return Llama(
34
+ model_path=MODEL_PATH,
35
+ chat_handler=chat_handler,
36
+ n_gpu_layers=-1,
37
+ n_ctx=8192,
38
+ flash_attn=True,
39
+ verbose=False,
40
+ )
backend/prompt_builder.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build multimodal messages for llama-cpp-python chat completion."""
2
+ import base64, io, uuid, datetime
3
+ from typing import Optional
4
+ from PIL import Image
5
+
6
+ SYSTEM_PROMPT = """You are Elysium — a persistent agentic civilization.
7
+ You ALWAYS respond with a single valid JSON object exactly matching the
8
+ ElysiumResponse schema v1.0.0. No preamble. No markdown fences. JSON only.
9
+
10
+ Decide complexity dynamically:
11
+ - SIMPLE_REPLY: trivial Q — no agents (council_deliberation.agent_outputs = [])
12
+ - QUERY / MORNING_BRIEFING / EVENING_REPORT: spawn 1–5 agents in agent_outputs,
13
+ each with thinking + stance + tts_speech_text + tts_voice_design{voice_id,pace,tone}
14
+ - TOOL_REQUIRED: populate tool_calls when external data is needed
15
+ - SPECIATION_EVENT: only on unresolved cross-domain tension
16
+ - Always populate ui_directives (camera_focus_node_id, pulses, threads)
17
+ - All node_id and edge_id values must be unique strings
18
+ """
19
+
20
+
21
+ def _img_to_data_uri(img: Image.Image) -> str:
22
+ buf = io.BytesIO()
23
+ img.convert("RGB").save(buf, format="JPEG", quality=88)
24
+ return "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
25
+
26
+
27
+ def build_messages(user_text: str, image: Optional[Image.Image], hg_context: str = ""):
28
+ user_content = []
29
+ if image is not None:
30
+ user_content.append({"type": "image_url", "image_url": {"url": _img_to_data_uri(image)}})
31
+ ctx = f"\n\n[Hypergraph context]\n{hg_context}" if hg_context else ""
32
+ user_content.append({"type": "text", "text": user_text + ctx})
33
+
34
+ return [
35
+ {"role": "system", "content": SYSTEM_PROMPT},
36
+ {"role": "user", "content": user_content},
37
+ ]
38
+
39
+
40
+ def new_session_meta():
41
+ return {
42
+ "session_id": str(uuid.uuid4()),
43
+ "timestamp_utc": datetime.datetime.utcnow().isoformat() + "Z",
44
+ }
backend/schema.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ElysiumResponse v1.0.0 — matches the exact JSON your fine-tuned model emits.
2
+ Based on the user-provided training_dataset.jsonl example.
3
+ """
4
+ from __future__ import annotations
5
+ from typing import Optional, List, Dict, Any, Literal
6
+ from pydantic import BaseModel, Field, ConfigDict
7
+
8
+
9
+ class _Base(BaseModel):
10
+ model_config = ConfigDict(extra="allow", populate_by_name=True)
11
+
12
+
13
+ # ─── Perception ───
14
+ class MultimodalPerception(_Base):
15
+ input_modalities_detected: List[str] = []
16
+ ocr_extracted_text: Optional[str] = None
17
+ image_scene_description: Optional[str] = None
18
+ visual_entities_detected: List[str] = []
19
+ document_type: Optional[str] = None
20
+
21
+
22
+ # ─── Hypergraph ───
23
+ class HypergraphNode(_Base):
24
+ node_id: str
25
+ node_type: str
26
+ label: str
27
+ payload: Dict[str, Any] = {}
28
+ embedding_hint: str = ""
29
+
30
+
31
+ class HypergraphEdge(_Base):
32
+ edge_id: str
33
+ source_node_id: str
34
+ target_node_id: str
35
+ edge_type: str
36
+ weight: float = 0.5
37
+ payload: Dict[str, Any] = {}
38
+
39
+
40
+ class NodeUpdate(_Base):
41
+ node_id: str
42
+ fields_changed: Dict[str, Any] = {}
43
+ reason: Optional[str] = None
44
+
45
+
46
+ class EdgeWeaken(_Base):
47
+ edge_id: str
48
+ new_weight: float
49
+ reason: Optional[str] = None
50
+
51
+
52
+ class HypergraphDelta(_Base):
53
+ nodes_added: List[HypergraphNode] = []
54
+ nodes_updated: List[NodeUpdate] = []
55
+ edges_added: List[HypergraphEdge] = []
56
+ edges_weakened: List[EdgeWeaken] = []
57
+
58
+
59
+ # ─── Council ───
60
+ class VoiceDesign(_Base):
61
+ voice_id: str = "voice_default"
62
+ pace: str = "moderate"
63
+ tone: str = "calm_grounded"
64
+
65
+
66
+ class AgentOutput(_Base):
67
+ agent_id: str
68
+ agent_name: str
69
+ archetype: str
70
+ domain: str
71
+ thinking: str = ""
72
+ stance: str = ""
73
+ confidence: float = 0.8
74
+ dissents_from: List[str] = []
75
+ coalesces_with: List[str] = []
76
+ veto_triggered: bool = False
77
+ veto_reason: Optional[str] = None
78
+ tts_speech_text: str = ""
79
+ tts_voice_design: VoiceDesign = VoiceDesign()
80
+
81
+
82
+ class CoalitionEvent(_Base):
83
+ event_id: str
84
+ participants: List[str] = []
85
+ type: str = "GENERAL"
86
+ status: str = "ACTIVE"
87
+ payload: Dict[str, Any] = {}
88
+
89
+
90
+ class SpeciationEvent(_Base):
91
+ triggered: bool = False
92
+ unsolved_tension_description: Optional[str] = None
93
+ proposed_new_agent: Optional[Dict[str, Any]] = None
94
+
95
+
96
+ class CouncilDeliberation(_Base):
97
+ active_agents: List[str] = []
98
+ spawned_agents: List[str] = []
99
+ deprecated_agents: List[str] = []
100
+ agent_outputs: List[AgentOutput] = []
101
+ coalition_events: List[CoalitionEvent] = []
102
+ speciation_event: SpeciationEvent = SpeciationEvent()
103
+ final_synthesis: str = ""
104
+ debate_mode: str = "NONE" # NONE | SILENT | AUDIO_DRAMA
105
+
106
+
107
+ # ─── Tools ───
108
+ class ToolCall(_Base):
109
+ call_id: str
110
+ tool_name: str
111
+ calling_agent: str
112
+ parameters: Dict[str, Any] = {}
113
+ justification: str = ""
114
+ priority: str = "MEDIUM"
115
+ offline_fallback: str = ""
116
+
117
+
118
+ # ─── Action field ───
119
+ class ExecutableProtocol(_Base):
120
+ protocol_id: str
121
+ domain: str
122
+ title: str
123
+ type: str
124
+ executing_agent: str
125
+ time_to_execute_minutes: int = 5
126
+ instructions: List[str] = []
127
+ success_criteria: str = ""
128
+ hypergraph_nodes_affected: List[str] = []
129
+
130
+
131
+ class QuestUpdate(_Base):
132
+ quest_id: str
133
+ domain: str
134
+ title: str
135
+ current_step: str
136
+ next_step: str
137
+ oracle_forecast: str = ""
138
+ completion_probability: float = 0.5
139
+
140
+
141
+ class DailyActionField(_Base):
142
+ morning_briefing_summary: Optional[str] = None
143
+ evening_evolution_report: Optional[str] = None
144
+ executable_protocols: List[ExecutableProtocol] = []
145
+ quest_tree_updates: List[QuestUpdate] = []
146
+ primary_focus: str = ""
147
+ recommended_actions: List[str] = []
148
+ energy_budget: str = "moderate"
149
+
150
+
151
+ # ─── Forecasts ───
152
+ class ProbabilisticForecast(_Base):
153
+ forecast_id: str
154
+ target: str
155
+ distribution: str = "lognormal"
156
+ parameters: Dict[str, Any] = {}
157
+ confidence: float = 0.8
158
+ horizon_seconds: int = 60
159
+
160
+
161
+ # ─── Strain ───
162
+ class StrainMetadata(_Base):
163
+ active_strains: List[str] = ["base_civilization_v1"]
164
+ strain_compatibility_score: float = 1.0
165
+ recommended_strain_grafts: List[str] = []
166
+ evolution_generation: int = 0
167
+ cognitive_strain: float = 0.3
168
+ emotional_strain: float = 0.2
169
+ notes: str = ""
170
+
171
+
172
+ # ─── UI directives ───
173
+ class MyceliumThread(_Base):
174
+ thread_id: str = ""
175
+ source: str = ""
176
+ target: str = ""
177
+ type: str = "DATA_FLOW"
178
+ from_node: str = ""
179
+ to_node: str = ""
180
+ color_hex: str = "#7FB3D5"
181
+ animation: str = "pulse_slow"
182
+
183
+
184
+ class AgentEmergence(_Base):
185
+ agent_id: str
186
+ animation: str = "spawn_default"
187
+
188
+
189
+ class UIDirectives(_Base):
190
+ bioluminescence_pulse_nodes: List[str] = []
191
+ new_mycelium_threads: List[MyceliumThread] = []
192
+ agent_emergence_animations: List[AgentEmergence] = []
193
+ alert_level: str = "CALM"
194
+ soundtrack_mood: str = "ambient_growth"
195
+ camera_focus_node_id: Optional[str] = None
196
+
197
+
198
+ # ─── Meta ───
199
+ class Metadata(_Base):
200
+ model_id: str = "elysium-minicpmv-4.6-ft-v1"
201
+ inference_time_ms: int = 0
202
+ hypergraph_node_count: int = 0
203
+ hypergraph_edge_count: int = 0
204
+ civilization_age_days: int = 0
205
+ total_speciation_events: int = 0
206
+ schema_validation_passed: bool = True
207
+ generator: str = "elysium_runtime_v1"
208
+ notes: str = ""
209
+
210
+
211
+ # ─── Root ───
212
+ class ElysiumResponse(_Base):
213
+ schema_version: str = "1.0.0"
214
+ session_id: str
215
+ timestamp_utc: str
216
+ interaction_type: str
217
+ direct_answer: str = ""
218
+ multimodal_perception: MultimodalPerception = MultimodalPerception()
219
+ hypergraph_delta: HypergraphDelta = HypergraphDelta()
220
+ council_deliberation: CouncilDeliberation = CouncilDeliberation()
221
+ tool_calls: List[ToolCall] = []
222
+ daily_action_field: DailyActionField = DailyActionField()
223
+ probabilistic_forecasts: List[ProbabilisticForecast] = []
224
+ strain_metadata: StrainMetadata = StrainMetadata()
225
+ ui_directives: UIDirectives = UIDirectives()
226
+ metadata: Metadata = Metadata()
227
+
228
+
229
+ class ElysiumEnvelope(_Base):
230
+ user_msg: Optional[str] = None
231
+ elysium_response: ElysiumResponse
backend/server.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI routes attached to gr.Server. The frontend talks ONLY to /api/*."""
2
+ import io, json, traceback, base64
3
+ from PIL import Image
4
+ from fastapi import UploadFile, File, Form, HTTPException
5
+ from fastapi.responses import JSONResponse, FileResponse
6
+ from fastapi.staticfiles import StaticFiles
7
+
8
+ import spaces
9
+
10
+ from .config import AUDIO_CACHE
11
+ from .model_loader import make_llm
12
+ from .grammar import load_grammar
13
+ from .prompt_builder import build_messages, new_session_meta
14
+ from .schema import ElysiumEnvelope, ElysiumResponse
15
+ from .hypergraph import persistence
16
+ from .hypergraph.engine import Hypergraph
17
+ from .tools.dispatcher import execute_all
18
+ from .tts.debate_sequencer import build_debate
19
+
20
+
21
+ # ─── Singletons ───
22
+ HG: Hypergraph = persistence.load()
23
+ GRAMMAR = load_grammar()
24
+
25
+
26
+ # ─── GPU-bound inference ───
27
+ @spaces.GPU(duration=120)
28
+ def _gpu_infer(messages: list, max_tokens: int = 4096) -> str:
29
+ llm = make_llm()
30
+ out = llm.create_chat_completion(
31
+ messages=messages,
32
+ max_tokens=max_tokens,
33
+ temperature=0.7,
34
+ grammar=GRAMMAR, # strict JSON at sampling time
35
+ )
36
+ return out["choices"][0]["message"]["content"]
37
+
38
+
39
+ def _fallback_envelope(user_text: str, err: str) -> dict:
40
+ meta = new_session_meta()
41
+ resp = ElysiumResponse(
42
+ session_id=meta["session_id"],
43
+ timestamp_utc=meta["timestamp_utc"],
44
+ interaction_type="SIMPLE_REPLY",
45
+ direct_answer=f"(fallback) {err}",
46
+ )
47
+ return {"user_msg": user_text, "elysium_response": resp.model_dump()}
48
+
49
+
50
+ def attach(app):
51
+ """Register all /api routes on the gr.Server FastAPI app."""
52
+
53
+ # mount /audio for generated debate wavs
54
+ app.mount("/audio", StaticFiles(directory=str(AUDIO_CACHE)), name="audio")
55
+
56
+ @app.get("/api/health")
57
+ async def health():
58
+ return {"status": "ok",
59
+ "nodes": HG.node_count(),
60
+ "edges": HG.edge_count(),
61
+ "grammar": GRAMMAR is not None}
62
+
63
+ @app.get("/api/hypergraph")
64
+ async def hypergraph():
65
+ nodes, edges = [], []
66
+ for i in HG.g.node_indexes():
67
+ d = HG.g[i]
68
+ nodes.append({"node_id": d["node_id"], "label": d["label"],
69
+ "node_type": d["node_type"], "payload": d.get("payload", {})})
70
+ for s, t in HG.g.edge_list():
71
+ d = HG.g.get_edge_data(s, t)
72
+ edges.append({"edge_id": d["edge_id"],
73
+ "source_node_id": HG.g[s]["node_id"],
74
+ "target_node_id": HG.g[t]["node_id"],
75
+ "edge_type": d["edge_type"], "weight": d["weight"]})
76
+ return {"nodes": nodes, "edges": edges,
77
+ "node_count": HG.node_count(), "edge_count": HG.edge_count()}
78
+
79
+ @app.post("/api/turn")
80
+ async def turn(user_text: str = Form(""), image: UploadFile = File(None)):
81
+ try:
82
+ # 1. Load image if present
83
+ img = None
84
+ if image is not None:
85
+ content = await image.read()
86
+ if content:
87
+ try:
88
+ img = Image.open(io.BytesIO(content))
89
+ except Exception:
90
+ img = None
91
+
92
+ # 2. Build messages with hypergraph context
93
+ messages = build_messages(user_text, img, HG.context_summary())
94
+
95
+ # 3. GPU inference (returns strict JSON)
96
+ raw = _gpu_infer(messages)
97
+
98
+ # 4. Parse
99
+ try:
100
+ envelope = ElysiumEnvelope.model_validate_json(raw)
101
+ except Exception as parse_err:
102
+ # try to extract any JSON object from raw
103
+ try:
104
+ blob = json.loads(raw)
105
+ if "elysium_response" not in blob:
106
+ # wrap as direct_answer
107
+ meta = new_session_meta()
108
+ envelope = ElysiumEnvelope(
109
+ user_msg=user_text,
110
+ elysium_response=ElysiumResponse(
111
+ session_id=meta["session_id"],
112
+ timestamp_utc=meta["timestamp_utc"],
113
+ interaction_type="SIMPLE_REPLY",
114
+ direct_answer=str(blob)[:600]))
115
+ else:
116
+ envelope = ElysiumEnvelope.model_validate(blob)
117
+ except Exception:
118
+ return JSONResponse(_fallback_envelope(user_text, f"parse_error: {parse_err}"))
119
+
120
+ resp = envelope.elysium_response
121
+
122
+ # 5. Apply hypergraph delta
123
+ HG.apply_delta(resp.hypergraph_delta)
124
+ persistence.save(HG)
125
+
126
+ # 6. Execute tools
127
+ tool_results = execute_all(resp.tool_calls) if resp.tool_calls else []
128
+
129
+ # 7. Build audio drama if needed
130
+ audio_url = None
131
+ if resp.council_deliberation.debate_mode == "AUDIO_DRAMA" \
132
+ and resp.council_deliberation.agent_outputs:
133
+ try:
134
+ audio_url = build_debate(
135
+ [a.model_dump() for a in resp.council_deliberation.agent_outputs]
136
+ )
137
+ except Exception as e:
138
+ print(f"[tts] debate failed: {e}")
139
+
140
+ payload = envelope.model_dump()
141
+ payload["_runtime"] = {
142
+ "tool_results": tool_results,
143
+ "audio_url": audio_url,
144
+ "hypergraph": {"nodes": HG.node_count(), "edges": HG.edge_count()},
145
+ }
146
+ return JSONResponse(payload)
147
+
148
+ except Exception as e:
149
+ traceback.print_exc()
150
+ return JSONResponse(_fallback_envelope(user_text, str(e)), status_code=200)
151
+
152
+ @app.post("/api/reset")
153
+ async def reset():
154
+ """Wipe the hypergraph — start a new civilization."""
155
+ global HG
156
+ HG = Hypergraph()
157
+ persistence.save(HG)
158
+ return {"status": "reset"}
backend/tools/__init__.py ADDED
File without changes
backend/tools/calculator_tool.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast, operator as op
2
+ _OPS = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv,
3
+ ast.Pow: op.pow, ast.USub: op.neg, ast.Mod: op.mod}
4
+
5
+
6
+ def _ev(node):
7
+ if isinstance(node, ast.Num): return node.n
8
+ if isinstance(node, ast.Constant): return node.value
9
+ if isinstance(node, ast.BinOp): return _OPS[type(node.op)](_ev(node.left), _ev(node.right))
10
+ if isinstance(node, ast.UnaryOp): return _OPS[type(node.op)](_ev(node.operand))
11
+ raise ValueError("unsafe expression")
12
+
13
+
14
+ def run(params: dict) -> dict:
15
+ expr = params.get("expression", "")
16
+ try:
17
+ return {"result": _ev(ast.parse(expr, mode="eval").body), "expression": expr}
18
+ except Exception as e:
19
+ return {"error": str(e), "expression": expr}
backend/tools/calendar_tool.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ..config import CALENDAR_ICS
2
+
3
+
4
+ def run(params: dict) -> dict:
5
+ if not CALENDAR_ICS.exists():
6
+ return {"status": "empty", "events": []}
7
+ try:
8
+ from icalendar import Calendar
9
+ cal = Calendar.from_ical(CALENDAR_ICS.read_bytes())
10
+ events = [{"summary": str(c.get("SUMMARY", "")),
11
+ "start": str(c.get("DTSTART", "").dt) if c.get("DTSTART") else ""}
12
+ for c in cal.walk("VEVENT")]
13
+ return {"status": "ok", "events": events[:20]}
14
+ except Exception as e:
15
+ return {"status": "error", "error": str(e)}
backend/tools/dispatcher.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tool dispatcher. Maps tool_name → implementation. Offline-safe."""
2
+ from ..schema import ToolCall
3
+ from . import (duckduckgo_tool, email_tool, reminder_tool, calendar_tool,
4
+ file_tool, weather_tool, calculator_tool, fetch_url_tool,
5
+ transit_lookup_tool)
6
+
7
+ REGISTRY = {
8
+ "duckduckgo_search": duckduckgo_tool.run,
9
+ "web_search": duckduckgo_tool.run,
10
+ "send_email": email_tool.run,
11
+ "set_reminder": reminder_tool.run,
12
+ "calendar_lookup": calendar_tool.run,
13
+ "read_file": file_tool.read,
14
+ "write_file": file_tool.write,
15
+ "weather_lookup": weather_tool.run,
16
+ "run_calculator": calculator_tool.run,
17
+ "fetch_url": fetch_url_tool.run,
18
+ "transit_lookup": transit_lookup_tool.run,
19
+ "read_discord": lambda p: {"status": "offline", "note": "no_token"},
20
+ "read_telegram": lambda p: {"status": "offline", "note": "no_token"},
21
+ "create_notion_page": lambda p: file_tool.write(
22
+ {"path": f"notion_{p.get('title','page')}.md", "content": p.get("content","")}),
23
+ }
24
+
25
+
26
+ def execute_all(tool_calls):
27
+ results = []
28
+ for tc in tool_calls:
29
+ fn = REGISTRY.get(tc.tool_name)
30
+ try:
31
+ out = fn(tc.parameters) if fn else {"error": f"Unknown tool: {tc.tool_name}"}
32
+ except Exception as e:
33
+ out = {"error": str(e), "fallback": tc.offline_fallback}
34
+ results.append({"call_id": tc.call_id, "tool_name": tc.tool_name,
35
+ "calling_agent": tc.calling_agent, "result": out})
36
+ return results
backend/tools/duckduckgo_tool.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ def run(params: dict) -> dict:
2
+ q = params.get("query", "")
3
+ try:
4
+ from duckduckgo_search import DDGS
5
+ with DDGS() as d:
6
+ res = list(d.text(q, max_results=int(params.get("max_results", 5))))
7
+ return {"status": "ok", "query": q, "results": res}
8
+ except Exception as e:
9
+ return {"status": "offline", "error": str(e), "query": q, "results": []}
backend/tools/email_tool.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import smtplib, os, json, time
2
+ from email.message import EmailMessage
3
+ from ..config import OUTBOX
4
+
5
+
6
+ def run(params: dict) -> dict:
7
+ host = os.environ.get("SMTP_HOST"); user = os.environ.get("SMTP_USER")
8
+ pw = os.environ.get("SMTP_PASS"); port = int(os.environ.get("SMTP_PORT", "587"))
9
+ if not all([host, user, pw]):
10
+ # queue to outbox
11
+ f = OUTBOX / f"email_{int(time.time()*1000)}.json"
12
+ f.write_text(json.dumps(params, indent=2))
13
+ return {"status": "queued", "outbox": str(f)}
14
+ msg = EmailMessage()
15
+ msg["From"] = user; msg["To"] = params["to"]; msg["Subject"] = params.get("subject", "")
16
+ msg.set_content(params.get("body", ""))
17
+ with smtplib.SMTP(host, port) as s:
18
+ s.starttls(); s.login(user, pw); s.send_message(msg)
19
+ return {"status": "sent"}
backend/tools/fetch_url_tool.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+
3
+
4
+ def run(params: dict) -> dict:
5
+ try:
6
+ r = httpx.get(params["url"], timeout=15, follow_redirects=True)
7
+ return {"status": r.status_code, "text": r.text[:8000], "url": params["url"]}
8
+ except Exception as e:
9
+ return {"error": str(e), "url": params.get("url")}
backend/tools/file_tool.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ..config import DATA_PATH
2
+
3
+
4
+ def read(params: dict) -> dict:
5
+ p = DATA_PATH / params["path"]
6
+ if not p.exists():
7
+ return {"exists": False, "content": ""}
8
+ return {"exists": True, "content": p.read_text()}
9
+
10
+
11
+ def write(params: dict) -> dict:
12
+ p = DATA_PATH / params["path"]
13
+ p.parent.mkdir(parents=True, exist_ok=True)
14
+ p.write_text(params.get("content", ""))
15
+ return {"status": "written", "path": str(p)}
backend/tools/reminder_tool.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ from ..config import REMINDERS_DB
3
+
4
+
5
+ def run(params: dict) -> dict:
6
+ con = sqlite3.connect(REMINDERS_DB)
7
+ con.execute("CREATE TABLE IF NOT EXISTS r(id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT, when_ TEXT)")
8
+ con.execute("INSERT INTO r(text, when_) VALUES(?,?)",
9
+ (params.get("text", ""), params.get("when", "")))
10
+ con.commit(); con.close()
11
+ return {"status": "scheduled", "when": params.get("when", ""), "text": params.get("text", "")}
backend/tools/transit_lookup_tool.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """transit_lookup — referenced in your training dataset.
2
+ Reuses DuckDuckGo for live web data as a generic lookup endpoint."""
3
+ from . import duckduckgo_tool
4
+
5
+
6
+ def run(params: dict) -> dict:
7
+ return duckduckgo_tool.run(params)
backend/tools/weather_tool.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+
3
+
4
+ def run(params: dict) -> dict:
5
+ loc = params.get("location", "")
6
+ try:
7
+ r = httpx.get(f"https://wttr.in/{loc}?format=j1", timeout=10)
8
+ return {"status": "ok", "location": loc, "data": r.json()}
9
+ except Exception as e:
10
+ return {"status": "offline", "error": str(e), "location": loc}
backend/tts/__init__.py ADDED
File without changes
backend/tts/debate_sequencer.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sequences multiple agent utterances into one debate audio drama."""
2
+ import uuid
3
+ from pathlib import Path
4
+ from pydub import AudioSegment
5
+ from ..config import AUDIO_CACHE
6
+ from .voxcpm_engine import synthesize
7
+
8
+
9
+ def build_debate(agent_outputs: list) -> str | None:
10
+ """Returns a server-relative path under /audio/... for the frontend."""
11
+ if not agent_outputs:
12
+ return None
13
+ track = AudioSegment.silent(duration=300)
14
+ for ao in agent_outputs:
15
+ text = ao.get("tts_speech_text", "")
16
+ voice = ao.get("tts_voice_design", {}) or {}
17
+ wav_path = synthesize(text, voice)
18
+ try:
19
+ seg = AudioSegment.from_wav(wav_path)
20
+ track += seg + AudioSegment.silent(duration=400)
21
+ except Exception:
22
+ continue
23
+ out_path = AUDIO_CACHE / f"debate_{uuid.uuid4().hex}.wav"
24
+ track.export(out_path, format="wav")
25
+ # Return /audio/<filename> for the static mount
26
+ return f"/audio/{out_path.name}"
backend/tts/voxcpm_engine.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """VoxCPM2 TTS — uses the exact API from openbmb/VoxCPM2 model card.
2
+ Voice design is achieved by prefixing text with '(voice description) actual text'.
3
+ """
4
+ import os, uuid, numpy as np, soundfile as sf
5
+ from pathlib import Path
6
+ from ..config import AUDIO_CACHE
7
+
8
+ _ENG = None
9
+
10
+
11
+ def _voice_clause(vd: dict) -> str:
12
+ pace = vd.get("pace", "moderate")
13
+ tone = vd.get("tone", "calm_grounded").replace("_", " ")
14
+ vid = vd.get("voice_id", "")
15
+ # heuristic: feminine/masculine hint based on voice_id name
16
+ gender = "female" if "guardian" in vid or "weaver" in vid or "presenter" in vid else \
17
+ "male" if "builder" in vid or "fetcher" in vid else "neutral"
18
+ return f"({gender} voice, {tone} tone, {pace} pace)"
19
+
20
+
21
+ def _lazy():
22
+ global _ENG
23
+ if _ENG is not None:
24
+ return _ENG
25
+ try:
26
+ from voxcpm import VoxCPM
27
+ _ENG = VoxCPM.from_pretrained("openbmb/VoxCPM2", load_denoiser=False)
28
+ print("[voxcpm] loaded")
29
+ except Exception as e:
30
+ print(f"[voxcpm] load failed ({e}) — using silent fallback")
31
+ _ENG = "SILENT"
32
+ return _ENG
33
+
34
+
35
+ def synthesize(text: str, voice_design: dict) -> str:
36
+ eng = _lazy()
37
+ out = AUDIO_CACHE / f"{uuid.uuid4().hex}.wav"
38
+ if not text.strip():
39
+ sf.write(out, np.zeros(int(48000 * 0.3), dtype=np.float32), 48000)
40
+ return str(out)
41
+ if eng == "SILENT" or eng is None:
42
+ sf.write(out, np.zeros(int(48000 * 0.6), dtype=np.float32), 48000)
43
+ return str(out)
44
+ prefix = _voice_clause(voice_design or {})
45
+ full_text = f"{prefix}{text}"
46
+ try:
47
+ wav = eng.generate(text=full_text, cfg_value=2.0, inference_timesteps=10)
48
+ sr = eng.tts_model.sample_rate
49
+ sf.write(out, wav, sr)
50
+ except Exception as e:
51
+ print(f"[voxcpm] generation failed: {e}")
52
+ sf.write(out, np.zeros(int(48000 * 0.6), dtype=np.float32), 48000)
53
+ return str(out)
elysium/.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
elysium/README.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Elysium
3
+ emoji: 🐢
4
+ colorFrom: yellow
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 6.18.0
8
+ python_version: '3.12'
9
+ app_file: app.py
10
+ pinned: false
11
+ license: mit
12
+ ---
13
+
14
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
frontend/dist/assets/api.js ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Tiny client for /api/* endpoints. */
2
+ window.ElysiumAPI = {
3
+ async turn(text, file) {
4
+ const fd = new FormData();
5
+ fd.append('user_text', text);
6
+ if (file) fd.append('image', file);
7
+ const r = await fetch('/api/turn', { method: 'POST', body: fd });
8
+ return r.json();
9
+ },
10
+ async health() { return (await fetch('/api/health')).json(); },
11
+ async hypergraph() { return (await fetch('/api/hypergraph')).json(); },
12
+ async reset() { return (await fetch('/api/reset', { method: 'POST' })).json(); },
13
+ };
frontend/dist/assets/boot.js ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Main app boot: wire input → /api/turn → update canvas + council. */
2
+ (() => {
3
+ const input = document.getElementById('q-input');
4
+ const send = document.getElementById('q-send');
5
+ const file = document.getElementById('q-file');
6
+ const seedHint = document.getElementById('seed-hint');
7
+
8
+ function toast(msg, kind = '') {
9
+ const t = document.createElement('div');
10
+ t.className = 'toast ' + kind; t.textContent = msg;
11
+ document.getElementById('toasts').appendChild(t);
12
+ setTimeout(() => t.remove(), 4500);
13
+ }
14
+
15
+ // Restore hypergraph on load
16
+ async function restore() {
17
+ try {
18
+ const h = await ElysiumAPI.hypergraph();
19
+ (h.nodes || []).forEach(n => window.elysiumAddNode(n));
20
+ (h.edges || []).forEach(e => window.elysiumAddEdge(e));
21
+ if ((h.nodes || []).length > 1) seedHint.classList.add('hidden');
22
+ updateLegend();
23
+ document.getElementById('s-engage').textContent = h.node_count || 0;
24
+ document.getElementById('s-laws').textContent = h.edge_count || 0;
25
+ } catch(e) { /* offline first-boot */ }
26
+ }
27
+ restore();
28
+
29
+ // Wire send
30
+ let busy = false;
31
+ async function submit() {
32
+ if (busy) return;
33
+ const text = input.value.trim();
34
+ if (!text && !file.files.length) return;
35
+ busy = true; send.classList.add('loading'); send.textContent = '◐';
36
+ seedHint.classList.add('hidden');
37
+ input.value = ''; input.placeholder = 'Council deliberating…';
38
+ try {
39
+ const data = await ElysiumAPI.turn(text, file.files[0]);
40
+ file.value = '';
41
+ handleResponse(data);
42
+ } catch(e) {
43
+ toast('Inference failed: ' + e.message, 'error');
44
+ } finally {
45
+ busy = false; send.classList.remove('loading'); send.textContent = '→';
46
+ input.placeholder = 'Speak to your council…';
47
+ }
48
+ }
49
+ send.onclick = submit;
50
+ input.addEventListener('keydown', e => { if (e.key === 'Enter') submit(); });
51
+
52
+ function handleResponse(payload) {
53
+ const resp = payload.elysium_response || {};
54
+ const rt = payload._runtime || {};
55
+ // 1. apply hypergraph delta
56
+ const delta = resp.hypergraph_delta || {};
57
+ (delta.nodes_added || []).forEach(n => window.elysiumAddNode(n));
58
+ (delta.edges_added || []).forEach(e => window.elysiumAddEdge(e));
59
+
60
+ // 2. UI directives
61
+ const ui = resp.ui_directives || {};
62
+ (ui.bioluminescence_pulse_nodes || []).forEach(id => window.elysiumPulse(id, 1400));
63
+ document.body.dataset.alert = ui.alert_level || 'CALM';
64
+ if (ui.camera_focus_node_id) window.elysiumFocus(ui.camera_focus_node_id);
65
+
66
+ // 3. council
67
+ window.renderCouncil(resp, rt);
68
+
69
+ // 4. stats
70
+ const meta = resp.metadata || {};
71
+ document.getElementById('s-engage').textContent = meta.hypergraph_node_count ?? '0';
72
+ document.getElementById('s-laws').textContent = meta.hypergraph_edge_count ?? '0';
73
+ document.getElementById('s-impl').textContent =
74
+ Math.round((1 - (resp.strain_metadata?.cognitive_strain ?? 0.3)) * 100) + '%';
75
+ document.getElementById('s-health').textContent = `${(rt.hypergraph?.nodes ?? 0)}`;
76
+
77
+ // 5. agent count
78
+ const ag = (resp.council_deliberation?.agent_outputs || []).length;
79
+ document.getElementById('agent-count').textContent = `+${ag}`;
80
+
81
+ // 6. legend
82
+ updateLegend();
83
+
84
+ // 7. tool toasts
85
+ (rt.tool_results || []).forEach(tr => {
86
+ const ok = tr.result && !tr.result.error;
87
+ toast(`🔧 ${tr.tool_name}: ${ok ? 'ok' : (tr.result?.error || 'offline')}`,
88
+ ok ? '' : 'warn');
89
+ });
90
+
91
+ // 8. direct answer toast if no agents
92
+ if (!ag && resp.direct_answer) toast(resp.direct_answer);
93
+ }
94
+
95
+ function updateLegend() {
96
+ const counts = {};
97
+ window.ELYSIUM.nodes.forEach(n => counts[n.type] = (counts[n.type] || 0) + 1);
98
+ const order = ['CORE','DOMAIN','AGENT','TOOL','PROJECT','LIFE_EVENT','EMOTION','PERSON','VALUE','MEMORY'];
99
+ const items = order.filter(t => counts[t]).map(t => {
100
+ const c = window.colorFor(t);
101
+ return `<div class="legend-item" data-type="${t}">
102
+ <span class="dot" style="background:${c};color:${c}"></span>
103
+ <span class="badge">${counts[t]}</span>
104
+ <span class="lbl">${t.replace('_',' ').toLowerCase()}</span>
105
+ </div>`;
106
+ }).join('');
107
+ document.getElementById('legend-items').innerHTML = items;
108
+ document.getElementById('legend-gaps').textContent =
109
+ Math.min(99, Math.round((1 - (counts.AGENT || 0) / Math.max(1, counts.DOMAIN || 1)) * 50)) + '%';
110
+ document.getElementById('legend-updates').textContent =
111
+ Math.min(99, window.ELYSIUM.nodes.size * 3) + '%';
112
+ }
113
+
114
+ // RI Analysis = fit all + show health
115
+ document.getElementById('ri-analysis').onclick = () => {
116
+ window.elysiumFitAll();
117
+ toast('🔍 RI Analysis: civilization-wide view');
118
+ };
119
+ document.getElementById('my-agent').onclick = () => {
120
+ toast(`🤖 Active agents: ${document.getElementById('agent-count').textContent}`);
121
+ };
122
+ })();
frontend/dist/assets/canvas.js ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Google-Maps-style infinite-pan canvas with bioluminescent hypergraph. */
2
+ (() => {
3
+ const canvas = document.getElementById('elysium-canvas');
4
+ const ctx = canvas.getContext('2d', { alpha: false });
5
+
6
+ const DPR = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
7
+ function resize() {
8
+ canvas.width = innerWidth * DPR;
9
+ canvas.height = innerHeight * DPR;
10
+ canvas.style.width = innerWidth + 'px';
11
+ canvas.style.height = innerHeight + 'px';
12
+ }
13
+ resize(); addEventListener('resize', resize);
14
+
15
+ // World state
16
+ const E = window.ELYSIUM = {
17
+ cam: { x: 0, y: 0, z: 1, tx: 0, ty: 0, tz: 1, vx: 0, vy: 0, drag: false },
18
+ nodes: new Map(), edges: [], spawning: [],
19
+ hover: null, selected: null,
20
+ };
21
+
22
+ // Seed CORE
23
+ E.nodes.set('CORE', {
24
+ node_id: 'CORE', x: 0, y: 0, type: 'CORE', label: 'ELYSIUM',
25
+ radius: 34, color: '#c8a44a', phase: 0, born: performance.now() - 1000,
26
+ });
27
+
28
+ // ──────── PAN / ZOOM ────────
29
+ let lastX = 0, lastY = 0;
30
+ function clientToWorld(cx, cy) {
31
+ return {
32
+ x: (cx - innerWidth / 2) / E.cam.z + E.cam.x,
33
+ y: (cy - innerHeight / 2) / E.cam.z + E.cam.y,
34
+ };
35
+ }
36
+
37
+ canvas.addEventListener('pointerdown', e => {
38
+ // hit-test a node first
39
+ const w = clientToWorld(e.clientX, e.clientY);
40
+ let hit = null;
41
+ E.nodes.forEach(n => {
42
+ if (Math.hypot(n.x - w.x, n.y - w.y) < n.radius * 1.15) hit = n;
43
+ });
44
+ if (hit) { showNodeDetail(hit, e.clientX, e.clientY); return; }
45
+ E.cam.drag = true; canvas.classList.add('dragging');
46
+ lastX = e.clientX; lastY = e.clientY;
47
+ canvas.setPointerCapture(e.pointerId);
48
+ });
49
+ canvas.addEventListener('pointermove', e => {
50
+ if (!E.cam.drag) {
51
+ const w = clientToWorld(e.clientX, e.clientY);
52
+ let hover = null;
53
+ E.nodes.forEach(n => { if (Math.hypot(n.x - w.x, n.y - w.y) < n.radius * 1.15) hover = n; });
54
+ E.hover = hover; canvas.style.cursor = hover ? 'pointer' : '';
55
+ return;
56
+ }
57
+ const dx = (e.clientX - lastX) / E.cam.z;
58
+ const dy = (e.clientY - lastY) / E.cam.z;
59
+ E.cam.x -= dx; E.cam.y -= dy;
60
+ E.cam.vx = dx * 0.85; E.cam.vy = dy * 0.85;
61
+ E.cam.tx = E.cam.x; E.cam.ty = E.cam.y;
62
+ lastX = e.clientX; lastY = e.clientY;
63
+ });
64
+ function endPan(e){ E.cam.drag = false; canvas.classList.remove('dragging');
65
+ try { canvas.releasePointerCapture(e.pointerId); } catch{} }
66
+ canvas.addEventListener('pointerup', endPan);
67
+ canvas.addEventListener('pointercancel', endPan);
68
+
69
+ // wheel zoom
70
+ canvas.addEventListener('wheel', e => {
71
+ e.preventDefault();
72
+ const factor = e.deltaY > 0 ? 0.9 : 1.1;
73
+ const w = clientToWorld(e.clientX, e.clientY);
74
+ E.cam.tz = Math.max(0.15, Math.min(4, E.cam.z * factor));
75
+ E.cam.tx = w.x - (e.clientX - innerWidth / 2) / E.cam.tz;
76
+ E.cam.ty = w.y - (e.clientY - innerHeight / 2) / E.cam.tz;
77
+ }, { passive: false });
78
+
79
+ // double-click zoom-in
80
+ canvas.addEventListener('dblclick', e => {
81
+ const w = clientToWorld(e.clientX, e.clientY);
82
+ E.cam.tz = Math.min(4, E.cam.z * 1.6);
83
+ E.cam.tx = w.x; E.cam.ty = w.y;
84
+ });
85
+
86
+ // ──────── TOUCH (pinch) ────────
87
+ let touchDist = 0, touchMid = null;
88
+ canvas.addEventListener('touchstart', e => {
89
+ if (e.touches.length === 2) {
90
+ e.preventDefault();
91
+ const [a, b] = e.touches;
92
+ touchDist = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
93
+ touchMid = { x: (a.clientX + b.clientX) / 2, y: (a.clientY + b.clientY) / 2 };
94
+ }
95
+ }, { passive: false });
96
+ canvas.addEventListener('touchmove', e => {
97
+ if (e.touches.length === 2 && touchDist) {
98
+ e.preventDefault();
99
+ const [a, b] = e.touches;
100
+ const d = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
101
+ const factor = d / touchDist; touchDist = d;
102
+ const w = clientToWorld(touchMid.x, touchMid.y);
103
+ E.cam.tz = Math.max(0.15, Math.min(4, E.cam.z * factor));
104
+ E.cam.tx = w.x - (touchMid.x - innerWidth / 2) / E.cam.tz;
105
+ E.cam.ty = w.y - (touchMid.y - innerHeight / 2) / E.cam.tz;
106
+ }
107
+ }, { passive: false });
108
+ canvas.addEventListener('touchend', () => { touchDist = 0; });
109
+
110
+ // ──────── ZOOM BTNS ────────
111
+ document.getElementById('z-in').onclick = () => E.cam.tz = Math.min(4, E.cam.z * 1.35);
112
+ document.getElementById('z-out').onclick = () => E.cam.tz = Math.max(0.15, E.cam.z / 1.35);
113
+ document.getElementById('z-fit').onclick = fitAll;
114
+
115
+ function fitAll() {
116
+ if (E.nodes.size === 0) return;
117
+ let mnX = Infinity, mnY = Infinity, mxX = -Infinity, mxY = -Infinity;
118
+ E.nodes.forEach(n => { mnX = Math.min(mnX, n.x); mnY = Math.min(mnY, n.y);
119
+ mxX = Math.max(mxX, n.x); mxY = Math.max(mxY, n.y); });
120
+ const pad = 180;
121
+ E.cam.tx = (mnX + mxX) / 2;
122
+ E.cam.ty = (mnY + mxY) / 2;
123
+ E.cam.tz = Math.min(4, Math.max(0.25,
124
+ Math.min(innerWidth / (mxX - mnX + pad), innerHeight / (mxY - mnY + pad))));
125
+ }
126
+ window.elysiumFitAll = fitAll;
127
+
128
+ // ──────── MINIMAP ────────
129
+ const mini = document.getElementById('minimap');
130
+ const mctx = mini.getContext('2d');
131
+ function drawMinimap() {
132
+ mini.width = 160; mini.height = 100;
133
+ mctx.fillStyle = 'rgba(8,24,30,.95)';
134
+ mctx.fillRect(0, 0, 160, 100);
135
+ if (E.nodes.size === 0) return;
136
+ let mnX = Infinity, mnY = Infinity, mxX = -Infinity, mxY = -Infinity;
137
+ E.nodes.forEach(n => { mnX = Math.min(mnX, n.x); mnY = Math.min(mnY, n.y);
138
+ mxX = Math.max(mxX, n.x); mxY = Math.max(mxY, n.y); });
139
+ const pad = 80; mnX -= pad; mnY -= pad; mxX += pad; mxY += pad;
140
+ const sx = 160 / (mxX - mnX), sy = 100 / (mxY - mnY);
141
+ // edges
142
+ mctx.strokeStyle = 'rgba(0,200,160,.18)'; mctx.lineWidth = .5;
143
+ E.edges.forEach(ed => {
144
+ const s = E.nodes.get(ed.src), t = E.nodes.get(ed.dst);
145
+ if (!s || !t) return;
146
+ mctx.beginPath();
147
+ mctx.moveTo((s.x - mnX) * sx, (s.y - mnY) * sy);
148
+ mctx.lineTo((t.x - mnX) * sx, (t.y - mnY) * sy);
149
+ mctx.stroke();
150
+ });
151
+ // nodes
152
+ E.nodes.forEach(n => {
153
+ mctx.fillStyle = n.color;
154
+ mctx.beginPath(); mctx.arc((n.x - mnX) * sx, (n.y - mnY) * sy, 2.2, 0, Math.PI * 2);
155
+ mctx.fill();
156
+ });
157
+ // viewport rect
158
+ const vx = (E.cam.x - innerWidth / 2 / E.cam.z - mnX) * sx;
159
+ const vy = (E.cam.y - innerHeight / 2 / E.cam.z - mnY) * sy;
160
+ const vw = innerWidth / E.cam.z * sx, vh = innerHeight / E.cam.z * sy;
161
+ mctx.strokeStyle = 'rgba(0,229,255,.7)'; mctx.lineWidth = 1;
162
+ mctx.setLineDash([3, 3]);
163
+ mctx.strokeRect(vx, vy, vw, vh);
164
+ mctx.setLineDash([]);
165
+ }
166
+ // minimap click → pan
167
+ mini.addEventListener('click', e => {
168
+ if (E.nodes.size === 0) return;
169
+ const rect = mini.getBoundingClientRect();
170
+ const fx = (e.clientX - rect.left) / rect.width;
171
+ const fy = (e.clientY - rect.top) / rect.height;
172
+ let mnX = Infinity, mnY = Infinity, mxX = -Infinity, mxY = -Infinity;
173
+ E.nodes.forEach(n => { mnX = Math.min(mnX, n.x); mnY = Math.min(mnY, n.y);
174
+ mxX = Math.max(mxX, n.x); mxY = Math.max(mxY, n.y); });
175
+ const pad = 80; mnX -= pad; mnY -= pad; mxX += pad; mxY += pad;
176
+ E.cam.tx = mnX + fx * (mxX - mnX);
177
+ E.cam.ty = mnY + fy * (mxY - mnY);
178
+ });
179
+
180
+ // ──────── NODE DETAIL POPOVER ────────
181
+ function showNodeDetail(n, sx, sy) {
182
+ const el = document.getElementById('node-detail');
183
+ el.style.left = Math.min(innerWidth - 300, sx + 16) + 'px';
184
+ el.style.top = Math.min(innerHeight - 200, sy) + 'px';
185
+ el.innerHTML = `
186
+ <div class="nd-title">
187
+ <span style="width:14px;height:14px;border-radius:50%;background:${n.color};
188
+ box-shadow:0 0 8px ${n.color}"></span>${n.label || n.node_id}</div>
189
+ <div class="nd-type">${n.type}</div>
190
+ <div class="nd-stats">
191
+ <div class="nd-pill"><b>${(Math.random()*40+10|0)}</b>activity</div>
192
+ <div class="nd-pill"><b>${(Math.random()*70+10|0)}%</b>weight</div>
193
+ <div class="nd-pill"><b>${(Math.random()*30+5|0)}</b>links</div>
194
+ </div>
195
+ <div style="font-size:11px;color:var(--muted);line-height:1.5">
196
+ Node connected to your civilization mycelium. Pan and zoom to explore.
197
+ </div>`;
198
+ el.classList.add('show');
199
+ clearTimeout(showNodeDetail._t);
200
+ showNodeDetail._t = setTimeout(() => el.classList.remove('show'), 4500);
201
+ }
202
+
203
+ // ──────── PUBLIC API ────────
204
+ const SPAWN_RADIUS = 280;
205
+ window.elysiumAddNode = function (node, parentId = 'CORE') {
206
+ if (E.nodes.has(node.node_id)) return;
207
+ const parent = E.nodes.get(parentId) || E.nodes.get('CORE') || { x: 0, y: 0 };
208
+ const idx = E.nodes.size;
209
+ const angle = idx * 0.7 + Math.random() * 0.6 - 0.3;
210
+ const dist = SPAWN_RADIUS + (idx * 14) % 180;
211
+ const tx = parent.x + Math.cos(angle) * dist;
212
+ const ty = parent.y + Math.sin(angle) * dist;
213
+ const type = node.node_type || 'DOMAIN';
214
+ const radius = type === 'CORE' ? 34 :
215
+ type === 'AGENT' ? 22 :
216
+ type === 'TOOL' ? 14 : 18;
217
+ E.nodes.set(node.node_id, {
218
+ node_id: node.node_id,
219
+ x: parent.x, y: parent.y, tx, ty,
220
+ type, label: node.label || node.node_id,
221
+ radius, color: window.colorFor(type),
222
+ phase: Math.random() * Math.PI * 2,
223
+ born: performance.now(),
224
+ spawnFrom: parentId,
225
+ });
226
+ // auto-pan a little toward new node
227
+ E.cam.tx = E.cam.x * 0.85 + tx * 0.15;
228
+ E.cam.ty = E.cam.y * 0.85 + ty * 0.15;
229
+ };
230
+
231
+ window.elysiumAddEdge = function (edge) {
232
+ E.edges.push({
233
+ src: edge.source_node_id, dst: edge.target_node_id,
234
+ type: edge.edge_type, weight: edge.weight ?? 0.5,
235
+ color: edge.edge_type === 'CONFLICT' ? 'rgba(220,80,80,.55)' :
236
+ edge.edge_type === 'COALITION' ? 'rgba(120,190,255,.6)' :
237
+ edge.edge_type === 'CAUSAL' ? 'rgba(0,229,255,.45)' :
238
+ 'rgba(200,164,74,.5)',
239
+ born: performance.now(), dashOffset: 0,
240
+ });
241
+ };
242
+
243
+ window.elysiumPulse = function (nodeId, ms = 1200) {
244
+ const n = E.nodes.get(nodeId); if (n) n.pulseUntil = performance.now() + ms;
245
+ };
246
+
247
+ window.elysiumFocus = function (nodeId) {
248
+ const n = E.nodes.get(nodeId); if (!n) return;
249
+ E.cam.tx = n.x; E.cam.ty = n.y;
250
+ E.cam.tz = Math.max(1, E.cam.z);
251
+ };
252
+
253
+ // ──────── RENDER LOOP ────────
254
+ function lerp(a, b, t) { return a + (b - a) * t; }
255
+ function loop(t) {
256
+ // animate spawning nodes toward target
257
+ E.nodes.forEach(n => {
258
+ if (n.tx != null) {
259
+ n.x = lerp(n.x, n.tx, 0.12);
260
+ n.y = lerp(n.y, n.ty, 0.12);
261
+ if (Math.abs(n.x - n.tx) < 0.3 && Math.abs(n.y - n.ty) < 0.3) {
262
+ n.x = n.tx; n.y = n.ty; n.tx = null; n.ty = null;
263
+ }
264
+ }
265
+ });
266
+ // smooth camera
267
+ E.cam.z = lerp(E.cam.z, E.cam.tz, 0.10);
268
+ if (!E.cam.drag) {
269
+ E.cam.x = lerp(E.cam.x, E.cam.tx, 0.12);
270
+ E.cam.y = lerp(E.cam.y, E.cam.ty, 0.12);
271
+ E.cam.x -= E.cam.vx; E.cam.y -= E.cam.vy;
272
+ E.cam.vx *= 0.92; E.cam.vy *= 0.92;
273
+ if (Math.abs(E.cam.vx) < 0.05) E.cam.vx = 0;
274
+ if (Math.abs(E.cam.vy) < 0.05) E.cam.vy = 0;
275
+ }
276
+
277
+ // background haze
278
+ ctx.fillStyle = '#0a1a20';
279
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
280
+ const grad = ctx.createRadialGradient(canvas.width/2, canvas.height/2, 0,
281
+ canvas.width/2, canvas.height/2, canvas.width*0.55);
282
+ grad.addColorStop(0, 'rgba(10,48,64,.22)');
283
+ grad.addColorStop(1, 'rgba(6,14,18,0)');
284
+ ctx.fillStyle = grad; ctx.fillRect(0, 0, canvas.width, canvas.height);
285
+
286
+ // particles (subtle ambient)
287
+ drawAmbient(ctx, t);
288
+
289
+ // world transform
290
+ ctx.save();
291
+ ctx.scale(DPR, DPR);
292
+ ctx.translate(innerWidth / 2, innerHeight / 2);
293
+ ctx.scale(E.cam.z, E.cam.z);
294
+ ctx.translate(-E.cam.x, -E.cam.y);
295
+
296
+ // edges
297
+ E.edges.forEach(e => {
298
+ const s = E.nodes.get(e.src), d = E.nodes.get(e.dst); if (!s || !d) return;
299
+ ctx.strokeStyle = e.color;
300
+ ctx.lineWidth = (0.8 + (e.weight ?? .5) * 0.8) / E.cam.z;
301
+ if (e.type === 'COALITION' || e.type === 'CAUSAL') {
302
+ ctx.setLineDash([6 / E.cam.z, 4 / E.cam.z]);
303
+ ctx.lineDashOffset = -(t * 0.02);
304
+ } else {
305
+ ctx.setLineDash([]);
306
+ }
307
+ ctx.beginPath(); ctx.moveTo(s.x, s.y); ctx.lineTo(d.x, d.y); ctx.stroke();
308
+ ctx.setLineDash([]);
309
+ });
310
+
311
+ // nodes
312
+ E.nodes.forEach(n => window.drawNode(ctx, n, t, E.cam.z));
313
+ ctx.restore();
314
+
315
+ drawMinimap();
316
+ requestAnimationFrame(loop);
317
+ }
318
+
319
+ // ambient floating particles
320
+ const PARTICLES = Array.from({length: 50}, () => ({
321
+ x: Math.random() * innerWidth, y: Math.random() * innerHeight,
322
+ r: Math.random() * 1.4 + 0.4, vx: (Math.random() - .5) * 0.15,
323
+ vy: (Math.random() - .5) * 0.15,
324
+ c: Math.random() > .6 ? 'rgba(0,200,160,.35)' : 'rgba(200,164,74,.22)',
325
+ }));
326
+ function drawAmbient(ctx, t) {
327
+ ctx.save(); ctx.scale(DPR, DPR);
328
+ PARTICLES.forEach(p => {
329
+ p.x += p.vx; p.y += p.vy;
330
+ if (p.x < 0) p.x = innerWidth; if (p.x > innerWidth) p.x = 0;
331
+ if (p.y < 0) p.y = innerHeight; if (p.y > innerHeight) p.y = 0;
332
+ ctx.fillStyle = p.c;
333
+ ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill();
334
+ });
335
+ ctx.restore();
336
+ }
337
+
338
+ requestAnimationFrame(loop);
339
+ })();
frontend/dist/assets/council.js ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Council panel + agent debate playback. */
2
+ const AGENT_COLORS = {
3
+ THE_BUILDER: '#e05080',
4
+ THE_GUARDIAN: '#9060c0',
5
+ THE_ORACLE: '#40d0e0',
6
+ THE_WEAVER: '#d070a0',
7
+ THE_WILDCARD: '#40e080',
8
+ DYNAMIC: '#c8903a',
9
+ };
10
+ window.agentColor = (a) => AGENT_COLORS[a] || '#9060c0';
11
+
12
+ window.renderCouncil = function (resp, runtime) {
13
+ const council = document.getElementById('council');
14
+ const body = document.getElementById('council-body');
15
+ const synth = document.getElementById('council-synth');
16
+ const count = document.getElementById('council-count');
17
+ const audio = document.getElementById('debate-audio');
18
+
19
+ const cd = resp.council_deliberation || {};
20
+ const agents = cd.agent_outputs || [];
21
+ if (!agents.length) {
22
+ council.classList.remove('show');
23
+ setTimeout(() => council.classList.add('hidden'), 400);
24
+ return;
25
+ }
26
+ council.classList.remove('hidden');
27
+ requestAnimationFrame(() => council.classList.add('show'));
28
+
29
+ count.textContent = `+${agents.length}`;
30
+ body.innerHTML = agents.map((a, i) => {
31
+ const c = window.agentColor(a.archetype);
32
+ return `
33
+ <div class="agent-card" style="border-color:${c}; animation-delay:${i * 80}ms"
34
+ data-aid="${a.agent_id}">
35
+ <div class="row1">
36
+ <span class="ad-dot" style="background:${c};color:${c}"></span>
37
+ <span class="name" style="color:${c}">${a.agent_name}</span>
38
+ <span class="archetype">${a.archetype}</span>
39
+ ${a.veto_triggered ? '<span class="archetype" style="background:#7a2230;color:#ffb0b0">VETO</span>' : ''}
40
+ </div>
41
+ <div class="thinking">${escapeHtml(a.thinking || '')}</div>
42
+ <div class="stance">${escapeHtml(a.stance || '')}</div>
43
+ <div class="conf">confidence ${(a.confidence ?? 0.8).toFixed(2)}</div>
44
+ </div>`;
45
+ }).join('');
46
+
47
+ synth.textContent = cd.final_synthesis || resp.direct_answer || '';
48
+
49
+ // audio playback
50
+ if (runtime && runtime.audio_url) {
51
+ audio.style.display = 'block';
52
+ audio.src = runtime.audio_url;
53
+ audio.play().catch(() => {});
54
+ // highlight speaking agent as audio plays
55
+ let idx = 0;
56
+ const cards = body.querySelectorAll('.agent-card');
57
+ cards[0]?.classList.add('speaking');
58
+ audio.ontimeupdate = () => {
59
+ if (!audio.duration) return;
60
+ const target = Math.min(cards.length - 1,
61
+ Math.floor((audio.currentTime / audio.duration) * cards.length));
62
+ if (target !== idx) {
63
+ cards[idx]?.classList.remove('speaking');
64
+ cards[target]?.classList.add('speaking');
65
+ idx = target;
66
+ }
67
+ };
68
+ audio.onended = () => cards.forEach(c => c.classList.remove('speaking'));
69
+ } else {
70
+ audio.style.display = 'none';
71
+ }
72
+ };
73
+
74
+ function escapeHtml(s) {
75
+ return String(s || '').replace(/[&<>"']/g, c => ({
76
+ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
77
+ }[c]));
78
+ }
79
+
80
+ document.getElementById('council-close').onclick = () => {
81
+ const c = document.getElementById('council');
82
+ c.classList.remove('show');
83
+ setTimeout(() => c.classList.add('hidden'), 400);
84
+ };
frontend/dist/assets/elysium.css ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root{
2
+ --bg:#0a1a20; --bg-2:#060e12;
3
+ --panel:rgba(8,24,30,.85); --panel-2:rgba(15,35,42,.7);
4
+ --border:rgba(0,200,160,.2); --border-strong:rgba(0,200,160,.45);
5
+ --teal:#00c8a0; --cyan:#00e5ff; --gold:#c8a44a;
6
+ --pink:#e05080; --lav:#9060c0; --green:#40e080; --warm:#c8903a;
7
+ --text:#e0eef0; --muted:#7aabb8;
8
+ --shadow:0 8px 32px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.05);
9
+ --font-sans:'Inter',system-ui,sans-serif;
10
+ --font-mono:'Space Mono',monospace;
11
+ --font-disp:'Space Grotesk',sans-serif;
12
+ }
13
+ *,*::before,*::after{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
14
+ html,body{margin:0;padding:0;height:100%;width:100%;overflow:hidden;
15
+ background:radial-gradient(ellipse at center,#0d2228 0%,#060e12 100%);
16
+ color:var(--text);font-family:var(--font-sans);font-size:14px;
17
+ user-select:none;-webkit-user-select:none}
18
+ button{font-family:inherit;color:inherit}
19
+
20
+ /* CANVAS */
21
+ #elysium-canvas{position:fixed;inset:0;width:100vw;height:100vh;cursor:grab;
22
+ touch-action:none;z-index:0}
23
+ #elysium-canvas.dragging{cursor:grabbing}
24
+
25
+ /* GLASS PANELS */
26
+ .glass{background:var(--panel);border:1px solid var(--border);border-radius:16px;
27
+ backdrop-filter:blur(16px) saturate(1.2);-webkit-backdrop-filter:blur(16px) saturate(1.2);
28
+ box-shadow:var(--shadow)}
29
+
30
+ /* TIMELINE */
31
+ #timeline{position:fixed;left:0;top:0;bottom:0;width:54px;z-index:10;
32
+ display:flex;flex-direction:column;align-items:center;padding:40px 0;
33
+ background:linear-gradient(90deg,rgba(10,26,32,.55),transparent);
34
+ pointer-events:none}
35
+ #timeline .t-edge{position:absolute;left:0;top:0;bottom:0;width:2px;
36
+ background:repeating-linear-gradient(180deg,transparent 0 8px,rgba(0,200,160,.2) 8px 9px)}
37
+ #timeline .years{display:flex;flex-direction:column;gap:46px;font-size:10px;
38
+ color:var(--muted);letter-spacing:.5px;font-family:var(--font-mono);
39
+ pointer-events:auto}
40
+ #timeline .year{opacity:.55}
41
+ #timeline .year-active{padding:4px 11px;border:1.5px solid var(--teal);
42
+ border-radius:18px;color:var(--text);font-weight:700;font-size:11px;
43
+ box-shadow:0 0 14px rgba(0,200,160,.55);background:rgba(0,40,48,.45);
44
+ animation:pillpulse 3s ease-in-out infinite}
45
+ @keyframes pillpulse{0%,100%{box-shadow:0 0 14px rgba(0,200,160,.5)}
46
+ 50%{box-shadow:0 0 22px rgba(0,200,160,.85)}}
47
+
48
+ /* LEGEND */
49
+ #legend{position:fixed;top:24px;right:24px;width:220px;padding:18px;z-index:20}
50
+ .legend-header{font-size:11px;letter-spacing:2.5px;color:var(--muted);margin-bottom:14px;
51
+ font-weight:600}
52
+ .legend-item{display:flex;align-items:center;gap:10px;margin:9px 0;font-size:13px}
53
+ .legend-item .dot{width:18px;height:18px;border-radius:50%;
54
+ box-shadow:0 0 10px currentColor;flex-shrink:0}
55
+ .legend-item .badge{background:rgba(0,0,0,.4);padding:2px 8px;border-radius:10px;
56
+ font-family:var(--font-mono);font-size:10px;min-width:24px;text-align:center}
57
+ .legend-item .lbl{color:var(--text);font-size:12px}
58
+ .meta-row{display:flex;justify-content:space-between;align-items:center;
59
+ font-size:11px;color:var(--muted);margin-top:10px;padding-top:10px;
60
+ border-top:1px solid var(--border)}
61
+ .meta-val{background:rgba(0,0,0,.3);padding:3px 9px;border-radius:10px;
62
+ font-family:var(--font-mono);color:var(--text);font-size:11px}
63
+
64
+ /* MINIMAP */
65
+ #minimap{position:fixed;left:74px;bottom:140px;width:160px;height:100px;
66
+ background:rgba(8,24,30,.82);border:1px solid var(--border);
67
+ border-radius:10px;z-index:15;cursor:pointer;
68
+ box-shadow:var(--shadow)}
69
+ .minimap-label{position:fixed;left:80px;bottom:244px;font-size:9px;
70
+ letter-spacing:2px;color:var(--muted);z-index:15;pointer-events:none}
71
+
72
+ /* ZOOM CTRL */
73
+ #zoom-ctrl{position:fixed;right:24px;bottom:140px;display:flex;flex-direction:column;
74
+ gap:6px;z-index:15}
75
+ #zoom-ctrl button{width:38px;height:38px;background:rgba(8,24,30,.85);
76
+ border:1px solid var(--border);border-radius:10px;color:var(--teal);
77
+ font-size:18px;cursor:pointer;backdrop-filter:blur(10px);
78
+ transition:all .15s ease}
79
+ #zoom-ctrl button:hover{background:rgba(0,200,160,.15);transform:scale(1.05)}
80
+
81
+ /* ALERT SCRIM */
82
+ #alert-scrim{position:fixed;inset:0;z-index:5;pointer-events:none;
83
+ background:radial-gradient(ellipse at center,transparent 55%,rgba(220,60,60,0) 100%);
84
+ transition:background 800ms ease}
85
+ body[data-alert="ATTENTION"] #alert-scrim{background:radial-gradient(ellipse at center,transparent 50%,rgba(200,160,80,.10) 100%)}
86
+ body[data-alert="TENSION"] #alert-scrim{background:radial-gradient(ellipse at center,transparent 40%,rgba(220,140,60,.18) 100%)}
87
+ body[data-alert="CRISIS"] #alert-scrim{background:radial-gradient(ellipse at center,transparent 30%,rgba(220,60,60,.25) 100%);
88
+ animation:crisisPulse 1.6s ease-in-out infinite}
89
+ @keyframes crisisPulse{0%,100%{opacity:.6}50%{opacity:1}}
90
+
91
+ /* STATS BAR */
92
+ #stats{position:fixed;left:54px;right:0;bottom:56px;height:54px;
93
+ display:flex;align-items:center;padding:0 28px;gap:36px;z-index:18;
94
+ border-radius:0;border-left:none;border-right:none;border-bottom:none}
95
+ .stat{display:flex;align-items:center;gap:10px}
96
+ .stat-icon{font-size:18px;color:var(--teal);opacity:.85}
97
+ .stat-label{font-size:10px;letter-spacing:1.4px;color:var(--muted);
98
+ text-transform:uppercase;font-weight:500}
99
+ .stat-value{font-size:22px;font-family:var(--font-disp);color:var(--cyan);
100
+ font-weight:700;line-height:1}
101
+
102
+ /* QUERY BAR */
103
+ #query-bar{position:fixed;left:54px;right:0;bottom:0;height:56px;
104
+ display:flex;align-items:center;padding:0 22px;gap:10px;z-index:20;
105
+ border-radius:0;border-left:none;border-right:none}
106
+ #q-input{flex:1;height:40px;background:var(--panel-2);
107
+ border:1px solid var(--border);border-radius:28px;padding:0 22px;
108
+ color:var(--text);font-size:13px;outline:none;
109
+ transition:border-color .2s,box-shadow .2s}
110
+ #q-input:focus{border-color:var(--border-strong);
111
+ box-shadow:0 0 0 3px rgba(0,200,160,.12)}
112
+ #q-input::placeholder{color:var(--muted)}
113
+ #q-upload{width:36px;height:36px;display:grid;place-items:center;
114
+ background:var(--panel-2);border:1px solid var(--border);border-radius:50%;
115
+ cursor:pointer;font-size:14px;color:var(--muted);transition:all .15s}
116
+ #q-upload:hover{color:var(--teal);border-color:var(--border-strong)}
117
+ #q-send{width:38px;height:38px;border-radius:50%;background:var(--teal);
118
+ color:#03252a;border:none;cursor:pointer;font-size:18px;font-weight:700;
119
+ box-shadow:0 0 18px rgba(0,200,160,.45);transition:all .15s}
120
+ #q-send:hover{transform:scale(1.06);box-shadow:0 0 24px rgba(0,200,160,.7)}
121
+ #q-send.loading{animation:spin 1s linear infinite}
122
+ @keyframes spin{to{transform:rotate(360deg)}}
123
+ .mode-row{position:absolute;right:24px;bottom:62px;display:flex;gap:8px}
124
+ .mode-row button{background:var(--panel-2);border:1px solid var(--border);
125
+ border-radius:18px;padding:6px 12px;font-size:11px;color:var(--text);
126
+ cursor:pointer;letter-spacing:.5px;display:flex;align-items:center;gap:6px}
127
+ .mode-row button:hover{border-color:var(--border-strong)}
128
+ .mode-row .badge{background:rgba(0,200,160,.18);padding:1px 6px;border-radius:8px;
129
+ font-family:var(--font-mono);font-size:10px;color:var(--teal)}
130
+
131
+ /* COUNCIL */
132
+ #council{position:fixed;left:50%;bottom:130px;transform:translateX(-50%) translateY(20px);
133
+ width:min(580px,92vw);max-height:50vh;overflow-y:auto;padding:20px;z-index:25;
134
+ opacity:0;pointer-events:none;transition:all .4s cubic-bezier(.34,1.56,.64,1)}
135
+ #council.show{opacity:1;transform:translateX(-50%) translateY(0);pointer-events:auto}
136
+ #council.hidden{display:none}
137
+ .council-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}
138
+ .council-title{font-size:11px;letter-spacing:2.2px;color:var(--muted);font-weight:600}
139
+ .council-title span{color:var(--teal);margin-left:6px}
140
+ #council-close{background:none;border:none;color:var(--muted);font-size:22px;
141
+ cursor:pointer;line-height:1}
142
+ #council-close:hover{color:var(--text)}
143
+ .agent-card{border-left:3px solid;padding:12px 14px;margin:10px 0;
144
+ background:rgba(10,26,32,.55);border-radius:8px;animation:slideIn .4s ease both}
145
+ @keyframes slideIn{from{opacity:0;transform:translateX(-12px)}
146
+ to{opacity:1;transform:translateX(0)}}
147
+ .agent-card .row1{display:flex;align-items:center;gap:8px;margin-bottom:6px}
148
+ .agent-card .ad-dot{width:10px;height:10px;border-radius:50%;box-shadow:0 0 8px currentColor}
149
+ .agent-card .name{font-weight:700;font-size:13px}
150
+ .agent-card .archetype{font-size:10px;background:rgba(0,0,0,.25);padding:2px 7px;
151
+ border-radius:8px;letter-spacing:.8px;color:var(--muted)}
152
+ .agent-card .thinking{color:var(--muted);font-size:11px;margin-top:4px;font-style:italic;
153
+ line-height:1.45}
154
+ .agent-card .stance{font-size:12.5px;margin-top:8px;color:var(--text)}
155
+ .agent-card .conf{font-family:var(--font-mono);font-size:10px;color:var(--teal);
156
+ margin-top:6px}
157
+ .agent-card.speaking{box-shadow:0 0 0 1px rgba(0,200,160,.45);
158
+ animation:speakPulse 1.2s ease-in-out infinite}
159
+ @keyframes speakPulse{0%,100%{transform:translateX(0)}50%{transform:translateX(2px)}}
160
+ #council-synth{margin-top:14px;padding-top:14px;border-top:1px solid var(--border);
161
+ color:var(--text);font-size:13px;line-height:1.55}
162
+ #debate-audio{width:100%;margin-top:14px;height:36px;border-radius:18px;
163
+ background:rgba(0,0,0,.3)}
164
+ #debate-audio::-webkit-media-controls-panel{background:rgba(8,24,30,.7)}
165
+
166
+ /* NODE DETAIL */
167
+ #node-detail{position:fixed;width:280px;padding:18px;z-index:24;
168
+ opacity:0;pointer-events:none;transition:opacity .25s}
169
+ #node-detail.show{opacity:1;pointer-events:auto}
170
+ #node-detail .nd-title{font-size:14px;font-weight:700;margin-bottom:8px;
171
+ display:flex;align-items:center;gap:8px}
172
+ #node-detail .nd-type{font-size:10px;letter-spacing:1.4px;color:var(--muted);
173
+ text-transform:uppercase;margin-bottom:12px}
174
+ #node-detail .nd-stats{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;
175
+ margin-bottom:10px}
176
+ #node-detail .nd-pill{background:rgba(0,0,0,.25);padding:6px 8px;border-radius:8px;
177
+ font-size:10px;text-align:center;color:var(--muted)}
178
+ #node-detail .nd-pill b{display:block;font-family:var(--font-mono);color:var(--cyan);font-size:13px}
179
+
180
+ /* TOASTS */
181
+ #toasts{position:fixed;top:24px;left:50%;transform:translateX(-50%);z-index:30;
182
+ display:flex;flex-direction:column;gap:8px;pointer-events:none}
183
+ .toast{background:rgba(8,28,35,.92);border:1px solid var(--border);border-left:3px solid var(--teal);
184
+ padding:10px 16px;border-radius:10px;font-size:12px;
185
+ box-shadow:0 8px 20px rgba(0,0,0,.4);animation:toastIn .35s ease both,toastOut .4s ease 4s both}
186
+ .toast.warn{border-left-color:var(--warm)}
187
+ .toast.error{border-left-color:#e0604c}
188
+ @keyframes toastIn{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
189
+ @keyframes toastOut{to{opacity:0;transform:translateY(-8px)}}
190
+
191
+ /* SEED HINT */
192
+ #seed-hint{position:fixed;left:50%;top:38%;transform:translate(-50%,-50%);
193
+ z-index:8;padding:9px 18px;border-radius:20px;
194
+ background:rgba(8,24,30,.7);border:1px solid var(--border);
195
+ font-size:12px;color:var(--muted);backdrop-filter:blur(10px);
196
+ animation:hintFloat 3.5s ease-in-out infinite;pointer-events:none;
197
+ letter-spacing:.4px}
198
+ #seed-hint.hidden{display:none}
199
+ @keyframes hintFloat{0%,100%{opacity:.7;transform:translate(-50%,-50%)}
200
+ 50%{opacity:1;transform:translate(-50%,calc(-50% - 4px))}}
201
+
202
+ /* RESPONSIVE */
203
+ @media (max-width:900px){
204
+ #legend{width:180px;padding:14px;top:14px;right:14px}
205
+ #minimap{width:130px;height:84px;left:64px;bottom:124px}
206
+ .minimap-label{left:70px;bottom:212px}
207
+ #zoom-ctrl{right:14px;bottom:124px}
208
+ #stats{gap:20px;padding:0 18px}
209
+ .stat-value{font-size:18px}
210
+ .stat-label{font-size:9px}
211
+ }
212
+ @media (max-width:640px){
213
+ #timeline{width:42px}
214
+ #legend{width:160px;padding:12px;top:10px;right:10px;font-size:11px}
215
+ #legend .lbl{display:none}
216
+ .meta-row{font-size:10px}
217
+ #minimap{display:none}
218
+ .minimap-label{display:none}
219
+ #stats{display:none}
220
+ #query-bar{left:42px}
221
+ .mode-row{display:none}
222
+ #council{width:96vw;max-height:62vh;bottom:80px}
223
+ #zoom-ctrl{bottom:80px}
224
+ }
225
+
226
+ /* HIDE GRADIO if ever shown */
227
+ .gradio-container,footer,.built-with,gradio-app>.main>.contain{display:none!important}
228
+
229
+ .hidden{display:none!important}
frontend/dist/assets/nodes.js ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Bioluminescent node rendering. */
2
+ const TYPE_COLOR = {
3
+ CORE: '#c8a44a',
4
+ DOMAIN: '#9060c0',
5
+ AGENT: '#e05080',
6
+ TOOL: '#c8903a',
7
+ PROJECT: '#40d0e0',
8
+ LIFE_EVENT: '#40e080',
9
+ EMOTION: '#e080b0',
10
+ PERSON: '#d070a0',
11
+ VALUE: '#8050c0',
12
+ MEMORY: '#30c0d0',
13
+ };
14
+
15
+ function shade(hex, amt) {
16
+ const c = parseInt(hex.slice(1), 16);
17
+ let r = (c >> 16) + amt, g = ((c >> 8) & 255) + amt, b = (c & 255) + amt;
18
+ r = Math.max(0, Math.min(255, r)); g = Math.max(0, Math.min(255, g)); b = Math.max(0, Math.min(255, b));
19
+ return '#' + ((r << 16) | (g << 8) | b).toString(16).padStart(6, '0');
20
+ }
21
+
22
+ window.colorFor = (type) => TYPE_COLOR[type] || '#9060c0';
23
+
24
+ window.drawNode = function (ctx, n, t, zoom) {
25
+ const ageMs = t - (n.born || t);
26
+ const age = Math.min(1, ageMs / 700);
27
+ // overshoot easeOutBack
28
+ const k = age;
29
+ const overshoot = 1 + 1.7 * (k - 1) ** 3 + 0.7 * (k - 1) ** 2;
30
+ const fadeScale = age < 1 ? overshoot : 1;
31
+
32
+ const pulse = 1 + 0.06 * Math.sin((t / 1000) * 0.55 + (n.phase || 0));
33
+ const r = n.radius * pulse * fadeScale;
34
+ const x = n.x, y = n.y;
35
+
36
+ ctx.globalAlpha = age;
37
+
38
+ // outer halo (soft glow)
39
+ const halo = ctx.createRadialGradient(x, y, r * 0.9, x, y, r * 2.4);
40
+ halo.addColorStop(0, n.color + 'aa');
41
+ halo.addColorStop(1, n.color + '00');
42
+ ctx.fillStyle = halo;
43
+ ctx.beginPath(); ctx.arc(x, y, r * 2.4, 0, Math.PI * 2); ctx.fill();
44
+
45
+ // rotating dashed orbital ring (always for CORE)
46
+ if (n.type === 'CORE') {
47
+ ctx.save();
48
+ ctx.translate(x, y);
49
+ ctx.rotate(t * 0.0004);
50
+ ctx.strokeStyle = n.color + '66';
51
+ ctx.lineWidth = 1.2 / zoom;
52
+ ctx.setLineDash([6 / zoom, 6 / zoom]);
53
+ ctx.beginPath(); ctx.arc(0, 0, r * 1.8, 0, Math.PI * 2); ctx.stroke();
54
+ ctx.setLineDash([]);
55
+ ctx.restore();
56
+ }
57
+
58
+ // main sphere
59
+ const grad = ctx.createRadialGradient(x - r * 0.35, y - r * 0.35, r * 0.1, x, y, r);
60
+ grad.addColorStop(0, shade(n.color, 50));
61
+ grad.addColorStop(0.55, n.color);
62
+ grad.addColorStop(1, shade(n.color, -55));
63
+
64
+ ctx.shadowColor = n.color;
65
+ ctx.shadowBlur = 24 * pulse;
66
+ ctx.fillStyle = grad;
67
+ ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
68
+ ctx.shadowBlur = 0;
69
+
70
+ // specular highlight
71
+ ctx.fillStyle = 'rgba(255,255,255,.45)';
72
+ ctx.beginPath();
73
+ ctx.ellipse(x - r * 0.32, y - r * 0.32, r * 0.26, r * 0.16, -0.5, 0, Math.PI * 2);
74
+ ctx.fill();
75
+
76
+ // counter badge (small inset)
77
+ if (n.count != null && zoom > 0.5) {
78
+ const bx = x - r * 1.05, by = y;
79
+ ctx.fillStyle = 'rgba(0,0,0,.55)';
80
+ ctx.beginPath();
81
+ ctx.roundRect ? ctx.roundRect(bx - 12, by - 8, 22, 16, 8) : ctx.rect(bx - 12, by - 8, 22, 16);
82
+ ctx.fill();
83
+ ctx.fillStyle = '#fff'; ctx.font = `bold ${10}px Space Mono`;
84
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
85
+ ctx.fillText(String(n.count), bx - 1, by);
86
+ }
87
+
88
+ // label
89
+ if (zoom > 0.4 && n.label) {
90
+ const fs = Math.min(14, 11 * Math.max(1, zoom * 0.9));
91
+ ctx.fillStyle = '#c8e8f0';
92
+ ctx.font = `500 ${fs / zoom}px Inter, sans-serif`;
93
+ ctx.textAlign = 'center'; ctx.textBaseline = 'top';
94
+ ctx.fillText(n.label.length > 22 ? n.label.slice(0, 20) + '…' : n.label,
95
+ x, y + r + 6 / zoom);
96
+ }
97
+
98
+ // pulse highlight ring (when pulsed via UI directive)
99
+ if (n.pulseUntil && t < n.pulseUntil) {
100
+ const p = (n.pulseUntil - t) / 1200;
101
+ ctx.strokeStyle = `rgba(0,229,255,${p})`;
102
+ ctx.lineWidth = 2 / zoom;
103
+ ctx.beginPath(); ctx.arc(x, y, r + (1 - p) * 30, 0, Math.PI * 2); ctx.stroke();
104
+ }
105
+
106
+ ctx.globalAlpha = 1;
107
+ };
frontend/dist/index.html ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" />
6
+ <title>Elysium</title>
7
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Ctext y='50' font-size='50'%3E🌿%3C/text%3E%3C/svg%3E">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Mono:wght@400;700&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
11
+ <link rel="stylesheet" href="/assets/elysium.css" />
12
+ </head>
13
+ <body data-alert="CALM">
14
+
15
+ <!-- BACKGROUND CANVAS -->
16
+ <canvas id="elysium-canvas"></canvas>
17
+
18
+ <!-- LEFT TIMELINE -->
19
+ <aside id="timeline">
20
+ <div class="t-edge"></div>
21
+ <div class="years">
22
+ <div class="year">2016</div><div class="year">2017</div><div class="year">2018</div>
23
+ <div class="year">2019</div>
24
+ <div class="year-active">2026</div>
25
+ <div class="year">2027</div><div class="year">2028</div><div class="year">2029</div>
26
+ <div class="year">2030</div>
27
+ </div>
28
+ </aside>
29
+
30
+ <!-- LEGEND PANEL -->
31
+ <section id="legend" class="glass">
32
+ <div class="legend-header">LEGEND</div>
33
+ <div id="legend-items">
34
+ <div class="legend-item" data-type="CORE">
35
+ <span class="dot" style="background:#c8a44a;color:#c8a44a"></span>
36
+ <span class="badge">1</span><span class="lbl">Civilization</span>
37
+ </div>
38
+ </div>
39
+ <div class="meta-row"><span>🏛 GAPS</span><span id="legend-gaps" class="meta-val">20%</span></div>
40
+ <div class="meta-row"><span>⟳ Updates</span><span id="legend-updates" class="meta-val">30%</span></div>
41
+ </section>
42
+
43
+ <!-- MINIMAP -->
44
+ <canvas id="minimap"></canvas>
45
+ <div class="minimap-label">CIVILIZATION MAP</div>
46
+
47
+ <!-- ZOOM CONTROLS -->
48
+ <div id="zoom-ctrl">
49
+ <button id="z-in" title="Zoom in">+</button>
50
+ <button id="z-fit" title="Fit all">⊡</button>
51
+ <button id="z-out" title="Zoom out">−</button>
52
+ </div>
53
+
54
+ <!-- ALERT SCRIM -->
55
+ <div id="alert-scrim"></div>
56
+
57
+ <!-- STATS BAR -->
58
+ <section id="stats" class="glass">
59
+ <div class="stat">
60
+ <div class="stat-icon">↗</div>
61
+ <div><div class="stat-label">Compliance Rate</div><div class="stat-value" id="s-health">—</div></div>
62
+ </div>
63
+ <div class="stat">
64
+ <div class="stat-icon">📜</div>
65
+ <div><div class="stat-label">Total Laws</div><div class="stat-value" id="s-laws">0</div></div>
66
+ </div>
67
+ <div class="stat">
68
+ <div class="stat-icon">👥</div>
69
+ <div><div class="stat-label">Public Engagement</div><div class="stat-value" id="s-engage">0</div></div>
70
+ </div>
71
+ <div class="stat">
72
+ <div class="stat-icon">✨</div>
73
+ <div><div class="stat-label">Implementation Rate</div><div class="stat-value" id="s-impl">0%</div></div>
74
+ </div>
75
+ </section>
76
+
77
+ <!-- QUERY BAR -->
78
+ <section id="query-bar" class="glass">
79
+ <input id="q-input" placeholder="Speak to your civilization seed…" autocomplete="off"/>
80
+ <label id="q-upload" title="Attach image">
81
+ <input type="file" id="q-file" accept="image/*" hidden />📎
82
+ </label>
83
+ <button id="q-send" title="Send">→</button>
84
+ <div class="mode-row">
85
+ <button id="my-agent">🤖 My agent <span class="badge" id="agent-count">+0</span></button>
86
+ <button id="ri-analysis">✨ RI Analysis</button>
87
+ </div>
88
+ </section>
89
+
90
+ <!-- COUNCIL PANEL (slides in when agents present) -->
91
+ <section id="council" class="glass hidden">
92
+ <div class="council-head">
93
+ <div class="council-title">COUNCIL DELIBERATION <span id="council-count">+0</span></div>
94
+ <button id="council-close">×</button>
95
+ </div>
96
+ <div id="council-body"></div>
97
+ <div id="council-synth"></div>
98
+ <audio id="debate-audio" controls preload="auto"></audio>
99
+ </section>
100
+
101
+ <!-- NODE DETAIL POPOVER -->
102
+ <section id="node-detail" class="glass hidden"></section>
103
+
104
+ <!-- TOAST NOTIFICATIONS -->
105
+ <div id="toasts"></div>
106
+
107
+ <!-- INITIAL HINT -->
108
+ <div id="seed-hint">Ask anything to wake your civilization</div>
109
+
110
+ <script src="/assets/api.js"></script>
111
+ <script src="/assets/nodes.js"></script>
112
+ <script src="/assets/canvas.js"></script>
113
+ <script src="/assets/council.js"></script>
114
+ <script src="/assets/boot.js"></script>
115
+ </body>
116
+ </html>
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ffmpeg
requirements.txt ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --extra-index-url https://download.pytorch.org/whl/cu128
2
+ torch==2.8.0
3
+ hf_transfer
4
+ huggingface_hub
5
+ --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu124
6
+ llama-cpp-python
7
+ gradio==6.17.3
8
+ spaces
9
+ fastapi
10
+ pydantic>=2.0
11
+ rustworkx>=0.15.0
12
+ duckduckgo-search
13
+ httpx
14
+ icalendar
15
+ pypdf2
16
+ python-docx
17
+ pydub
18
+ soundfile
19
+ numpy
20
+ Pillow
21
+ voxcpm
22
+ python-multipart