from __future__ import annotations import json from pathlib import Path from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, Field from .config_loader import business_config from .leads import add_lead, list_leads from .ragflow_client import RagflowClient, RagflowError from .scheme_engine import create_session, handle_message from .session_store import store from .settings import ROOT, settings STATIC_DIR = Path(__file__).resolve().parent / "static" CORPUS_DIR = ROOT / "knowledge" / "corpus" app = FastAPI(title=settings.app_name, version="0.2.0") app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") class ChatRequest(BaseModel): message: str = Field(..., min_length=1) session_id: str | None = None class SessionResponse(BaseModel): session_id: str class HandoffRequest(BaseModel): session_id: str name: str = Field(..., min_length=1, max_length=40) phone: str = Field(..., min_length=5, max_length=30) note: str = "" @app.get("/") def index() -> FileResponse: return FileResponse(STATIC_DIR / "index.html") @app.get("/api/health") def health() -> dict: rag = RagflowClient().health() return { "ok": True, "app": settings.app_name, "retrieval_backend": settings.retrieval_backend, "slots": len(business_config.slots), "rules": len(business_config.rules), "ragflow": rag, "ragflow_web": settings.ragflow_web_url, "dataset_ids_configured": bool(settings.ragflow_dataset_ids.strip()), } @app.get("/api/config/slots") def get_slots() -> dict: return business_config.slots_doc @app.get("/api/config/decision-tree") def get_tree() -> dict: return business_config.tree_doc @app.post("/api/admin/reload-config") def reload_config() -> dict: business_config.reload() return {"reloaded": True, "slots": len(business_config.slots), "rules": len(business_config.rules)} @app.get("/api/knowledge/files") def knowledge_files() -> dict: files = [] if CORPUS_DIR.exists(): for path in sorted(CORPUS_DIR.glob("*.md")): files.append({"name": path.name, "bytes": path.stat().st_size}) return {"files": files} @app.get("/api/ragflow/status") def ragflow_status() -> dict: client = RagflowClient() health = client.health() datasets: list[dict] = [] error = "" if health.get("ok") and settings.ragflow_api_key: try: for item in client.list_datasets(): datasets.append( { "id": item.get("id"), "name": item.get("name"), "document_count": item.get("document_count"), "chunk_count": item.get("chunk_count"), } ) except Exception as exc: error = str(exc) return { "health": health, "web": settings.ragflow_web_url, "api_key_set": bool(settings.ragflow_api_key), "dataset_ids": [x for x in settings.ragflow_dataset_ids.split(",") if x.strip()], "datasets": datasets, "error": error, } @app.get("/api/leads") def api_leads() -> dict: return {"leads": list_leads()} @app.post("/api/sessions", response_model=SessionResponse) def api_create_session() -> SessionResponse: session = create_session() return SessionResponse(session_id=session.id) @app.post("/api/chat") def api_chat(body: ChatRequest) -> dict: session_id = body.session_id if not session_id: session_id = create_session().id try: return handle_message(session_id, body.message) except KeyError: raise HTTPException(status_code=404, detail="session_not_found") from None @app.post("/api/handoff") def api_handoff(body: HandoffRequest) -> dict: session = store.get(body.session_id) slots = session.slots if session else {} scheme = session.last_scheme if session else None lead_id = add_lead( session_id=body.session_id, name=body.name.strip(), phone=body.phone.strip(), note=body.note.strip(), slots_json=json.dumps(slots, ensure_ascii=False), scheme_json=json.dumps(scheme or {}, ensure_ascii=False), ) if session: session.phase = "handoff" return {"ok": True, "lead_id": lead_id}