"""FastAPI application factory and configuration.""" import os # Opt-in to future behavior for python-telegram-bot os.environ["PTB_TIMEDELTA"] = "1" import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.responses import JSONResponse from .routes import router from .dependencies import cleanup_provider from providers.exceptions import ProviderError from config.settings import get_settings # Configure logging logging.basicConfig( level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s", handlers=[logging.FileHandler("server.log", encoding="utf-8", mode="w")], ) logger = logging.getLogger(__name__) # Suppress noisy uvicorn logs logging.getLogger("uvicorn").setLevel(logging.WARNING) logging.getLogger("uvicorn.access").setLevel(logging.WARNING) logging.getLogger("uvicorn.error").setLevel(logging.WARNING) @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan manager.""" settings = get_settings() logger.info("Starting Claude Code Proxy...") # Initialize messaging platform if configured messaging_platform = None message_handler = None cli_manager = None try: if settings.telegram_bot_token: from messaging.telegram import TelegramPlatform from messaging.handler import ClaudeMessageHandler from messaging.session import SessionStore from cli.manager import CLISessionManager # Setup workspace - CLI runs in allowed_dir if set (e.g. project root) workspace = ( os.path.abspath(settings.allowed_dir) if settings.allowed_dir else os.getcwd() ) os.makedirs(workspace, exist_ok=True) # Session data (Telegram session, app sessions) stored in .agent_workspace data_path = os.path.abspath(settings.claude_workspace) os.makedirs(data_path, exist_ok=True) allowed_dirs = [workspace] if settings.allowed_dir else [] cli_manager = CLISessionManager( workspace_path=workspace, api_url="http://localhost:8082/v1", allowed_dirs=allowed_dirs, max_sessions=settings.max_cli_sessions, ) # Initialize session store session_store = SessionStore( storage_path=os.path.join(data_path, "sessions.json") ) # Create Telegram platform messaging_platform = TelegramPlatform( bot_token=settings.telegram_bot_token, allowed_user_id=settings.allowed_telegram_user_id, ) # Create and register message handler message_handler = ClaudeMessageHandler( platform=messaging_platform, cli_manager=cli_manager, session_store=session_store, ) # Restore tree state if available if session_store._trees: logger.info( f"Restoring {len(session_store._trees)} conversation trees..." ) from messaging.tree_queue import TreeQueueManager message_handler.tree_queue = TreeQueueManager.from_dict( { "trees": session_store._trees, "node_to_tree": session_store._node_to_tree, } ) # Reconcile restored state - anything PENDING/IN_PROGRESS is lost across restart if message_handler.tree_queue.cleanup_stale_nodes() > 0: # Sync back and save session_store._trees = message_handler.tree_queue.to_dict()["trees"] session_store._node_to_tree = message_handler.tree_queue.to_dict()[ "node_to_tree" ] session_store._save() # Wire up the handler messaging_platform.on_message(message_handler.handle_message) # Start the platform await messaging_platform.start() logger.info("Telegram platform started with message handler") except ImportError as e: logger.warning(f"Messaging module import error: {e}") except Exception as e: logger.error(f"Failed to start messaging platform: {e}") import traceback logger.error(traceback.format_exc()) # Store in app state for access in routes app.state.messaging_platform = messaging_platform app.state.message_handler = message_handler app.state.cli_manager = cli_manager yield # Cleanup if messaging_platform: await messaging_platform.stop() if cli_manager: await cli_manager.stop_all() await cleanup_provider() logger.info("Server shutting down...") def create_app() -> FastAPI: """Create and configure the FastAPI application.""" app = FastAPI( title="Claude Code Proxy", version="2.0.0", lifespan=lifespan, ) # Register routes app.include_router(router) # Exception handlers @app.exception_handler(ProviderError) async def provider_error_handler(request: Request, exc: ProviderError): """Handle provider-specific errors and return Anthropic format.""" logger.error(f"Provider Error: {exc.error_type} - {exc.message}") return JSONResponse( status_code=exc.status_code, content=exc.to_anthropic_format(), ) @app.exception_handler(Exception) async def general_error_handler(request: Request, exc: Exception): """Handle general errors and return Anthropic format.""" logger.error(f"General Error: {str(exc)}") import traceback logger.error(traceback.format_exc()) return JSONResponse( status_code=500, content={ "type": "error", "error": { "type": "api_error", "message": "An unexpected error occurred.", }, }, ) return app # Default app instance for uvicorn app = create_app()