# Project export: PromptToPath

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: Tell it what you want to learn, a team of AI agents debates, researches, and draws you a personalized, resource-backed roadmap, powered by a system on Fetch.ai + ASI:One.
- Devpost: https://devpost.com/software/prompttopath
- GitHub: https://github.com/Artsyadi/CalHacks_Berkeley
- Demo: https://asi1.ai/shared-chat/fe8d1d70-642e-4f6a-a214-60f2c6856b0a
- Video: https://www.youtube.com/embed/tG__fkuy9fs?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Aditya Arunkumar Dawale (11 commits), Claude Opus 4.8 (6 commits)

## Devpost submission (written by the team)

### Inspiration

Learning something new in 2026 means drowning in content. Ask a chatbot for "how to become an ML engineer" and you get generic advice and, worse, made-up or dead links. There's no trustworthy, structured, visual path and no real sense that anything more sophisticated than autocomplete is happening behind the scenes. I wanted to prove two things: (1) you can turn any learning intent into a realistic, resourced plan, and (2) a team of collaborating agents can do it far better than a single model and do it where users already are: inside an ASI:One conversation, with no app to install.

### What it does

Tell PromptToPath what you want to learn "become an ML engineer in 6 months," "learn to cook Italian food," "I want to learn how to dance" and it returns, right inside an ASI:One chat: a visual Mermaid diagram of your learning path a phased, time-boxed roadmap with realistic milestones verified, clickable resources for each topic (YouTube videos, docs, courses) every link HTTP-validated, so nothing is dead or hallucinated How I built it A multi-agent system on Fetch.ai (uAgents), discoverable and usable through ASI:One: Orchestrator - the only public-facing agent. Implements the Agent Chat Protocol (so ASI:One can talk to it) and a sandboxed Payment Protocol. Coordinates the pipeline and delivers the final answer. Planner - runs a two-pass propose-critique-and-finalize debate to design a realistic roadmap, returned as structured JSON. Resource - fetches real links from live web search (Tavily) and HTTP-validates every URL concurrently within a strict time budget. Graph - renders the enriched roadmap as a Mermaid diagram plus a clean markdown outline. Challenges I ran into Mailbox auth was flaky for agent-to-agent messaging. Routing every internal hop through Agentverse mailboxes caused intermittent "Could not validate credentials" failures. I re-architected so only the orchestrator uses a mailbox (for ASI:One); the worker agents communicate over fast local HTTP endpoints. That single change made the pipeline reliable. ASI:One has a response window. A roadmap that arrived too late was silently dropped. I had to make the pipeline fast and stream heartbeats to hold the session open. What I learned The practical realities of building on Agentverse + ASI:One: when to use mailboxes vs. local transport, and how a chat front-end's timing constraints shape backend design. Reliability is a feature. Heartbeats, timeouts, and fallbacks were the difference between "demo that breaks" and "demo that works every time." ##

### What's next

Real payments - flip the Payment Protocol out of sandbox into live Stripe checkout for premium deep-dive roadmaps. Personalization - adapt to the learner's current skill level, weekly time budget, and preferred formats; track progress across sessions. Richer resources & interactivity - more sources, and the ability to refine any phase ("go deeper on transformers," "make it 3 months instead")

## README (from the GitHub repository)

# PromptToPath 🚀

**A better way of learning in the age of AI.**

PromptToPath is a multi-agent system on [Fetch.ai](https://fetch.ai) that turns *any*
learning intent — "become an ML engineer in 6 months", "learn to cook Italian food in
4 weeks", "understand transformers" — into a **personalized, time-boxed roadmap** with a
**visual diagram** and **real, validated learning resources** (YouTube videos, docs,
courses). The entire experience happens inside an **ASI:One** conversation — no custom
frontend required.

It's not another chatbot. Specialized agents **debate, research, and draw**:

- a **Planner** that runs an internal Proposer → Critic → Synthesizer debate (cross-model:
  ASI:One + Claude) so the roadmap is realistic, not generic;
- a **Resource** agent that fetches **real** links from the YouTube Data API and web search,
  then **HTTP-validates every URL** so nothing is hallucinated or dead;
- a **Graph** agent that renders the roadmap as a **Mermaid diagram + outline** in chat;
- an **Orchestrator** that coordinates them, recovers from failures, and (optionally)
  gates a "premium" roadmap behind a **sandboxed Payment Protocol**.

---

## Problem · target user · outcome

- **Problem:** Learners are drowning in content. Generic AI answers give vague advice and
  often invent links. There's no trustworthy, structured, *visual* path with verified resources.
- **Target user:** Anyone learning anything self-directed — career switchers, students,
  hobbyists.
- **Outcome:** One ASI:One message in → a phased, realistic roadmap + a visual map + a set of
  **verified** resources per topic, out.

---

## Architecture

The Orchestrator is the only agent ASI:One talks to. A `SharedAgentState` object flows
through a forward pipeline and returns to the Orchestrator for delivery (minimizes mailbox hops):

```
ASI:One user
    │  ChatMessage (Agent Chat Protocol)
    ▼
Orchestrator ──SharedAgentState──▶ Planner ──▶ Resource ──▶ Graph ──┐
    ▲                                                                │
    └──────────────── SharedAgentState (complete) ◀──────────────────┘
    │  ChatMessage: Mermaid diagram + outline + validated links
    ▼
ASI:One user
```

| Agent | Role | Key tech |
|-------|------|----------|
| **Orchestrator** | Chat Protocol surface; pipeline coordination; timeout fallback; sandboxed Payment Protocol | `chat_protocol_spec`, `payment_protocol_spec` |
| **Planner** | Internal multi-persona **debate** → structured roadmap | ASI:One + Claude (cross-model critic) |
| **Resource** | Real links + **HTTP validation** (drops dead/hallucinated URLs) | YouTube Data API, Tavily |
| **Graph** | Roadmap → **Mermaid** flowchart + markdown outline | pure Python |

**Reliability by design:** every cross-agent hop has a fallback. If a sub-agent fails or the
pipeline times out, the Orchestrator generates a roadmap directly via ASI:One — the conversation
never hard-fails. (See `agents/services/fallback_service.py`.)

---

## Project layout

```
agents/
  config.py                       # .env loading (accepts ASI_ONE_API_KEY or ASI:ONE_API_KEY)
  chat_common.py                  # Agent Chat Protocol helpers
  models/models.py                # SharedAgentState + Roadmap/Phase/Topic/Resource
  services/
    asi_client.py                 # ASI:One (+ optional Claude) LLM calls
    planner_service.py            # Proposer -> Critic -> Synthesizer debate
    resource_service.py           # YouTube + Tavily + link validation
    graph_service.py              # Mermaid + outline rendering
    fallback_service.py           # single-call resilient roadmap
    state_service.py              # in-memory session state
  orchestrator/                   # orchestrator_agent.py, chat_protocol.py, payment_protocol.py, sessions.py
  planner/planner_agent.py
  resource/resource_agent.py
  graph/graph_agent.py
scripts/
  print_addresses.py              # derive agent addresses from seeds
  test_pipeline.py                # local brain test (no mailbox)
```

---

## Setup

Requires **Python 3.11+**.

```bash
python -m venv .venv
# Windows:  .venv\Scripts\activate     | macOS/Linux:  source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env        # then fill in the values
```

### Environment (`.env`)

| Variable | Required | Where to get it |
|----------|----------|-----------------|
| `ASI_ONE_API_KEY` | ✅ | https://asi1.ai |
| `ANTHROPIC_API_KEY` | optional | https://console.anthropic.com (cross-model debate critic) |
| `YOUTUBE_API_KEY` | for real video links | Google Cloud → enable *YouTube Data API v3* → API key (free) |
| `TAVILY_API_KEY` | for docs/courses | https://tavily.com (free tier) |
| `*_SEED` | ✅ | any unique random strings (no spaces) |
| `*_ADDRESS` | ✅ | run `python -m scripts.print_addresses` and paste in |
| `PAYMENT_SANDBOX` | default `true` | keep `true` — no real charges, no card capture |

Without the resource keys the system still works — it just attaches fewer links (graceful degradation).

---

## Run

1. **Compute agent addresses** and paste them into `.env`:
   ```bash
   python -m scripts.print_addresses
   ```
2. **Quick brain test** (no mailbox needed):
   ```bash
   python -m scripts.test_pipeline "Give me a roadmap to become an ML engineer in 6 months"
   ```
3. **Start all four agents**, each in its own terminal:
   ```bash
   make orchestrator      # or: python -m agents.orchestrator.orchestrator_agent
   make planner           #     python -m agents.planner.planner_agent
   make resource          #     python -m agents.resource.resource_agent
   make graph             #     python -m agents.graph.graph_agent
   ```
   On Windows without `make`, use the `python -m ...` commands directly.
4. **Connect each agent's mailbox:** open the Agent Inspector URL each agent prints on startup
   (or find it on [agentverse.ai](https://agentverse.ai)), and click **Connect → Mailbox**.
5. **Use it in ASI:One:** open [asi1.ai](https://asi1.ai), find the Orchestrator agent, and send:
   > *Give me a roadmap to become an ML engineer in 6 months.*

---

## Testing / demo checklist

- [ ] `scripts/test_pipeline.py` prints a Mermaid diagram + outline.
- [ ] All four agents register on Agentverse (mailbox connected).
- [ ] A roadmap request in ASI:One returns diagram + outline + **validated** links.
- [ ] **Resilience:** kill the Resource agent mid-run → still get a roadmap (fewer links), never an error.
- [ ] Non-technical prompt ("learn to cook Italian food in 4 weeks") also works.

---

## Challenge alignment (ASI:One Agent Challenge)

- ✅ Multiple agents **registered on Agentverse**, discoverable + usable via **ASI:One**
- ✅ **Agent Chat Protocol** implemented
- ✅ Real **tool execution** (YouTube/web APIs + validation) **and** agent-to-agent orchestration
- ✅ Full workflow completes **with no custom frontend**
- 🎁 Bonus: multi-agent debate, real-time data, reliability/recovery, sandboxed Payment Protocol

---

## Monetization (sandboxed Payment Protocol)

PromptToPath has a credible, built-in monetization model: **roadmaps are free; a
$1 "premium" deep-dive roadmap** (expanded resources and detail) is gated behind
Fetch.ai's **Payment Protocol**. The Orchestrator implements the seller role and
publishes the `AgentPaymentProtocol` manifest.

For the hackathon this runs in **sandbox mode** (`PAYMENT_SANDBOX=true`) — **no
cards are collected and no real money moves**. The seller verifies and settles the
transaction automatically so the full `CommitPayment → CompletePayment` handshake
is demonstrable end-to-end.

Demo it against a running orchestrator (no changes to the live agents):
```bash
python -m scripts.payment_demo
# Buyer commits a $1 sandbox payment → Orchestrator auto-completes → "PAYMENT COMPLETE ✅"
```
Swapping in real Stripe checkout is a config change (set `PAYMENT_SANDBOX=false` and
provide Stripe test keys); it's intentionally disabled here.

---

## License

MIT (or your choice).


## Detected evidence (automated analysis)

Indexed codebase: 32 recognized source files, 78 KB.
- Anthropic (technology) — detected in the code
- HTML (language) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (36 of 36)

```
.env.example
.gitignore
agents/__init__.py
agents/chat_common.py
agents/config.py
agents/graph/__init__.py
agents/graph/graph_agent.py
agents/models/__init__.py
agents/models/models.py
agents/orchestrator/__init__.py
agents/orchestrator/chat_protocol.py
agents/orchestrator/orchestrator_agent.py
agents/orchestrator/payment_protocol.py
agents/orchestrator/sessions.py
agents/planner/__init__.py
agents/planner/planner_agent.py
agents/resource/__init__.py
agents/resource/resource_agent.py
agents/services/__init__.py
agents/services/asi_client.py
agents/services/fallback_service.py
agents/services/graph_service.py
agents/services/planner_service.py
agents/services/resource_service.py
agents/services/state_service.py
docs/assets/README.md
docs/deck.html
docs/README.md
Makefile
README.md
requirements.txt
scripts/__init__.py
scripts/payment_demo.py
scripts/print_addresses.py
scripts/test_local_pipeline.py
scripts/test_pipeline.py
```

### Dependencies

- requirements.txt: anthropic@>=0.40.0, openai@>=1.40.0, python-dotenv@>=1.0.1, requests@>=2.31.0, tavily-python@>=0.5.0, uagents@>=0.22.0, uagents-core@>=0.3.0

### Recent commits (newest first)

- Add HTML pitch deck and screenshots
- Add sandboxed Payment Protocol buyer demo + monetization docs
- Merge branch 'main' of https://github.com/Artsyadi/CalHacks_Berkeley
- Add docs/README copy and ignore agentverse-profile.md
- Delete docs/agentverse-profiles.md
- Fix formatting inconsistencies in agentverse profiles
- docs: tidy repo-link line in Agentverse profile copy
- docs: fill real GitHub URL into Agentverse profile copy
- Merge branch 'main' of https://github.com/Artsyadi/CalHacks_Berkeley
- Initial commit: PromptToPath multi-agent learning-roadmap system
- Initial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
uagents>=0.22.0
uagents-core>=0.3.0
openai>=1.40.0
requests>=2.31.0
python-dotenv>=1.0.1
tavily-python>=0.5.0
anthropic>=0.40.0

```

### scripts/print_addresses.py

```python
"""Print each agent's deterministic address (derived from its seed in .env).

Agent addresses are derived from the seed, so we can compute them without
running the agents — then paste them into .env for inter-agent routing.

Run:  python -m scripts.print_addresses
"""
from __future__ import annotations

from uagents import Agent

from agents import config

_AGENTS = [
    ("ORCHESTRATOR_ADDRESS", config.ORCHESTRATOR_SEED),
    ("PLANNER_ADDRESS", config.PLANNER_SEED),
    ("RESOURCE_ADDRESS", config.RESOURCE_SEED),
    ("GRAPH_ADDRESS", config.GRAPH_SEED),
]


def main() -> None:
    print("\n# Paste these into your .env:\n")
    for env_name, seed in _AGENTS:
        # Constructing an Agent derives the address from the seed (no network).
        addr = Agent(name=env_name, seed=seed).address
        print(f"{env_name}={addr}")
    print()


if __name__ == "__main__":
    main()

```

### scripts/test_pipeline.py

```python
"""Local end-to-end test of the pipeline BRAINS (no agents / no mailbox).

Runs Planner debate -> Resource enrichment -> Graph rendering directly so we
can validate the core logic before wiring up Agentverse mailboxes.

Run:  python -m scripts.test_pipeline "your learning goal"
"""
from __future__ import annotations

import sys

# Windows consoles default to cp1252 and choke on emoji; force UTF-8 output.
try:
    sys.stdout.reconfigure(encoding="utf-8")
except Exception:
    pass

from agents.services import graph_service, planner_service, resource_service


def main() -> None:
    query = " ".join(sys.argv[1:]) or "Give me a roadmap to become an ML engineer in 6 months"
    print(f"\n=== GOAL: {query} ===\n")

    print("[1/3] Planner debate (Proposer -> Critic -> Synthesizer)…")
    roadmap = planner_service.build_roadmap(query)
    print(f"  -> {len(roadmap.phases)} phases, timeline: {roadmap.timeline!r}")

    print("[2/3] Resource enrichment (YouTube + Tavily + validation)…")
    roadmap = resource_service.enrich(roadmap)
    n_links = sum(len(t.resources) for p in roadmap.phases for t in p.topics)
    print(f"  -> {n_links} validated links attached")

    print("[3/3] Graph rendering (Mermaid + outline)…\n")
    mermaid, outline = graph_service.render(roadmap)

    print("------ MERMAID ------")
    print(mermaid)
    print("\n------ OUTLINE ------")
    print(outline)


if __name__ == "__main__":
    main()

```

### agents/chat_common.py

```python
"""Shared helpers for the Agent Chat Protocol used by every agent.

Keeps the boilerplate (acknowledging messages, extracting text, building a
final reply with an end-of-session marker) in one place.
"""
from __future__ import annotations

from datetime import datetime
from uuid import uuid4

from uagents import Context
from uagents_core.contrib.protocols.chat import (
    ChatAcknowledgement,
    ChatMessage,
    EndSessionContent,
    TextContent,
)


def extract_text(msg: ChatMessage) -> str:
    """Concatenate all TextContent parts of an incoming chat message."""
    out = ""
    for item in msg.content:
        if isinstance(item, TextContent):
            out += item.text
    return out.strip()


async def acknowledge(ctx: Context, sender: str, msg: ChatMessage) -> None:
    """Immediately ack receipt, as required by the chat protocol."""
    await ctx.send(
        sender,
        ChatAcknowledgement(timestamp=datetime.now(), acknowledged_msg_id=msg.msg_id),
    )


def build_chat_message(text: str, *, end_session: bool = True) -> ChatMessage:
    """Build a ChatMessage carrying text, optionally ending the session."""
    content: list = [TextContent(type="text", text=text)]
    if end_session:
        content.append(EndSessionContent(type="end-session"))
    return ChatMessage(timestamp=datetime.utcnow(), msg_id=uuid4(), content=content)


def session_id_of(ctx: Context, sender: str) -> str:
    """Best-effort stable session id. Falls back to the sender address."""
    return getattr(ctx, "session", None) and str(ctx.session) or sender

```

### scripts/test_local_pipeline.py

```python
"""Local end-to-end test that mimics ASI:One over local transport.

Starts a tiny client agent that sends a ChatMessage to the Orchestrator and
prints every ChatMessage it gets back (ack/heartbeats/final roadmap). This
exercises the full Orchestrator -> Planner -> Resource -> Graph -> Orchestrator
chain via local HTTP endpoints — no ASI:One / mailbox needed.

Run (with all 4 agents already running):  python -m scripts.test_local_pipeline
"""
from __future__ import annotations

import sys
from datetime import datetime
from uuid import uuid4

try:
    sys.stdout.reconfigure(encoding="utf-8")
except Exception:
    pass

from uagents import Agent, Context, Protocol
from uagents_core.contrib.protocols.chat import (
    ChatAcknowledgement,
    ChatMessage,
    TextContent,
    chat_protocol_spec,
)

from agents import config

QUERY = "Give me a roadmap to become an ML engineer in 6 months"

client = Agent(
    name="test-client",
    seed="prompttopath-test-client-seed-001",
    port=8009,
    endpoint=["http://127.0.0.1:8009/submit"],
)

proto = Protocol(spec=chat_protocol_spec)


@client.on_event("startup")
async def _go(ctx: Context):
    ctx.logger.info(f"client={client.address}")
    ctx.logger.info(f"sending to orchestrator={config.ORCHESTRATOR_ADDRESS}")
    await ctx.send(
        config.ORCHESTRATOR_ADDRESS,
        ChatMessage(
            timestamp=datetime.utcnow(),
            msg_id=uuid4(),
            content=[TextContent(type="text", text=QUERY)],
        ),
    )


@proto.on_message(ChatMessage)
async def _on_msg(ctx: Context, sender: str, msg: ChatMessage):
    text = "".join(c.text for c in msg.content if isinstance(c, TextContent))
    ctx.logger.info(f"##### CLIENT RECEIVED ({len(text)} chars) #####")
    print(text)
    print("##### END MESSAGE #####", flush=True)
    # ack back so the protocol is happy
    await ctx.send(
        sender,
        ChatAcknowledgement(timestamp=datetime.now(), acknowledged_msg_id=msg.msg_id),
    )


@proto.on_message(ChatAcknowledgement)
async def _on_ack(ctx: Context, sender: str, msg: ChatAcknowledgement):
    pass


client.include(proto)

if __name__ == "__main__":
    client.run()

```

### scripts/payment_demo.py

```python
"""Sandboxed Payment Protocol demo — a standalone 'buyer' client.

Demonstrates PromptToPath's monetization model end-to-end WITHOUT real money:
a buyer commits a $1 (sandbox) payment for a premium roadmap to the running
Orchestrator (the seller); the Orchestrator's sandbox handler verifies and
replies with CompletePayment. No cards, no Stripe checkout, no charge.

This talks to the ALREADY-RUNNING orchestrator over its published Payment
Protocol — it does not modify or restart any of the four agents.

Run (with the orchestrator running):  python -m scripts.payment_demo
"""
from __future__ import annotations

import sys

try:
    sys.stdout.reconfigure(encoding="utf-8")
except Exception:
    pass

from uagents import Agent, Context, Protocol
from uagents_core.contrib.protocols.payment import (
    CancelPayment,
    CommitPayment,
    CompletePayment,
    Funds,
    RequestPayment,
    payment_protocol_spec,
)

from agents import config

buyer = Agent(
    name="prompttopath-payment-buyer-demo",
    seed="prompttopath-payment-buyer-demo-seed-001",
    port=8010,
    endpoint=["http://127.0.0.1:8010/submit"],
)

proto = Protocol(spec=payment_protocol_spec, role="buyer")


@buyer.on_event("startup")
async def _buy(ctx: Context):
    ctx.logger.info("buyer=%s", buyer.address)
    ctx.logger.info("Purchasing PREMIUM roadmap for $1.00 (SANDBOX — no real charge)…")
    await ctx.send(
        config.ORCHESTRATOR_ADDRESS,
        CommitPayment(
            funds=Funds(amount="1.00", currency="USD", payment_method="stripe"),
            recipient=config.ORCHESTRATOR_ADDRESS,
            transaction_id="sandbox-demo-0001",
            description="PromptToPath premium deep-dive roadmap (sandbox)",
        ),
    )


@proto.on_message(CompletePayment)
async def _on_complete(ctx: Context, sender: str, msg: CompletePayment):
    ctx.logger.info("##### PAYMENT COMPLETE #####")
    ctx.logger.info("Seller confirmed transaction_id=%s", msg.transaction_id)
    ctx.logger.info("Sandbox payment settled — premium roadmap unlocked. ✅")


# Required by the buyer role even if unused in this demo flow.
@proto.on_message(RequestPayment)
async def _on_request(ctx: Context, sender: str, msg: RequestPayment):
    ctx.logger.info("Received payment request: %s", msg.description)


@proto.on_message(CancelPayment)
async def _on_cancel(ctx: Context, sender: str, msg: CancelPayment):
    ctx.logger.info("Payment cancelled by seller.")


buyer.include(proto)

if __name__ == "__main__":
    buyer.run()

```

### agents/config.py

```python
"""Central config + .env loading for all PromptToPath agents.

Importing this module loads the .env file once and exposes typed getters.
Both ``ASI_ONE_API_KEY`` (preferred) and the colon form ``ASI:ONE_API_KEY``
are accepted so the user's original .env keeps working.
"""
from __future__ import annotations

import os
from pathlib import Path

from dotenv import load_dotenv

# Load the .env that sits at the repo root (parent of the agents/ package).
_ENV_PATH = Path(__file__).resolve().parent.parent / ".env"
load_dotenv(_ENV_PATH)


def _get(name: str, default: str | None = None) -> str | None:
    val = os.getenv(name)
    return val if val not in (None, "") else default


# ── LLM keys ───────────────────────────────────────────────────────────────
ASI_ONE_API_KEY = _get("ASI_ONE_API_KEY") or _get("ASI:ONE_API_KEY")
ANTHROPIC_API_KEY = _get("ANTHROPIC_API_KEY")
ASI_ONE_BASE_URL = _get("ASI_ONE_BASE_URL", "https://api.asi1.ai/v1")
ASI_ONE_MODEL = _get("ASI_ONE_MODEL", "asi1")

# ── Resource agent keys ──────────────────────────────────────────────────────
YOUTUBE_API_KEY = _get("YOUTUBE_API_KEY")
TAVILY_API_KEY = _get("TAVILY_API_KEY")

# ── Agent seeds ──────────────────────────────────────────────────────────────
ORCHESTRATOR_SEED = _get("ORCHESTRATOR_SEED", "prompttopath-orchestrator-dev-seed")
PLANNER_SEED = _get("PLANNER_SEED", "prompttopath-planner-dev-seed")
RESOURCE_SEED = _get("RESOURCE_SEED", "prompttopath-resource-dev-seed")
GRAPH_SEED = _get("GRAPH_SEED", "prompttopath-graph-dev-seed")

# ── Inter-agent addresses (filled in after first run) ────────────────────────
PLANNER_ADDRESS = _get("PLANNER_ADDRESS")
RESOURCE_ADDRESS = _get("RESOURCE_ADDRESS")
GRAPH_ADDRESS = _get("GRAPH_ADDRESS")
ORCHESTRATOR_ADDRESS = _get("ORCHESTRATOR_ADDRESS")

# ── Ports ────────────────────────────────────────────────────────────────────
ORCHESTRATOR_PORT = int(_get("ORCHESTRATOR_PORT", "8001"))
PLANNER_PORT = int(_get("PLANNER_PORT", "8002"))
RESOURCE_PORT = int(_get("RESOURCE_PORT", "8003"))
GRAPH_PORT = int(_get("GRAPH_PORT", "8004"))

# ── Payment (sandboxed) ──────────────────────────────────────────────────────
PAYMENT_SANDBOX = _get("PAYMENT_SANDBOX", "true").lower() == "true"
STRIPE_SECRET_KEY = _get("STRIPE_SECRET_KEY")
STRIPE_PUBLISHABLE_KEY = _get("STRIPE_PUBLISHABLE_KEY")


def require(name: str) -> str:
    """Fetch a config value by attribute name or raise a clear error."""
    val = globals().get(name)
    if not val:
        raise RuntimeError(
            f"Missing required config '{name}'. Add it to your .env "
            f"(see .env.example)."
        )
    return val

```

### docs/deck.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PromptToPath Pitch Deck</title>
<style>
  :root{
    --bg:#0c1020; --bg2:#11162b; --card:#161d38; --ink:#eaf0ff;
    --muted:#9fb0d6; --accent:#6c8cff; --accent2:#36e2b4; --line:#2a335a;
    --good:#36e2b4; --bad:#ff7a7a;
  }
  *{box-sizing:border-box;margin:0;padding:0}
  html,body{height:100%}
  body{
    background:var(--bg); color:var(--ink);
    font-family:'Segoe UI',system-ui,-apple-system,sans-serif;
    overflow:hidden;
  }
  .slide{
    position:fixed; inset:0; display:none; flex-direction:column;
    justify-content:center; padding:6vh 8vw;
    background:radial-gradient(1200px 600px at 80% -10%, #1a2347 0%, var(--bg) 60%);
  }
  .slide.active{display:flex; animation:fade .35s ease}
  @keyframes fade{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}

  h1{font-size:5.2vw; line-height:1.05; letter-spacing:-.5px}
  h2{font-size:3vw; margin-bottom:2.2vh; letter-spacing:-.3px}
  .kicker{color:var(--accent2); font-weight:700; text-transform:uppercase;
    letter-spacing:3px; font-size:1.1vw; margin-bottom:1.6vh}
  p,li{font-size:1.55vw; line-height:1.5; color:var(--ink)}
  .muted{color:var(--muted)}
  ul{list-style:none; display:flex; flex-direction:column; gap:1.4vh; margin-top:1vh}
  li{padding-left:2.4vw; position:relative}
  li::before{content:"▸"; position:absolute; left:0; color:var(--accent)}
  .tag{display:inline-block;background:var(--card);border:1px solid var(--line);
    color:var(--muted);padding:.5vh 1.1vw;border-radius:999px;font-size:1vw;margin-top:3vh}
  .accent{color:var(--accent)}
  .accent2{color:var(--accent2)}
  .big{font-size:2vw}
  .quote{border-left:4px solid var(--accent2); padding:1.5vh 0 1.5vh 2vw;
    margin-top:3vh; font-style:italic; color:var(--ink); font-size:1.7vw}

  .row{display:flex; gap:3vw; align-items:center}
  .col{flex:1}

  .imgbox{flex:1; min-height:46vh; border:1px solid var(--line); border-radius:14px;
    background:var(--bg2); overflow:hidden; display:flex; align-items:center;
    justify-content:center; position:relative}
  .imgbox img{width:100%; height:100%; object-fit:contain; display:block}
  .ph{color:var(--muted); text-align:center; padding:2vw; font-size:1.1vw}
  .ph b{color:var(--accent); display:block; font-size:1.3vw; margin-bottom:1vh}

  table{width:100%; border-collapse:collapse; margin-top:2vh; font-size:1.35vw}
  th,td{text-align:left; padding:1.4vh 1.4vw; border-bottom:1px solid var(--line)}
  th{color:var(--accent2); font-size:1.2vw; text-transform:uppercase; letter-spacing:1px}
  td:first-child{color:var(--muted); width:22%}
  .vs-bad{color:var(--bad)} .vs-good{color:var(--good); font-weight:600}

  pre{background:var(--bg2); border:1px solid var(--line); border-radius:12px;
    padding:2.2vh 2vw; font-size:1.25vw; line-height:1.7; color:var(--ink); margin-top:1vh;
    font-family:'Cascadia Code',Consolas,monospace; white-space:pre}
  .pipe .accent{font-weight:700}

  .footer{position:fixed; bottom:2.4vh; left:8vw; right:8vw; display:flex;
    justify-content:space-between; align-items:center; color:var(--muted); font-size:1vw}
  .dots{display:flex; gap:.6vw}
  .dot{width:.7vw; height:.7vw; border-radius:50%; background:var(--line)}
  .dot.on{background:var(--accent)}
  .brand{font-weight:700; letter-spacing:.5px}
  .brand .accent2{font-weight:800}

  .links li::before{content:""}
  .links li{padding-left:0; font-family:monospace; font-size:1.25vw; color:var(--accent2)}

  .hint{position:fixed; top:2vh; right:2vw; color:var(--muted); font-size:.95vw; opacity:.6}

  @media print{
    body{overflow:visible}
    .slide{position:relative; display:flex !important; page-break-after:always;
      height:100vh; inset:auto}
    .footer,.hint{position:static}
  }
</style>
</head>
<body>

<!-- 1 TITLE -->
<section class="slide active">
  <div class="kicker">CalHacks · UC Berkeley · Fetch.ai ASI:One Agent Challenge</div>
  <h1>PromptToPath <span class="accent2">🚀</span></h1>
  <p class="big muted" style="margin-top:2vh">A better way to learn in the age of AI.</p>
  <p style="margin-top:3vh; max-width:60vw">Turn <span class="accent">any</span> learning goal into a
    personalized roadmap, with <span class="accent2">verified resources</span>, inside an ASI:One chat.</p>
  <span class="tag">Solo build · Aditya (USC)</span>
</section>

<!-- 2 PROBLEM -->
<section class="slide">
  <div class="kicker">The Problem</div>
  <h2>Learning is broken by too much content, not too little.</h2>
  <ul>
    <li>Generic AI advice like "pick a style, practice consistently"</li>
    <li>🔗 <span class="accent">Made-up or dead links</span> when you ask for resources</li>
    <li>No structure, no timeline, no <span class="accent2">visual</span> path</li>
    <li>You still have to go do all the searching yourself</li>
  </ul>
</section>

<!-- 3 SOLUTION -->
<section class="slide">
  <div class="kicker">The Solution</div>
  <h2>Tell PromptToPath what you want to learn. Get a real plan.</h2>
  <div class="row">
    <div class="col">
      <ul>
        <li>🗺️ A <span class="accent2">visual roadmap diagram</span></li>
        <li>📅 <span class="accent">Phased, time-boxed</span> milestones</li>
        <li>🎥 <span class="accent2">Verified, clickable resources</span> per topic</li>
        <li>💬 All inside <span class="accent">ASI:One</span>. No app, no frontend</li>
      </ul>
    </div>
    <div class="imgbox" data-img="a1-roadmap.png" data-label="ASI:One roadmap reply (diagram + links)"></div>
  </div>
</section>

<!-- 4 DEMO -->
<section class="slide">
  <div class="kicker">Live Demo</div>
  <h2>From prompt → roadmap in ~30 seconds.</h2>
  <div class="imgbox" data-img="a1b-diagram.png" data-label="ASI:One roadmap (diagram + phases)"></div>
  <p class="muted" style="margin-top:2vh">Prompt → "spinning up the agent team" → live progress → full roadmap with diagram and <span class="accen
[truncated — 6659 more characters]
```

### agents/services/state_service.py

```python
"""In-memory store for SharedAgentState keyed by chat_session_id.

Demonstrates the persistence pattern from the Fetch.ai quickstarter — swap
this for Redis or a database and nothing else in the pipeline changes.
"""
from __future__ import annotations

from agents.models.models import SharedAgentState


class InMemoryStateService:
    def __init__(self) -> None:
        self._store: dict[str, SharedAgentState] = {}

    def set_state(self, chat_session_id: str, state: SharedAgentState) -> None:
        self._store[chat_session_id] = state

    def get_state(self, chat_session_id: str) -> SharedAgentState | None:
        return self._store.get(chat_session_id)

    def clear(self, chat_session_id: str) -> None:
        self._store.pop(chat_session_id, None)


state_service = InMemoryStateService()

```

### agents/services/fallback_service.py

```python
"""Single-call fallback roadmap generator.

Used when the multi-agent pipeline can't complete (a sub-agent failed or
timed out). Produces a useful roadmap directly via ASI:One so the user
always gets an answer — turning a failed tool call into graceful recovery
rather than a dead conversation.
"""
from __future__ import annotations

from agents.services import asi_client

_SYSTEM = (
    "You are an expert learning-roadmap designer. Produce a clear, phased, "
    "time-boxed roadmap for the user's goal. Format in markdown with: a short "
    "summary, then phases (with timeframes) as '### ' headers, each with a "
    "bulleted list of concrete topics. Begin the reply with a single Mermaid "
    "flowchart in a ```mermaid``` block (flowchart TD) showing the phases in "
    "sequence. Suggest the *type* of resource to look for per phase, but do "
    "not invent specific URLs."
)


def generate(query: str) -> str:
    body = asi_client.asi_chat(_SYSTEM, query, temperature=0.4, max_tokens=2500)
    return (
        "Here's your learning roadmap 🚀\n"
        "_(generated in resilient mode — link enrichment was unavailable)_\n\n"
        f"{body}"
    )

```

[12 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]