"""FastAPI + Gradio entrypoint for Hugging Face Spaces.""" from __future__ import annotations import inspect from fastapi import FastAPI from gamemaster_copilot.catalog import build_catalog_index, catalog_summary_markdown, get_catalog from gamemaster_copilot.config import MODES, get_settings from gamemaster_copilot.schemas import ChatRequest, ChatResponse from gamemaster_copilot.service import CopilotService settings = get_settings() service = CopilotService(settings) app = FastAPI( title="GameMaster Design Copilot", version="0.1.0", description="CPU-first game design assistant with local RAG and a plugin-friendly JSON API.", ) @app.get("/api/health") def health() -> dict: return service.health() @app.post("/api/chat", response_model=ChatResponse) def chat(request: ChatRequest) -> ChatResponse: return service.chat(request) def _format_response(response: ChatResponse) -> str: parts = [response.answer] if response.citations: citation_lines = [] for citation in response.citations: target = citation.url or "local source" attribution = f", {citation.attribution}" if citation.attribution else "" citation_lines.append( f"- {citation.source_id}: {citation.title} ({citation.license}{attribution}) - {target}" ) parts.append("Citations:\n" + "\n".join(citation_lines)) if response.warnings: parts.append("Warnings:\n" + "\n".join(f"- {warning}" for warning in response.warnings)) return "\n\n".join(parts) def _build_gradio_ui(): try: import gradio as gr except Exception: return None catalog_entries = get_catalog() catalog_choice_labels = [ f"{entry.id} | {entry.label} | {entry.license}" for entry in catalog_entries ] catalog_label_to_id = { f"{entry.id} | {entry.label} | {entry.license}": entry.id for entry in catalog_entries } def submit(message: str, history: list[dict], project_context: str, mode: str, retrieval_k: int): history = history or [] if not message or not message.strip(): return "", history response = service.chat( ChatRequest( message=message, project_context=project_context or "", mode=mode, retrieval_k=int(retrieval_k), ) ) history = history + [ {"role": "user", "content": message}, {"role": "assistant", "content": _format_response(response)}, ] return "", history def refresh_index_status(): return service.health() def run_legal_scraper(selected_labels: list[str], max_docs_per_source: int): selected_labels = selected_labels or [] selected_ids = [catalog_label_to_id[label] for label in selected_labels if label in catalog_label_to_id] if not selected_ids: return "Select at least one catalog source before scraping.", service.health() try: manifest = build_catalog_index( selected_ids=selected_ids, index_dir=settings.index_dir, embedding_backend=settings.embedding_backend, embedding_model=settings.embedding_model, embedding_dimensions=settings.embedding_dimensions, max_docs_per_source=int(max_docs_per_source), ) health = service.reload_index() except Exception as exc: return f"Scrape failed: `{exc}`", service.health() warning_text = "" if manifest.get("warnings"): warning_text = "\n\nWarnings:\n" + "\n".join(f"- {warning}" for warning in manifest["warnings"][:20]) if len(manifest["warnings"]) > 20: warning_text += f"\n- ... {len(manifest['warnings']) - 20} more warnings" summary = ( f"Built legal catalog index with **{manifest['scraped_document_count']} documents** " f"and **{manifest['chunk_count']} chunks** from **{len(selected_ids)} catalog sources**.\n\n" f"Embedding backend: `{manifest['embedding_backend']}`\n\n" f"The chat retrieval layer has been reloaded. Current index chunks: `{health['chunk_count']}`." f"{warning_text}" ) return summary, manifest with gr.Blocks(title="GameMaster Design Copilot") as demo: gr.HTML( """
CPU-first design assistant for mechanics, level flow, balance critique, and live GM prep.