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
51 lines
1.4 KiB
Python
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")
|