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;
--realneedssymfonic-core[anthropic]+ an API key - Key concepts: custom state,
depscontainer, promptsections/,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¶
- The full web-UI application with all memory layers → full_demo
- Scaffold a fresh app instead of copying → Scaffold Runtime App