Upload folder using huggingface_hub
Browse files- README.md +39 -14
- app.py +52 -0
- backend/__init__.py +0 -0
- backend/config.py +30 -0
- backend/elysium_schema.gbnf +24 -0
- backend/grammar.py +17 -0
- backend/hypergraph/__init__.py +0 -0
- backend/hypergraph/engine.py +50 -0
- backend/hypergraph/persistence.py +40 -0
- backend/model_loader.py +40 -0
- backend/prompt_builder.py +44 -0
- backend/schema.py +231 -0
- backend/server.py +158 -0
- backend/tools/__init__.py +0 -0
- backend/tools/calculator_tool.py +19 -0
- backend/tools/calendar_tool.py +15 -0
- backend/tools/dispatcher.py +36 -0
- backend/tools/duckduckgo_tool.py +9 -0
- backend/tools/email_tool.py +19 -0
- backend/tools/fetch_url_tool.py +9 -0
- backend/tools/file_tool.py +15 -0
- backend/tools/reminder_tool.py +11 -0
- backend/tools/transit_lookup_tool.py +7 -0
- backend/tools/weather_tool.py +10 -0
- backend/tts/__init__.py +0 -0
- backend/tts/debate_sequencer.py +26 -0
- backend/tts/voxcpm_engine.py +53 -0
- elysium/.gitattributes +35 -0
- elysium/README.md +14 -0
- frontend/dist/assets/api.js +13 -0
- frontend/dist/assets/boot.js +122 -0
- frontend/dist/assets/canvas.js +339 -0
- frontend/dist/assets/council.js +84 -0
- frontend/dist/assets/elysium.css +229 -0
- frontend/dist/assets/nodes.js +107 -0
- frontend/dist/index.html +116 -0
- packages.txt +1 -0
- requirements.txt +22 -0
README.md
CHANGED
|
@@ -1,14 +1,39 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Elysium
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
pinned:
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
'&':'&','<':'<','>':'>','"':'"',"'":'''
|
| 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
|