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
# 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
```bash
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
```bash
# 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())
"
```