GitHub Actions
Deploy from GitHub Actions (2026-03-16 04:38 UTC)
4413b85
# app/main.py
# pylint: disable=unused-import
import uuid
import time
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
# NOTE: Using absolute imports from project root (app.*) instead of relative imports
# This ensures imports work consistently in both IDE and Docker environments
from app.middleware.tenant_auth import TenantAuthMiddleware
from app.utils.config import get_config, get_logger, get_version
from app.models.nlp_engine import NLPEngine
from app.models.conversation_manager import ConversationManager
from app.models.database import (
SessionLocal,
init_database,
health_check as db_health_check,
)
from app.models.smart_models import UserPreference, UserInsight, ConversationTopic
from app.data.training_data import TRAINING_DATA
from app.routers import knowledge_base
from app.utils.seeding import seed_demo_tenant
# Load configuration (this sets up logging automatically)
config = get_config()
logger = get_logger(__name__)
# Train the mode on startup
@asynccontextmanager
async def lifespan(fastapi_app: FastAPI): # pylint: disable=unused-argument
"""Application lifespan manager for startup and shutdown events"""
# Startup
logger.info("Starting application..")
logger.info("CORS origins configured: %s", config.middleware["cors_origins"])
# Initialize database
try:
init_database()
with SessionLocal() as session:
seed_demo_tenant(session)
if db_health_check():
logger.info("Database connection established successfully")
else:
logger.warning("Database health check failed, but continuing...")
except Exception as e: # pylint: disable=broad-exception-caught
logger.error("Database initialization failed: %s: %s", type(e).__name__, str(e))
logger.warning("Continuing without database - some features may not work...")
# Train NLP model
nlp_engine.train_intent_classifier(TRAINING_DATA)
logger.info("NLP model training completed")
logger.info("Application started successfully in %s mode", config.env.name)
yield # App runs here
# Shutdown (if needed)
logger.info("Application shutting down...")
app = FastAPI(title=config.api.title, debug=config.api.debug, lifespan=lifespan)
app.include_router(knowledge_base.router)
# Enable CORS
app.add_middleware(
CORSMiddleware,
allow_origins=config.middleware["cors_origins"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Add Tenant Auth
app.add_middleware(TenantAuthMiddleware)
# Initialize components
nlp_engine = NLPEngine(config)
conversation_manager = ConversationManager(config)
# Request logging middleware
if config.middleware["enable_request_logging"]:
@app.middleware("http")
async def log_requests(request, call_next):
logger.info("Request: %s %s", request.method, request.url)
response = await call_next(request)
logger.info("Response: %s", response.status_code)
return response
class ChatRequest(BaseModel):
message: str
session_id: str | None = None
class ChatResponse(BaseModel):
thinking: str | None = None
response: str
session_id: str
intent: str
confidence: float
entities: dict
debug_info: dict | None = None
@app.get("/health")
async def health_check():
logger.debug("Health check endpoint accessed")
# Check database health
db_status = "healthy" if db_health_check() else "unhealthy"
return {
"status": "healthy",
"environment": config.env.name,
"version": get_version(), # Read from pyproject.toml
"database": db_status,
"components": {
"nlp_engine": "ready",
"conversation_manager": "ready",
"database": db_status,
},
}
@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
logger.debug("Chat request received: %s ...", request.message[:50])
start_time = time.time()
try:
# Generate session ID if not provided
session_id = request.session_id or str(uuid.uuid4())
# Detect language
language = nlp_engine.detect_language(request.message)
# Process message
intent, confidence = nlp_engine.classify_intent(request.message)
entities = nlp_engine.extract_entities(request.message, language)
# Convert NumPy types to Python types
intent = str(intent)
confidence = float(confidence)
logger.debug(
"Processed - Language: %s, Intent: %s, Confidence: %.2f",
language,
intent,
confidence,
)
# Apply confidence threshold
if confidence <= config.nlp["confidence_threshold"]:
logger.info(
"Low confidence (%.2f}) for intent %s, using fallback",
confidence,
intent,
)
intent = "low_confidence"
# Generate response (now with context awareness)
thinking_block, response = conversation_manager.get_response(
intent, language, request.message, session_id, entities
)
# Calculate response time
response_time_ms = int((time.time() - start_time) * 1000)
# Save conversation (now to database)
conversation_manager.save_conversation(
session_id,
request.message,
response,
intent,
confidence,
entities,
language,
response_time_ms,
)
# Prepare response
chat_response = ChatResponse(
thinking=thinking_block,
response=response,
session_id=session_id,
intent=intent,
confidence=confidence,
entities=entities,
)
# Add debug info if enabled
if config.nlp["enable_debug"]:
chat_response.debug_info = {
"language": language,
"original_confidence": confidence,
"threshold_applied": confidence < config.nlp["confidence_threshold"],
"environment": config.env.name,
"response_time_ms": response_time_ms,
"database_enabled": db_health_check(),
}
logger.info("Chat response generated successfully for session %s", session_id)
return chat_response
except Exception as e:
logger.error("Error processing chat request: %s", str(e), exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error") from e
@app.get("/analytics/{session_id}")
async def get_conversation_history(session_id: str):
logger.debug("Analytics requested for session: %s", session_id)
try:
history = conversation_manager.get_conversation_history(session_id)
if history:
logger.info("Conversation history found for session %s", session_id)
return {
"session_id": session_id,
"message_count": len(history),
"messages": history,
}
else:
logger.warning("No conversation history found for session %s", session_id)
raise HTTPException(status_code=404, detail="Session not found")
except HTTPException:
raise
except Exception as e:
logger.error("Error retrieving conversation history: %s", str(e), exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error") from e
@app.get("/analytics/{session_id}/stats")
async def get_user_stats(session_id: str):
logger.debug("User stats requested for session: %s", session_id)
try:
stats = conversation_manager.get_user_stats(session_id)
if stats:
logger.info("User stats found for session %s", session_id)
return stats
else:
logger.warning("No user stats found for session %s", session_id)
raise HTTPException(status_code=404, detail="Session not found")
except HTTPException:
raise
except Exception as e:
logger.error("Error retrieving user stats: %s", str(e), exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error") from e
# Serve frontend static files (HF Spaces has no separate nginx)
# Must be mounted LAST so API routes above take precedence
_frontend_dir = Path(__file__).parent.parent / "frontend"
if _frontend_dir.exists():
app.mount(
"/", StaticFiles(directory=str(_frontend_dir), html=True), name="frontend"
)