bookly/server.py
Cody Borders 30cdea2aac Build Bookly customer support agent
A FastAPI + vanilla JS chat app fronting an Anthropic Claude agent for
order status, returns, and policy questions.

Architecture:
- agent.py: system prompt, runtime reminder injection, output validation,
  agentic tool-use loop with prompt caching on the system prompt block
- tools.py: four tools (lookup_order, check_return_eligibility,
  initiate_return, lookup_policy) with per-session SessionGuardState
  enforcing protocol ordering on the tool side
- mock_data.py: orders, return policy, and FAQ entries used as the single
  source of truth by both the prompt and the tools
- server.py: FastAPI app exposing /api/chat, /health, and the static UI
- static/: vanilla HTML/CSS/JS chat UI, no build step
- tests/: 30 tests covering tool-side enforcement, the privacy boundary,
  output validation, and the agent loop with a mocked Anthropic client
- deploy/: systemd unit and nginx site config for production
2026-04-14 22:17:59 -07:00

51 lines
1.4 KiB
Python

"""FastAPI app for Bookly. Hosts /api/chat, /health, and the static chat UI."""
from __future__ import annotations
import logging
from fastapi import FastAPI, HTTPException
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field
import agent
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s")
logger = logging.getLogger("bookly.server")
app = FastAPI(title="Bookly", docs_url=None, redoc_url=None)
class ChatRequest(BaseModel):
session_id: str = Field(..., min_length=1, max_length=128)
message: str = Field(..., min_length=1, max_length=4000)
class ChatResponse(BaseModel):
session_id: str
reply: str
@app.get("/health")
def health() -> dict:
return {"status": "ok"}
@app.get("/")
def root() -> RedirectResponse:
return RedirectResponse(url="/static/index.html")
@app.post("/api/chat", response_model=ChatResponse)
def chat(request: ChatRequest) -> ChatResponse:
try:
reply = agent.run_turn(request.session_id, request.message)
except Exception:
logger.exception("chat_failed session=%s", request.session_id)
raise HTTPException(status_code=500, detail="Something went wrong handling that message.")
return ChatResponse(session_id=request.session_id, reply=reply)
app.mount("/static", StaticFiles(directory="static"), name="static")