the-echo / echo /CLAUDE.md
frankyy03's picture
Deploy The Echo (MockLLM path): Gradio app + echo package
897d5bd verified
|
Raw
History Blame Contribute Delete
6.76 kB

A newer version of the Gradio SDK is available: 6.20.0

Upgrade

CLAUDE.md

Guidance for working on The Echo — an agentic tree of the lives you didn't live.

What this project is

The Echo takes one fork in a person's life ("I left Brazil instead of staying") and grows a tree of alternate selves. Each node is a coherent alternate life; each branch is a dramatic turning point. The user navigates the tree, and each echo speaks back in a subtly altered version of their own voice.

It is built for the Thousand Token Wood track of a small-model hackathon (all models ≤32B, Gradio + HF Spaces, demo video + social post). The award surface it targets: Thousand Token Wood (delight), Best Agent (true multi-agent + tools), Off-Brand (the living tree UI), Best Demo (voice + emotional reveal), and Tiny Titan (if a ≤4B model holds coherence).

This is an agentic inference system, not an RL project. There is no training loop, no reward, no policy gradient. The "intelligence" is three coordinated agents using a frozen LLM zero-shot. Do not add RL unless explicitly asked.

The core idea that must never break

The single hardest, most important property: "the same you" stays recognizable across every branch. A child life is always generated as a causal consequence of its parent's WorldState — never invented from scratch. Age math must add up, relationships and dependents must persist or change for a reason, carried losses must echo forward. The Verifier exists to enforce this. If a change would let branches drift into unrelated people, it is wrong.

Architecture (dependency order — leaves first)

core/world_state.py   data: WorldState, LifeFacts, EmotionalTone   (no deps)
core/tree.py          LifeTree: branching graph + history           (-> world_state)
llm/client.py         LLMClient; MockLLM (offline) + LocalLLM       (HF model)
tools/research.py     world-grounding (real place/era detail)       (-> world_state)
tools/voice.py        TTS; pitch shifts by emotional valence        (-> world_state)
agents/base.py        thin Agent base                               (-> llm)
agents/curator.py     grows child life from parent (causal)         (-> base, world_state)
agents/screenwriter.py plans the next two dramatic forks            (-> base, world_state)
agents/verifier.py    audits branch coherence; triggers regen       (-> base, world_state)
core/orchestrator.py  conductor: curator→verifier loop, voice, plan (-> everything)
smoke_test.py         full pipeline with mocks, no GPU              (-> everything)

When rewriting files, follow this same order: leaves before roots, so the project never sits in a broken state.

The agentic loop (one user choice → one new node)

  1. Curator reads parent + full branch history (+ research grounding) → child WorldState
  2. Verifier audits child vs branch; orchestrator regenerates up to max_regen times
  3. Voice tool synthesizes the child's spoken line (pitch nudged by valence)
  4. Screenwriter plans the child's next two forks
  5. node added to LifeTree

The orchestrator is UI-agnostic. The (future) Gradio app calls seed() once, then choose_fork(node_id, i) per click, and renders graph().

Hard rules

  • Keep MockLLM and the mock tools working at all times. python -m echo.smoke_test must pass with zero ML dependencies installed. This is the project's correctness gate. Every change is validated here first.
  • Lazy-import heavy deps. torch, transformers, peft, bitsandbytes, TTS backends are imported inside the methods that need them, never at module top level. Importing any echo module must stay cheap and dependency-free.
  • Never import orchestrator from core/__init__.py. It pulls in agents + tools, and tools/voice.py imports core.world_state — importing the orchestrator at package load creates a circular import. Import it explicitly: from echo.core.orchestrator import Orchestrator.
  • Agents stay single-purpose and thin. One responsibility each. This is what makes the ≤4B (Tiny Titan) path viable — small focused calls instead of one giant generation. Don't merge agents.
  • All model access goes through LLMClient. Agents never touch transformers directly. Swapping 14B ↔ 3B must remain a one-line config change.
  • Favor concrete, mundane, specific detail over vague profundity in any prompt or generated content. Specificity is what makes a life feel real; vagueness is the main failure mode.

Quantization & LoRA (planned, on the LocalLLM)

These are the next technical additions and belong only in llm/client.py:

  • 4-bit quantization via BitsAndBytesConfig (bitsandbytes): load_in_4bit, NF4, double quantization. This is what lets a 14B fit on a free-tier GPU and makes the honest small-model fit real.
  • Optional LoRA adapters for light supervised fine-tuning of an agent (e.g. teaching the Curator format + tone). This is SFT, not RL. Per-agent adapters are loadable but training is out of scope unless asked.

The MockLLM must remain untouched by either — it stays the offline test path.

Tiny Titan experiment

The orchestrator tracks GrowthStats.regen_rate (regenerations / nodes grown). This is the metric that decides 14B vs ≤4B: run the same pipeline on both and compare how often each model breaks branch coherence. A ≤4B model with a low regeneration rate wins the honest-fit argument and the Tiny Titan award.

Tone & safety of the experience

Dramatic forks touch real emotion (loss, regret). The framing must be wonder, not regret — "look how many vibrant lives lived inside one choice," never "look what you lost." The closing artifact (final_map_summary) deliberately ends with gentleness ("none of them are more real than the one you're in"). Keep that spirit in any new generated content. This is a toy that should leave people lighter, not heavier.

How to verify any change

python -m echo.smoke_test     # must print "ALL STAGES PASSED ✓"

If that passes, the agentic loop, tree, tools, and serialization are all intact. Only after that should you test with a real model.

Commands

# offline correctness gate (no GPU, no ML deps)
python -m echo.smoke_test

# real model run (requires: pip install -r requirements.txt)
python -c "
from echo.llm.client import LocalLLM, LLMConfig
from echo.tools.research import MockResearch
from echo.tools.voice import MockVoice
from echo.core.orchestrator import Orchestrator
llm = LocalLLM(LLMConfig(model_name='Qwen/Qwen2.5-3B-Instruct')); llm.load()
orch = Orchestrator(llm, MockResearch(), MockVoice())
root = orch.seed('I stayed instead of moving abroad', base_age=24)
print(root.facts.constraints_text())
"