Upload folder using huggingface_hub
Browse files- README.md +52 -39
- backend/config.py +49 -30
- backend/model_loader.py +76 -40
- backend/prompt_builder.py +66 -44
README.md
CHANGED
|
@@ -1,39 +1,52 @@
|
|
| 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
|
| 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 |
-
-
|
| 34 |
-
-
|
| 35 |
-
- **
|
| 36 |
-
- **
|
| 37 |
-
- **
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
- Repo: [`build-small-hackathon/elysium-MiniCPM-V-4.6-F16-GGUF`](https://huggingface.co/build-small-hackathon/elysium-MiniCPM-V-4.6-F16-GGUF)
|
| 34 |
+
- File: `elysium-f16.gguf`
|
| 35 |
+
- **Voice:** VoxCPM2 (agent debate audio drama)
|
| 36 |
+
- **Memory:** `rustworkx` hypergraph persisted to `/data`
|
| 37 |
+
- **JSON:** GBNF grammar enforces a strict ElysiumResponse schema on every forward pass
|
| 38 |
+
- **Tools:** offline-first DuckDuckGo, email, reminders, calendar, weather, calculator, fetch_url, transit_lookup, files
|
| 39 |
+
- **Frontend:** 100% custom HTML/Canvas2D on `gradio.Server` (no default Gradio UI)
|
| 40 |
+
|
| 41 |
+
Runs identically online (HF Spaces) and offline (`git clone β¦ && python app.py`).
|
| 42 |
+
|
| 43 |
+
## Required environment variables (Settings β Variables and secrets)
|
| 44 |
+
|
| 45 |
+
| Variable | Value |
|
| 46 |
+
|---|---|
|
| 47 |
+
| `ELYSIUM_MODEL_REPO` | `build-small-hackathon/elysium-MiniCPM-V-4.6-F16-GGUF` |
|
| 48 |
+
| `ELYSIUM_GGUF_FILE` | `elysium-f16.gguf` |
|
| 49 |
+
| `ELYSIUM_MMPROJ_FILE` | *(leave empty β no mmproj published yet, vision auto-disables)* |
|
| 50 |
+
| `HF_TOKEN` | your read token (optional for public repos but recommended) |
|
| 51 |
+
|
| 52 |
+
> The defaults already point at the real published repo, so you don't strictly need to set these β but doing so makes the Space's config explicit.
|
backend/config.py
CHANGED
|
@@ -1,30 +1,49 @@
|
|
| 1 |
-
"""Path detection β HF Spaces /data bucket vs local ./local_data clone.
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Path detection β HF Spaces /data bucket vs local ./local_data clone.
|
| 2 |
+
|
| 3 |
+
Environment variables (defaults match the real published repo):
|
| 4 |
+
ELYSIUM_MODEL_REPO default: build-small-hackathon/elysium-MiniCPM-V-4.6-F16-GGUF
|
| 5 |
+
ELYSIUM_GGUF_FILE default: elysium-f16.gguf
|
| 6 |
+
ELYSIUM_MMPROJ_FILE default: "" (no mmproj file is published yet β vision auto-disables)
|
| 7 |
+
HF_TOKEN optional read token for private/gated repos
|
| 8 |
+
ELYSIUM_OFFLINE "1" to force offline mode
|
| 9 |
+
"""
|
| 10 |
+
import os
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _detect() -> Path:
|
| 15 |
+
p = Path("/data")
|
| 16 |
+
if p.exists() and os.access(p, os.W_OK):
|
| 17 |
+
return p
|
| 18 |
+
local = Path(__file__).resolve().parent.parent / "local_data"
|
| 19 |
+
local.mkdir(exist_ok=True)
|
| 20 |
+
return local
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
DATA_PATH = _detect()
|
| 24 |
+
HYPERGRAPH_DB = DATA_PATH / "hypergraph" / "civilization.db"
|
| 25 |
+
FOSSILS_DIR = DATA_PATH / "fossils"
|
| 26 |
+
AUDIO_CACHE = DATA_PATH / "audio_cache"
|
| 27 |
+
NODE_POSITIONS = DATA_PATH / "node_positions"
|
| 28 |
+
REMINDERS_DB = DATA_PATH / "reminders.db"
|
| 29 |
+
CALENDAR_ICS = DATA_PATH / "calendar.ics"
|
| 30 |
+
OUTBOX = DATA_PATH / "outbox"
|
| 31 |
+
|
| 32 |
+
for d in (HYPERGRAPH_DB.parent, FOSSILS_DIR, AUDIO_CACHE, NODE_POSITIONS, OUTBOX):
|
| 33 |
+
d.mkdir(parents=True, exist_ok=True)
|
| 34 |
+
|
| 35 |
+
# βββ Model configuration βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 36 |
+
# These defaults point at the REAL published repository:
|
| 37 |
+
# https://huggingface.co/build-small-hackathon/elysium-MiniCPM-V-4.6-F16-GGUF
|
| 38 |
+
# which currently contains:
|
| 39 |
+
# - elysium-f16.gguf (the main MiniCPM-V 4.6 fine-tuned weights)
|
| 40 |
+
# No mmproj file is published yet, so MMPROJ_FILE defaults to "" and vision
|
| 41 |
+
# is gracefully disabled at load time.
|
| 42 |
+
MODEL_REPO = os.environ.get("ELYSIUM_MODEL_REPO", "build-small-hackathon/elysium-MiniCPM-V-4.6-F16-GGUF")
|
| 43 |
+
GGUF_FILE = os.environ.get("ELYSIUM_GGUF_FILE", "elysium-f16.gguf")
|
| 44 |
+
MMPROJ_FILE = os.environ.get("ELYSIUM_MMPROJ_FILE", "") # empty β no vision
|
| 45 |
+
|
| 46 |
+
# Optional auth token (read token works for public repos too and avoids rate limits)
|
| 47 |
+
HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN") or None
|
| 48 |
+
|
| 49 |
+
OFFLINE_MODE = os.environ.get("ELYSIUM_OFFLINE", "0") == "1"
|
backend/model_loader.py
CHANGED
|
@@ -1,40 +1,76 @@
|
|
| 1 |
-
"""Loads the fine-tuned GGUF via llama-cpp-python.
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
""
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Loads the fine-tuned GGUF via llama-cpp-python.
|
| 2 |
+
|
| 3 |
+
Pattern follows the HF ZeroGPU + small-talk reference:
|
| 4 |
+
- hf_hub_download files at module import (warm cache)
|
| 5 |
+
- instantiate Llama inside @spaces.GPU function on each request
|
| 6 |
+
|
| 7 |
+
Robustness:
|
| 8 |
+
- Uses HF_TOKEN if available (avoids 401 on rate-limited / gated lookups)
|
| 9 |
+
- Treats MMPROJ_FILE as optional. If unset OR download fails, vision is
|
| 10 |
+
disabled gracefully (text-only chat still works).
|
| 11 |
+
- Fails loudly with a helpful message if the main GGUF cannot be fetched.
|
| 12 |
+
"""
|
| 13 |
+
import os
|
| 14 |
+
import traceback
|
| 15 |
+
from huggingface_hub import hf_hub_download
|
| 16 |
+
from .config import MODEL_REPO, GGUF_FILE, MMPROJ_FILE, HF_TOKEN
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _download(repo_id: str, filename: str):
|
| 20 |
+
"""hf_hub_download with optional token, returns path or raises."""
|
| 21 |
+
kwargs = {"repo_id": repo_id, "filename": filename}
|
| 22 |
+
if HF_TOKEN:
|
| 23 |
+
kwargs["token"] = HF_TOKEN
|
| 24 |
+
return hf_hub_download(**kwargs)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
print(f"[model_loader] downloading {MODEL_REPO}/{GGUF_FILE} β¦")
|
| 28 |
+
try:
|
| 29 |
+
MODEL_PATH = _download(MODEL_REPO, GGUF_FILE)
|
| 30 |
+
print(f"[model_loader] main model ready: {MODEL_PATH}")
|
| 31 |
+
except Exception as e:
|
| 32 |
+
# We re-raise so the Space fails fast with a clear message rather than
|
| 33 |
+
# silently running with no brain. The traceback is already useful.
|
| 34 |
+
print(f"[model_loader] FAILED to download {MODEL_REPO}/{GGUF_FILE}: {e}")
|
| 35 |
+
print("[model_loader] Check that ELYSIUM_MODEL_REPO and ELYSIUM_GGUF_FILE "
|
| 36 |
+
"point at a real public file, and (for private repos) that HF_TOKEN is set.")
|
| 37 |
+
raise
|
| 38 |
+
|
| 39 |
+
# βββ mmproj (vision projector) is OPTIONAL ββββββββββββββββββββββββββββββββββ
|
| 40 |
+
MMPROJ_PATH = None
|
| 41 |
+
if MMPROJ_FILE:
|
| 42 |
+
print(f"[model_loader] attempting mmproj {MODEL_REPO}/{MMPROJ_FILE} β¦")
|
| 43 |
+
try:
|
| 44 |
+
MMPROJ_PATH = _download(MODEL_REPO, MMPROJ_FILE)
|
| 45 |
+
print(f"[model_loader] mmproj ready: {MMPROJ_PATH}")
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"[model_loader] mmproj unavailable ({e}) β vision disabled")
|
| 48 |
+
MMPROJ_PATH = None
|
| 49 |
+
else:
|
| 50 |
+
print("[model_loader] ELYSIUM_MMPROJ_FILE not set β vision disabled (text-only mode)")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def make_llm():
|
| 54 |
+
"""Create a fresh Llama inside a GPU context.
|
| 55 |
+
The .gguf file is filesystem-cached, so this is fast after the first call.
|
| 56 |
+
"""
|
| 57 |
+
from llama_cpp import Llama
|
| 58 |
+
|
| 59 |
+
chat_handler = None
|
| 60 |
+
if MMPROJ_PATH:
|
| 61 |
+
try:
|
| 62 |
+
from llama_cpp.llama_chat_format import MiniCPMv26ChatHandler
|
| 63 |
+
chat_handler = MiniCPMv26ChatHandler(clip_model_path=MMPROJ_PATH, verbose=False)
|
| 64 |
+
except Exception as e:
|
| 65 |
+
print(f"[model_loader] vision chat handler failed: {e}")
|
| 66 |
+
traceback.print_exc()
|
| 67 |
+
chat_handler = None
|
| 68 |
+
|
| 69 |
+
return Llama(
|
| 70 |
+
model_path=MODEL_PATH,
|
| 71 |
+
chat_handler=chat_handler,
|
| 72 |
+
n_gpu_layers=-1,
|
| 73 |
+
n_ctx=8192,
|
| 74 |
+
flash_attn=True,
|
| 75 |
+
verbose=False,
|
| 76 |
+
)
|
backend/prompt_builder.py
CHANGED
|
@@ -1,44 +1,66 @@
|
|
| 1 |
-
"""Build
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
return
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build messages for llama-cpp-python chat completion.
|
| 2 |
+
|
| 3 |
+
If vision is available (mmproj loaded), images are embedded as data URIs in
|
| 4 |
+
a multimodal `content` list. Otherwise, we fall back to a text-only message
|
| 5 |
+
so the model never errors out on image input.
|
| 6 |
+
"""
|
| 7 |
+
import base64
|
| 8 |
+
import io
|
| 9 |
+
import uuid
|
| 10 |
+
import datetime
|
| 11 |
+
from typing import Optional
|
| 12 |
+
from PIL import Image
|
| 13 |
+
|
| 14 |
+
from .model_loader import MMPROJ_PATH
|
| 15 |
+
|
| 16 |
+
SYSTEM_PROMPT = """You are Elysium β a persistent agentic civilization.
|
| 17 |
+
You ALWAYS respond with a single valid JSON object exactly matching the
|
| 18 |
+
ElysiumResponse schema v1.0.0. No preamble. No markdown fences. JSON only.
|
| 19 |
+
|
| 20 |
+
Decide complexity dynamically:
|
| 21 |
+
- SIMPLE_REPLY: trivial Q β no agents (council_deliberation.agent_outputs = [])
|
| 22 |
+
- QUERY / MORNING_BRIEFING / EVENING_REPORT: spawn 1β5 agents in agent_outputs,
|
| 23 |
+
each with thinking + stance + tts_speech_text + tts_voice_design{voice_id,pace,tone}
|
| 24 |
+
- TOOL_REQUIRED: populate tool_calls when external data is needed
|
| 25 |
+
- SPECIATION_EVENT: only on unresolved cross-domain tension
|
| 26 |
+
- Always populate ui_directives (camera_focus_node_id, pulses, threads)
|
| 27 |
+
- All node_id and edge_id values must be unique strings
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _img_to_data_uri(img: Image.Image) -> str:
|
| 32 |
+
buf = io.BytesIO()
|
| 33 |
+
img.convert("RGB").save(buf, format="JPEG", quality=88)
|
| 34 |
+
return "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def build_messages(user_text: str, image: Optional[Image.Image], hg_context: str = ""):
|
| 38 |
+
ctx = f"\n\n[Hypergraph context]\n{hg_context}" if hg_context else ""
|
| 39 |
+
|
| 40 |
+
# Vision available β multimodal content list
|
| 41 |
+
if image is not None and MMPROJ_PATH:
|
| 42 |
+
user_content = [
|
| 43 |
+
{"type": "image_url", "image_url": {"url": _img_to_data_uri(image)}},
|
| 44 |
+
{"type": "text", "text": user_text + ctx},
|
| 45 |
+
]
|
| 46 |
+
return [
|
| 47 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 48 |
+
{"role": "user", "content": user_content},
|
| 49 |
+
]
|
| 50 |
+
|
| 51 |
+
# No vision β plain text. If user attached an image, note it textually.
|
| 52 |
+
note = ""
|
| 53 |
+
if image is not None and not MMPROJ_PATH:
|
| 54 |
+
note = "\n\n[Note: user attached an image but the vision projector is not loaded; respond based on text only.]"
|
| 55 |
+
|
| 56 |
+
return [
|
| 57 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 58 |
+
{"role": "user", "content": user_text + ctx + note},
|
| 59 |
+
]
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def new_session_meta():
|
| 63 |
+
return {
|
| 64 |
+
"session_id": str(uuid.uuid4()),
|
| 65 |
+
"timestamp_utc": datetime.datetime.utcnow().isoformat() + "Z",
|
| 66 |
+
}
|