Proofly / app.py
dipan004's picture
Update app.py
7970833 verified
"""
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"
)