Skip to content

demo_app

Level 5 · Production Patterns — how to structure a real application.

The single-file examples show mechanics. demo_app shows structure: how to organize a production agent into modules — a custom state schema, a dependency container, prompt sections, external-capability wrappers, and test fixtures — with a CLI that runs in mock, real, or capability-error modes.

This is a multi-module example. Rather than paste every file, this page maps the layout and shows the load-bearing pieces. Read the tree under examples/demo_app/ alongside it.

  • Prerequisites: none for mock mode; --real needs symfonic-core[anthropic] + an API key
  • Key concepts: custom state, deps container, prompt sections/, capabilities/, run modes

Run it

python -m examples.demo_app "What skills do you have?"            # mock mode (default)
python -m examples.demo_app "What skills do you have?" --real     # real LLM (needs API key)
python -m examples.demo_app "Fetch me some data" --real --mode capability_error
python -m examples.demo_app "Tell me a story" --real --stream     # token-by-token

Project layout

examples/demo_app/
├── __main__.py            # CLI: arg parsing, mode selection, run loop
├── state.py               # DemoAgentState — the custom state schema
├── deps.py                # DemoDeps — the dependency container
├── sections/              # prompt sections (composed by PromptBuilder)
│   ├── agent_instructions.py
│   ├── context_loader.py
│   ├── memory_section.py
│   └── skills_section.py
├── capabilities/          # external capability wrappers
│   ├── demo_fetch.py
│   └── demo_search.py
├── utils/
│   ├── fixtures.py        # deterministic test data
│   └── stdout_hook.py     # StdoutObservabilityHook — prints trace events
├── real_demo.py           # wiring for --real (AnthropicProvider)
└── e2e_demo.py            # end-to-end mode

The separation is the lesson: state, deps, prompt, capabilities, and observability each live in their own module, so each can be tested and swapped independently.

The load-bearing pieces

Custom state (state.py)

@dataclass
class DemoAgentState:
    """Demo-specific agent state."""
    query: str = ""
    demo_mode: str = "happy_path"
    user_name: str = "Demo User"
    session_id: str = field(default_factory=lambda: str(uuid4()))
    conversation_history: list[str] = field(default_factory=list)

    def __post_init__(self) -> None:
        allowed = {"happy_path", "degraded", "capability_error"}
        if self.demo_mode not in allowed:
            raise ValueError(f"Invalid demo_mode {self.demo_mode!r}. ...")

A real app carries more than the framework's base state. DemoAgentState adds app-specific fields (user_name, session_id, conversation_history) and validates them in __post_init__. Defining your own state dataclass is the supported extension point.

Gotcha: state keys the graph threads must be declared on the state schema. LangGraph silently drops keys it doesn't know about. See State Scope Mismatch.

Dependency container (deps.py)

class DemoDeps:
    """Demo dependency container. All deps injected -- no global state."""
    def __init__(self, fetch=None, search=None, mode="success", latency=0.0):
        self.fetch = fetch or DemoFetchCapability(mode=mode, latency=latency)
        self.search = search or DemoSearchCapability(mode=mode, latency=latency)

DemoDeps collects the app's external capabilities behind one object. Everything is injected (with sensible defaults), so tests can pass a fake fetch/search and drive mode/latency to simulate degraded conditions — no global state, no monkey-patching.

Prompt sections (sections/)

Each file is a PromptBuilder section (AgentInstructionsSection, MemorySection, SkillsSection, ContextLoaderSection). __main__.py composes them:

builder = PromptBuilder()
for s in [AgentInstructionsSection(), MemorySection(),
          SkillsSection(), ContextLoaderSection()]:
    builder.add_section(s)
result = await builder.build(build_state)

This is the same section pattern from e2e_basic_agent, scaled to an app: sections live in their own files and are assembled at startup.

Capabilities (capabilities/)

demo_fetch.py and demo_search.py wrap external actions behind a small interface with a mode switch, so the CLI's --mode capability_error can inject a realistic failure and you can watch the error path (from e2e_basic_agent's CapabilityError demo) end to end.

Observability (utils/stdout_hook.py)

StdoutObservabilityHook prints trace events to stdout as the agent runs — the simplest possible observability sink. Swap it for an OTEL exporter in production; see the OTEL setup and Cost Tracking.

The takeaway

When your agent outgrows a single file, split along these seams — state, deps, prompt, capabilities, observability. demo_app is a working template you can copy and rename.

What to try next

See also