A customer support agent for Bookly (fictional online bookstore) built on the Anthropic Messages API with explicit, hand-rolled tool-use orchestration. Interface is voice-first (browser mic in, spoken responses out) with text as a fallback, because that's the interaction model closest to what Decagon actually ships for CX.
pip install -r requirements.txt
export ANTHROPIC_API_KEY=sk-ant-...
# Text/CLI mode (fastest for iterating on agent logic)
python -m bookly_agent.cli
# Voice/web mode
python web/server.py
# open http://localhost:5001 in Chrome (Web Speech API support), click the micNo API key handy? python tests/test_offline_demo.py runs the mocked
tool/business logic (order lookup, return eligibility, policy search)
end-to-end without touching the model, to sanity-check the backend alone.
- Order status: "What's the status of order BK-10099?"
- Multi-turn return: "I want to return something" → agent asks for the
order ID → "BK-10021" → agent looks it up, asks which item and why →
provide both → agent calls
initiate_returnand confirms the refund. - Clarifying question: "Where's my order?" (no ID given) → agent asks for the order number or email instead of guessing.
- Policy question: "How long do I have to return something?" → grounded
in
data/policies.jsonviasearch_policy_kb, not the model's own guess. - Failure path: try returning an item from order
BK-10099(still in transit) — the agent explains why it can't, instead of failing silently.
┌─────────────────────────┐
mic / text ──────▶ │ Transport layer │
│ cli.py | web UI (JS) │ <- swappable, dumb
└────────────┬─────────────┘
│ plain text turn
▼
┌─────────────────────────┐
│ BooklyAgent (agent.py) │ <- transport-agnostic
│ - owns message history │ orchestration loop
│ - calls Claude w/ tools │
│ - executes tool calls │
│ - loops until final text │
└────────────┬─────────────┘
│ structured calls
▼
┌─────────────────────────┐
│ tools.py │ <- mocked Bookly backend
│ lookup_order │ (swap for real APIs)
│ initiate_return │
│ search_policy_kb │
│ send_password_reset │
└─────────────────────────┘
The web voice UI and the CLI both call the exact same BooklyAgent.send()
method. Speech-to-text and text-to-speech happen entirely client-side
(browser SpeechRecognition / speechSynthesis) and are converted to/from
plain text before ever touching the agent — so the agent core has zero
knowledge of "voice" as a concept. That's a deliberate choice, explained below.
bookly_agent/
agent.py core orchestration loop (transport-agnostic)
tools.py mocked backend + Anthropic tool schemas
system_prompt.py the agent's behavior contract
cli.py text REPL
web/
server.py Flask API wrapping BooklyAgent
static/ voice/chat UI (Web Speech API STT/TTS)
data/
orders.json, policies.json mock backend data
tests/
test_offline_demo.py offline tests of the mocked tool layer