""" FastAPI Application - Thin HTTP translation layer Stateless server that delegates all business logic to the orchestrator. Design principles: - No business logic in this file - Orchestrator handles all operations - Clean error handling with typed exceptions - Graceful degradation for optional features (OCR, AI) """ from fastapi import FastAPI, File, UploadFile, HTTPException, Form from fastapi.responses import JSONResponse from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field from typing import Optional import logging from contextlib import asynccontextmanager import os # Load environment variables from dotenv import load_dotenv load_dotenv() from core.orchestrator import Orchestrator from core.errors import ( ProofSystemError, ValidationError, ProofNotFoundError, OCRError ) from config.settings import settings # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # ============================================================================ # APPLICATION LIFECYCLE # ============================================================================ # Global orchestrator instance (initialized at startup) orchestrator: Optional[Orchestrator] = None @asynccontextmanager async def lifespan(app: FastAPI): """ Application lifespan handler. Initialize orchestrator at startup, cleanup at shutdown. """ global orchestrator logger.info("Starting Proof-of-Existence API") logger.info(f"OCR Enabled: {settings.OCR_ENABLED}") logger.info(f"AI Enabled: {settings.AI_ENABLED}") # Initialize orchestrator try: orchestrator = Orchestrator() logger.info("Orchestrator initialized successfully") except Exception as e: logger.error(f"Failed to initialize orchestrator: {str(e)}") raise yield # Cleanup (if needed) logger.info("Shutting down Proof-of-Existence API") # ============================================================================ # FASTAPI APP INITIALIZATION # ============================================================================ app = FastAPI( title="Proof-of-Existence API", description="Deterministic proof generation and verification with optional OCR and AI assistance", version="0.4.0", lifespan=lifespan ) # CORS middleware - PERMISSIVE for Flutter/Dreamflow app.add_middleware( CORSMiddleware, allow_origins=["*"], # Allow all origins (Flutter web, HF spaces, localhost) allow_credentials=True, allow_methods=["*"], # Allow all HTTP methods allow_headers=["*"], # Allow all headers expose_headers=["*"], # Expose all response headers max_age=3600, # Cache preflight for 1 hour ) # ============================================================================ # REQUEST/RESPONSE MODELS # ============================================================================ class TextProofRequest(BaseModel): """Request model for creating proof from text content.""" content: str = Field(..., description="Text content to create proof for") metadata: Optional[dict] = Field(None, description="Optional metadata") class VerifyProofRequest(BaseModel): """Request model for verifying a proof.""" proof_id: str = Field(..., description="Unique proof identifier") content: str = Field(..., description="Content to verify against proof") class AssistantRequest(BaseModel): """Request model for AI assistant queries.""" question: str = Field(..., description="Question to ask the AI assistant") proof_id: Optional[str] = Field(None, description="Optional proof ID for context") class ProofResponse(BaseModel): """Standard proof creation response.""" success: bool proof_id: Optional[str] = None hash: Optional[str] = None timestamp: Optional[str] = None message: str assistant: Optional[dict] = None class VerificationResponse(BaseModel): """Standard verification response.""" success: bool is_valid: Optional[bool] = None message: str assistant: Optional[dict] = None # ============================================================================ # CORS PREFLIGHT HANDLER # ============================================================================ @app.options("/{rest_of_path:path}") async def preflight_handler(rest_of_path: str): """Handle CORS preflight requests.""" return JSONResponse( content={"message": "OK"}, headers={ "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "*", "Access-Control-Allow-Headers": "*", } ) # ============================================================================ # ERROR HANDLERS # ============================================================================ @app.exception_handler(ValidationError) async def validation_error_handler(request, exc: ValidationError): """Handle validation errors with 400 Bad Request.""" return JSONResponse( status_code=400, content={ "success": False, "error": "validation_error", "message": str(exc) } ) @app.exception_handler(ProofNotFoundError) async def proof_not_found_handler(request, exc: ProofNotFoundError): """Handle proof not found with 404.""" return JSONResponse( status_code=404, content={ "success": False, "error": "proof_not_found", "message": str(exc) } ) @app.exception_handler(ProofSystemError) async def proof_system_error_handler(request, exc: ProofSystemError): """Handle general proof system errors with 500.""" logger.error(f"Proof system error: {str(exc)}") return JSONResponse( status_code=500, content={ "success": False, "error": "system_error", "message": str(exc) } ) @app.exception_handler(Exception) async def general_exception_handler(request, exc: Exception): """Handle unexpected errors with 500.""" logger.error(f"Unexpected error: {str(exc)}", exc_info=True) return JSONResponse( status_code=500, content={ "success": False, "error": "internal_error", "message": "An unexpected error occurred" } ) # ============================================================================ # HEALTH CHECK # ============================================================================ @app.get("/health") async def health_check(): """ Health check endpoint. Returns system status and feature availability. """ return { "status": "healthy", "version": "0.4.0", "features": { "ocr": settings.OCR_ENABLED, "ai_assistant": settings.AI_ENABLED and orchestrator.ai_sidecar.enabled if orchestrator else False } } # ============================================================================ # PROOF ENDPOINTS # ============================================================================ @app.post("/proof/create/text", response_model=ProofResponse) async def create_proof_from_text(request: TextProofRequest): """ Create a cryptographic proof from text content. This endpoint: 1. Validates text input 2. Generates SHA-256 hash 3. Creates proof with timestamp 4. Stores proof in database 5. Optionally provides AI explanation Returns: Proof ID, hash, timestamp, and optional AI explanation """ if not orchestrator: raise HTTPException(status_code=503, detail="Service not initialized") logger.info(f"Creating proof from text ({len(request.content)} chars)") result = orchestrator.create_proof({ "type": "text", "content": request.content }) if not result["success"]: raise HTTPException( status_code=500, detail=result.get("message", "Failed to create proof") ) proof = result["proof"] return ProofResponse( success=True, proof_id=proof.proof_id, hash=proof.content_hash, timestamp=proof.timestamp, message="Proof created successfully", assistant=result.get("assistant") ) @app.post("/proof/create/file") async def create_proof_from_file( file: UploadFile = File(...), metadata: Optional[str] = Form(None) ): """ Create a cryptographic proof from uploaded file. Supports: - Text files - Images (with optional OCR) - PDFs - Binary files Returns: Proof ID, hash, timestamp, OCR results (if applicable), and optional AI explanation """ if not orchestrator: raise HTTPException(status_code=503, detail="Service not initialized") logger.info(f"Creating proof from file: {file.filename} ({file.content_type})") # Read file content content = await file.read() if len(content) == 0: raise ValidationError("File is empty") result = orchestrator.create_proof({ "type": "file", "content": content, "filename": file.filename }) if not result["success"]: raise HTTPException( status_code=500, detail=result.get("message", "Failed to create proof") ) proof = result["proof"] return { "success": True, "proof_id": proof.proof_id, "hash": proof.content_hash, "timestamp": proof.timestamp, "content_type": proof.content_type, "size": proof.content_size, "ocr_status": proof.ocr_status, "extracted_text": proof.extracted_text[:200] + "..." if proof.extracted_text and len(proof.extracted_text) > 200 else proof.extracted_text, "message": "Proof created successfully", "assistant": result.get("assistant") } @app.get("/proof/{proof_id}") async def get_proof(proof_id: str): """ Retrieve a stored proof by ID. Args: proof_id: Unique proof identifier Returns: Complete proof object with all metadata """ if not orchestrator: raise HTTPException(status_code=503, detail="Service not initialized") logger.info(f"Retrieving proof: {proof_id}") result = orchestrator.get_proof(proof_id) if not result["success"]: raise ProofNotFoundError(f"Proof not found: {proof_id}") proof = result["proof"] return { "success": True, "proof": proof.to_dict(), "message": "Proof retrieved successfully" } @app.post("/proof/verify", response_model=VerificationResponse) async def verify_proof(request: VerifyProofRequest): """ Verify a proof against original content. This endpoint: 1. Retrieves original proof 2. Recomputes hash from provided content 3. Compares hashes 4. Returns verification result 5. Optionally provides AI explanation Returns: Verification status (valid/invalid) with explanation """ if not orchestrator: raise HTTPException(status_code=503, detail="Service not initialized") logger.info(f"Verifying proof: {request.proof_id}") # Convert content to bytes content_bytes = request.content.encode('utf-8') result = orchestrator.verify_proof(request.proof_id, content_bytes) if not result["success"]: raise HTTPException( status_code=500, detail=result.get("message", "Verification failed") ) verification_result = result["verification_result"] return VerificationResponse( success=True, is_valid=verification_result.is_valid, message=verification_result.message, assistant=result.get("assistant") ) @app.post("/proof/verify/file") async def verify_proof_with_file( proof_id: str = Form(...), file: UploadFile = File(...) ): """ Verify a proof by uploading the original file. This endpoint: 1. Accepts proof ID + file upload 2. Retrieves original proof from storage 3. Recomputes hash from uploaded file 4. Compares hashes 5. Returns verification result with original timestamp Args: proof_id: Unique proof identifier file: Original file to verify Returns: - is_valid: True if hashes match, False if tampered - original_hash: Hash from original proof - computed_hash: Hash from uploaded file - original_timestamp: When proof was created - message: Human-readable result """ if not orchestrator: raise HTTPException(status_code=503, detail="Service not initialized") logger.info(f"Verifying proof {proof_id} with file: {file.filename}") # Read uploaded file content = await file.read() if len(content) == 0: raise ValidationError("Uploaded file is empty") # Verify proof result = orchestrator.verify_proof(proof_id, content) if not result["success"]: raise HTTPException( status_code=500, detail=result.get("message", "Verification failed") ) verification_result = result["verification_result"] # Get original proof for timestamp proof_result = orchestrator.get_proof(proof_id) original_proof = proof_result.get("proof") if proof_result["success"] else None return { "success": True, "is_valid": verification_result.is_valid, "proof_id": proof_id, "original_hash": verification_result.original_hash, "computed_hash": verification_result.computed_hash, "original_timestamp": original_proof.timestamp if original_proof else None, "original_filename": original_proof.metadata.get("filename") if original_proof else None, "file_size": len(content), "message": verification_result.message, "assistant": result.get("assistant") } @app.get("/proof/{proof_id}/details") async def get_proof_details(proof_id: str): """ Get detailed information about a proof by its ID. Shows everything: hash, timestamp, file info, OCR results. Args: proof_id: Unique proof identifier Returns: Complete proof details including: - Hash - Original timestamp - File name and size - OCR results (if applicable) - Content type """ if not orchestrator: raise HTTPException(status_code=503, detail="Service not initialized") logger.info(f"Getting details for proof: {proof_id}") result = orchestrator.get_proof(proof_id) if not result["success"]: raise ProofNotFoundError(f"Proof not found: {proof_id}") proof = result["proof"] return { "success": True, "proof_id": proof.proof_id, "hash": proof.content_hash, "hash_algorithm": proof.hash_algorithm, "timestamp": proof.timestamp, "content_type": proof.content_type, "file_size": proof.content_size, "filename": proof.metadata.get("filename"), "ocr_status": proof.ocr_status, "extracted_text": proof.extracted_text, "message": "Proof details retrieved successfully" } @app.post("/proof/verify/hash") async def verify_by_hash( proof_id: str = Form(...), hash_to_verify: str = Form(...) ): """ Verify a proof by comparing hashes directly. Useful when you've computed the hash separately. Args: proof_id: Unique proof identifier hash_to_verify: SHA-256 hash to compare Returns: Verification result with timestamp info """ if not orchestrator: raise HTTPException(status_code=503, detail="Service not initialized") logger.info(f"Verifying proof {proof_id} with hash: {hash_to_verify[:16]}...") # Get original proof result = orchestrator.get_proof(proof_id) if not result["success"]: raise ProofNotFoundError(f"Proof not found: {proof_id}") proof = result["proof"] # Compare hashes is_valid = proof.content_hash.lower() == hash_to_verify.lower() return { "success": True, "is_valid": is_valid, "proof_id": proof_id, "original_hash": proof.content_hash, "provided_hash": hash_to_verify, "original_timestamp": proof.timestamp, "original_filename": proof.metadata.get("filename"), "message": "Hash match: proof is valid" if is_valid else "Hash mismatch: content has been modified" } # ============================================================================ # AI ASSISTANT ENDPOINTS (OPTIONAL) # ============================================================================ @app.post("/assistant/ask") async def ask_assistant(request: AssistantRequest): """ Ask the AI assistant a question about proofs. This is an OPTIONAL feature that provides explanations and guidance. The assistant is non-authoritative and never affects proof validity. Returns: AI-generated explanation (clearly marked as non-authoritative) """ if not orchestrator: raise HTTPException(status_code=503, detail="Service not initialized") if not settings.AI_ENABLED or not orchestrator.ai_sidecar.enabled: return { "success": False, "message": "AI assistant is not enabled. Set AI_ENABLED=true and configure GEMINI_API_KEY." } logger.info(f"AI assistant query: {request.question[:50]}...") result = orchestrator.ask_assistant(request.question, request.proof_id) return result # ============================================================================ # DOCUMENTATION ENDPOINTS # ============================================================================ @app.get("/") async def root(): """ API root with basic information and links. """ return { "name": "Proof-of-Existence API", "version": "0.4.0", "description": "Deterministic cryptographic proof generation and verification", "docs": "/docs", "health": "/health", "endpoints": { "create_text_proof": "POST /proof/create/text", "create_file_proof": "POST /proof/create/file", "get_proof": "GET /proof/{proof_id}", "get_proof_details": "GET /proof/{proof_id}/details", "verify_proof": "POST /proof/verify", "verify_proof_file": "POST /proof/verify/file", "verify_proof_hash": "POST /proof/verify/hash", "ask_assistant": "POST /assistant/ask (optional)" } } # ============================================================================ # DEVELOPMENT SERVER # ============================================================================ if __name__ == "__main__": import uvicorn # Run development server uvicorn.run( "app:app", host="0.0.0.0", port=7860, reload=True, # Enable auto-reload in development log_level="info" )