payparse / main.py
NabilHzs's picture
Upload folder using huggingface_hub
20e9e63 verified
Raw History Blame Contribute Delete
4.6 kB
"""
main.py
-------
FastAPI application serving the PayParse multi-turn NLU assistant.
Endpoints
---------
POST /api/chat : one dialogue turn (NLU -> slot filling -> reply)
DELETE /api/session/{id} : reset a conversation
GET /health : liveness probe + engine mode
Run locally:
uvicorn main:app --reload
Run on Hugging Face Spaces:
The Dockerfile sets PORT=7860. GEMINI_API_KEY is injected as a HF Secret.
PAYPARSE_SESSION_BACKEND=memory (HF filesystem is ephemeral).
"""
from __future__ import annotations
import logging
import os
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from nlu_engine import MockNLUEngine, create_engine
from schema import ChatRequest, ChatResponse
from session_store import SessionStore
from state_manager import StateManager
STATIC_DIR = Path(__file__).parent / "static"
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Initialize shared singletons once at startup."""
offline = os.getenv("PAYPARSE_OFFLINE", "0") == "1"
use_paraphrase = os.getenv("PAYPARSE_LLM_FOLLOWUP", "0") == "1"
engine = create_engine(offline=offline)
app.state.engine = engine
app.state.store = SessionStore()
app.state.manager = StateManager(
nlu_engine=engine, use_llm_paraphrase=use_paraphrase
)
logger.info(
"PayParse started (engine=%s, llm_followup=%s)",
type(engine).__name__,
use_paraphrase,
)
yield
app.state.store.close()
app = FastAPI(
title="PayParse — Indonesian NLU Voice Assistant",
description=(
"Zero-shot LLM NLU (Gemini Flash) + slot-filling state machine "
"for Indonesian financial voice queries."
),
version="1.0.0",
lifespan=lifespan,
)
# CORS — allow the Android app (and any local dev client) to call the API.
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # portfolio project; tighten for production
allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
allow_headers=["*"],
)
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
@app.get("/")
def serve_ui():
"""Serve the simple chat UI, or API info if static dir is missing."""
index = STATIC_DIR / "index.html"
if index.exists():
return FileResponse(index)
return {
"name": "PayParse — Indonesian NLU Voice Assistant",
"endpoints": {
"chat": "POST /api/chat",
"reset": "DELETE /api/session/{id}",
"health": "GET /health",
},
"docs": "/docs",
}
@app.get("/health")
def health() -> dict:
return {
"status": "ok",
"engine": type(app.state.engine).__name__,
"offline_mode": isinstance(app.state.engine, MockNLUEngine),
}
@app.post("/api/chat", response_model=ChatResponse)
def chat(request: ChatRequest) -> ChatResponse:
"""Process one user turn in a (possibly multi-turn) conversation."""
store: SessionStore = app.state.store
state = store.get_or_create(request.session_id)
nlu_result = app.state.engine.extract(request.message, state=state)
logger.info(
"session=%s intent=%s entities=%s",
request.session_id,
nlu_result.intent.value,
nlu_result.entities.model_dump(exclude_none=True),
)
result = app.state.manager.process(
state, nlu_result, request.message, request.resolve_contacts_locally
)
store.save(result.state)
# A completed transaction ends the session; a fresh one starts next turn.
if result.is_complete:
store.delete(request.session_id)
return ChatResponse(
session_id=request.session_id,
reply=result.reply,
intent=result.state.intent,
is_complete=result.is_complete,
missing_slots=result.state.missing_slots,
payload=result.payload,
needs_contact_resolution=result.needs_contact_resolution,
target_kontak=result.target_kontak,
)
@app.delete("/api/session/{session_id}")
def reset_session(session_id: str) -> dict:
"""Discard a conversation so the user can start over."""
deleted = app.state.store.delete(session_id)
if not deleted:
raise HTTPException(status_code=404, detail="Session not found.")
return {"status": "reset", "session_id": session_id}