Files
AI/ontology_platform/platform/api/deps.py

117 lines
3.5 KiB
Python
Raw Normal View History

2026-05-13 19:57:34 +09:00
"""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",
]