"""FastAPI dependencies and app lifespan. Owns the ToolBox singleton, the compiled LangGraph workflow, and the recursion limit. Construction happens once at app startup; teardown is a no-op because OntoCast tools don't currently expose a close hook. Why a module-level holder instead of `app.state`: The /process route is async and may be entered concurrently. A simple dict on `app.state` works, but pulling the ToolBox through a typed dependency function makes the contract explicit and gives every route the same view of "what tools and what workflow are available right now". """ from __future__ import annotations import asyncio import logging import sys from dataclasses import dataclass from pathlib import Path # Make vendored OntoCast importable. (Same trick as platform/config.py.) _REPO_ROOT = Path(__file__).resolve().parents[2] _VENDORED_ONTOCAST = _REPO_ROOT / "vendored" / "ontocast" if str(_VENDORED_ONTOCAST) not in sys.path: sys.path.insert(0, str(_VENDORED_ONTOCAST)) from langchain_core.runnables import RunnableConfig # noqa: E402 from langgraph.graph.state import CompiledStateGraph # noqa: E402 from ontocast.cli.serve import calculate_recursion_limit # noqa: E402 from ontocast.config import ServerConfig # noqa: E402 from ontocast.stategraph import create_agent_graph # noqa: E402 from ontocast.toolbox import ToolBox # noqa: E402 import importlib platform_config = importlib.import_module("platform.config") logger = logging.getLogger(__name__) @dataclass class AppContext: """Immutable holder for objects that live as long as the FastAPI app.""" settings: "platform_config.PlatformSettings" tools: ToolBox workflow: CompiledStateGraph server_config: ServerConfig recursion_limit: int _context: AppContext | None = None async def initialize_app_context( settings: "platform_config.PlatformSettings | None" = None, *, head_chunks: int | None = None, ) -> AppContext: """Build ToolBox + workflow once. Idempotent on repeat calls.""" global _context if _context is not None: return _context settings = settings or platform_config.load_settings() ontocast_config = platform_config.build_ontocast_config(settings) tools = ToolBox(ontocast_config) # OntoCast's ToolBox.initialize is async; do it here so a request doesn't # have to pay the cost. await tools.initialize() workflow = create_agent_graph(tools) server_config = ontocast_config.server recursion_limit = calculate_recursion_limit(head_chunks, server_config) _context = AppContext( settings=settings, tools=tools, workflow=workflow, server_config=server_config, recursion_limit=recursion_limit, ) logger.info( "App context initialized " f"(phase={int(settings.phase)}, backend={settings.storage_backend}, " f"working_dir={settings.working_directory})" ) return _context def get_app_context() -> AppContext: """FastAPI dependency. Raises clearly if the lifespan never ran.""" if _context is None: raise RuntimeError( "AppContext not initialized — the app lifespan must run " "`initialize_app_context()` before serving requests." ) return _context def reset_app_context_for_testing() -> None: """Test-only: drop the cached context so each test can rebuild it.""" global _context _context = None __all__ = [ "AppContext", "RunnableConfig", "get_app_context", "initialize_app_context", "reset_app_context_for_testing", ]