2026-05-13 19:57:34 +09:00
|
|
|
|
"""FastAPI application entry point — replaces OntoCast's Robyn server.
|
|
|
|
|
|
|
|
|
|
|
|
Endpoint semantics mirror the original Robyn server (OntoCast 분석 §13.1–§13.4)
|
|
|
|
|
|
with three deliberate differences:
|
|
|
|
|
|
|
|
|
|
|
|
1. `/flush` requires a confirmation token to prevent accidental destruction of
|
|
|
|
|
|
the triple store. OntoCast 분석 §21.1 (#6) flagged the unauth'd /flush as a
|
|
|
|
|
|
risk; we honor that.
|
|
|
|
|
|
2. `/process` only accepts ``application/json`` and ``multipart/form-data``,
|
|
|
|
|
|
matching the Robyn version, but error responses now use FastAPI's normal
|
|
|
|
|
|
status-code semantics rather than always-200-with-error-body.
|
|
|
|
|
|
3. The app no longer hard-codes ``version="0.1.1"``. The version is read from
|
|
|
|
|
|
the OntoCast vendored package so /health and /info stay in sync with the
|
|
|
|
|
|
vendored copy (OntoCast 분석 §21.1 #2).
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import importlib
|
|
|
|
|
|
import importlib.metadata as importlib_metadata
|
|
|
|
|
|
import logging
|
|
|
|
|
|
import sys
|
|
|
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from typing import Annotated, Any, AsyncIterator
|
|
|
|
|
|
|
|
|
|
|
|
import click
|
|
|
|
|
|
import uvicorn
|
|
|
|
|
|
from fastapi import Depends, FastAPI, File, Form, HTTPException, Query, Request, UploadFile
|
|
|
|
|
|
from fastapi.responses import JSONResponse
|
|
|
|
|
|
|
|
|
|
|
|
# Vendored OntoCast on sys.path before any ontocast import.
|
|
|
|
|
|
_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 ontocast.onto.enum import RenderMode # noqa: E402
|
|
|
|
|
|
from ontocast.onto.state import AgentState # noqa: E402
|
|
|
|
|
|
|
2026-05-14 09:05:24 +09:00
|
|
|
|
from ont_platform.api.deps import ( # noqa: E402
|
2026-05-13 19:57:34 +09:00
|
|
|
|
AppContext,
|
|
|
|
|
|
RunnableConfig,
|
|
|
|
|
|
get_app_context,
|
|
|
|
|
|
initialize_app_context,
|
|
|
|
|
|
)
|
2026-05-20 13:21:08 +09:00
|
|
|
|
from ont_platform.api.product_backend import include_product_backend # noqa: E402
|
2026-05-13 19:57:34 +09:00
|
|
|
|
|
2026-05-14 09:05:24 +09:00
|
|
|
|
platform_config = importlib.import_module("ont_platform.config")
|
2026-05-13 19:57:34 +09:00
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
2026-05-22 00:22:03 +09:00
|
|
|
|
_startup_error: str | None = None
|
2026-05-13 19:57:34 +09:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_ontocast_version() -> str:
|
|
|
|
|
|
"""Best-effort lookup of the vendored OntoCast version.
|
|
|
|
|
|
|
|
|
|
|
|
Reading the vendored ``pyproject.toml`` directly avoids requiring the
|
|
|
|
|
|
package to be installed into the active environment.
|
|
|
|
|
|
"""
|
|
|
|
|
|
pyproject = _VENDORED_ONTOCAST / "pyproject.toml"
|
|
|
|
|
|
try:
|
|
|
|
|
|
text = pyproject.read_text(encoding="utf-8")
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
return "unknown"
|
|
|
|
|
|
for line in text.splitlines():
|
|
|
|
|
|
stripped = line.strip()
|
|
|
|
|
|
if stripped.startswith("version") and "=" in stripped:
|
|
|
|
|
|
_, _, value = stripped.partition("=")
|
|
|
|
|
|
return value.strip().strip('"').strip("'")
|
|
|
|
|
|
# Fall back to package metadata if it happens to be installed.
|
|
|
|
|
|
try:
|
|
|
|
|
|
return importlib_metadata.version("ontocast")
|
|
|
|
|
|
except importlib_metadata.PackageNotFoundError:
|
|
|
|
|
|
return "unknown"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ONTOCAST_VERSION = _resolve_ontocast_version()
|
|
|
|
|
|
PLATFORM_VERSION = "0.0.1"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-19 20:31:52 +09:00
|
|
|
|
def _include_phase_routers(app: FastAPI) -> None:
|
|
|
|
|
|
"""Attach routers whose dependencies are enabled for the configured phase."""
|
|
|
|
|
|
settings = platform_config.load_settings()
|
2026-05-20 13:21:08 +09:00
|
|
|
|
enabled_routes: list[str] = ["product-backend"]
|
2026-05-19 20:31:52 +09:00
|
|
|
|
|
|
|
|
|
|
if settings.phase >= platform_config.Phase.TRAFILATURA:
|
|
|
|
|
|
try:
|
|
|
|
|
|
from ont_platform.api.routes import get_extraction_router
|
|
|
|
|
|
except ImportError as exc:
|
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
|
"Phase 1 route loading requires the Phase 1 extraction dependencies. "
|
|
|
|
|
|
"Install the Phase 1 dependency set or run with PHASE=0."
|
|
|
|
|
|
) from exc
|
|
|
|
|
|
app.include_router(get_extraction_router())
|
|
|
|
|
|
enabled_routes.append("extraction")
|
|
|
|
|
|
|
|
|
|
|
|
if settings.phase >= platform_config.Phase.CANDIDATE_REVIEW:
|
|
|
|
|
|
from ont_platform.api.routes import get_review_router
|
|
|
|
|
|
|
|
|
|
|
|
app.include_router(get_review_router())
|
|
|
|
|
|
enabled_routes.append("review")
|
|
|
|
|
|
|
|
|
|
|
|
if settings.phase >= platform_config.Phase.CRAWL4AI:
|
|
|
|
|
|
from ont_platform.api.routes import get_crawl_router
|
|
|
|
|
|
|
|
|
|
|
|
app.include_router(get_crawl_router())
|
|
|
|
|
|
enabled_routes.append("crawl")
|
|
|
|
|
|
|
|
|
|
|
|
if settings.phase >= platform_config.Phase.NEO4J_GRAPHRAG:
|
|
|
|
|
|
from ont_platform.api.routes import get_graph_router
|
|
|
|
|
|
|
|
|
|
|
|
app.include_router(get_graph_router())
|
|
|
|
|
|
enabled_routes.append("graph")
|
|
|
|
|
|
|
|
|
|
|
|
if settings.phase >= platform_config.Phase.MULTI_AGENT:
|
|
|
|
|
|
from ont_platform.api.routes import get_maintenance_router
|
|
|
|
|
|
|
|
|
|
|
|
app.include_router(get_maintenance_router())
|
|
|
|
|
|
enabled_routes.append("maintenance")
|
|
|
|
|
|
|
|
|
|
|
|
app.state.enabled_phase_routes = enabled_routes
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-13 19:57:34 +09:00
|
|
|
|
@asynccontextmanager
|
|
|
|
|
|
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
|
|
|
|
"""FastAPI lifespan: build ToolBox + workflow once on startup."""
|
2026-05-22 00:22:03 +09:00
|
|
|
|
global _startup_error
|
2026-05-13 19:57:34 +09:00
|
|
|
|
settings = platform_config.load_settings()
|
2026-05-22 00:22:03 +09:00
|
|
|
|
try:
|
|
|
|
|
|
await initialize_app_context(settings)
|
|
|
|
|
|
_startup_error = None
|
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
|
_startup_error = str(exc)
|
|
|
|
|
|
logger.exception("App context initialization failed; product backend will remain available")
|
2026-05-13 19:57:34 +09:00
|
|
|
|
try:
|
|
|
|
|
|
yield
|
|
|
|
|
|
finally:
|
|
|
|
|
|
# No explicit teardown is needed today. When Phase 2/4 adds DB/HTTP
|
|
|
|
|
|
# pools they'll close here.
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_app() -> FastAPI:
|
|
|
|
|
|
app = FastAPI(
|
|
|
|
|
|
title="Ontology Platform",
|
|
|
|
|
|
description=(
|
|
|
|
|
|
"Universal ontology construction platform. Phase 0 wraps the "
|
|
|
|
|
|
"OntoCast core engine; later phases add Trafilatura, Crawl4AI, "
|
|
|
|
|
|
"Guardrails, and Neo4j GraphRAG. See docs/통합설계서.md."
|
|
|
|
|
|
),
|
|
|
|
|
|
version=PLATFORM_VERSION,
|
|
|
|
|
|
lifespan=lifespan,
|
|
|
|
|
|
)
|
2026-05-20 13:21:08 +09:00
|
|
|
|
include_product_backend(app)
|
2026-05-13 19:57:34 +09:00
|
|
|
|
|
|
|
|
|
|
# ─── /health ──────────────────────────────────────────────────────
|
|
|
|
|
|
@app.get("/health", tags=["meta"])
|
2026-05-22 20:16:28 +09:00
|
|
|
|
async def health(request: Request) -> JSONResponse:
|
2026-05-22 00:22:03 +09:00
|
|
|
|
"""Liveness check for the HTTP service and optional LLM readiness."""
|
|
|
|
|
|
settings = platform_config.load_settings()
|
|
|
|
|
|
if _startup_error:
|
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
|
status_code=503,
|
|
|
|
|
|
content={
|
|
|
|
|
|
"status": "degraded",
|
|
|
|
|
|
"error": _startup_error,
|
|
|
|
|
|
"platform_version": PLATFORM_VERSION,
|
|
|
|
|
|
"ontocast_version": ONTOCAST_VERSION,
|
|
|
|
|
|
"phase": int(settings.phase),
|
|
|
|
|
|
"storage_backend": settings.storage_backend,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-22 20:16:28 +09:00
|
|
|
|
ctx = _request_app_context(request)
|
2026-05-13 19:57:34 +09:00
|
|
|
|
if ctx.tools.llm is None:
|
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
|
status_code=503,
|
|
|
|
|
|
content={"status": "unhealthy", "error": "LLM not initialized"},
|
|
|
|
|
|
)
|
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
|
status_code=200,
|
|
|
|
|
|
content={
|
|
|
|
|
|
"status": "healthy",
|
|
|
|
|
|
"platform_version": PLATFORM_VERSION,
|
|
|
|
|
|
"ontocast_version": ONTOCAST_VERSION,
|
|
|
|
|
|
"llm_provider": ctx.tools.llm_provider,
|
|
|
|
|
|
"phase": int(ctx.settings.phase),
|
|
|
|
|
|
"storage_backend": ctx.settings.storage_backend,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ─── /info ────────────────────────────────────────────────────────
|
|
|
|
|
|
@app.get("/info", tags=["meta"])
|
2026-05-22 20:16:28 +09:00
|
|
|
|
async def info(request: Request) -> JSONResponse:
|
2026-05-13 19:57:34 +09:00
|
|
|
|
"""Service-level capabilities (mirrors OntoCast /info semantics)."""
|
2026-05-22 00:22:03 +09:00
|
|
|
|
settings = platform_config.load_settings()
|
|
|
|
|
|
phase = int(settings.phase)
|
|
|
|
|
|
storage_backend = settings.storage_backend
|
|
|
|
|
|
if not _startup_error:
|
2026-05-22 20:16:28 +09:00
|
|
|
|
ctx = _request_app_context(request)
|
2026-05-22 00:22:03 +09:00
|
|
|
|
phase = int(ctx.settings.phase)
|
|
|
|
|
|
storage_backend = ctx.settings.storage_backend
|
|
|
|
|
|
|
2026-05-13 19:57:34 +09:00
|
|
|
|
return JSONResponse(
|
|
|
|
|
|
status_code=200,
|
|
|
|
|
|
content={
|
|
|
|
|
|
"name": "ontology-platform",
|
|
|
|
|
|
"platform_version": PLATFORM_VERSION,
|
|
|
|
|
|
"ontocast_version": ONTOCAST_VERSION,
|
|
|
|
|
|
"description": (
|
|
|
|
|
|
"Universal ontology construction platform built on the "
|
|
|
|
|
|
"OntoCast agentic core."
|
|
|
|
|
|
),
|
|
|
|
|
|
"capabilities": ["text-to-triples", "ontology-extraction"],
|
|
|
|
|
|
"input_types": ["text", "json", "pdf", "markdown"],
|
|
|
|
|
|
"output_types": ["turtle", "json"],
|
2026-05-22 00:22:03 +09:00
|
|
|
|
"phase": phase,
|
|
|
|
|
|
"storage_backend": storage_backend,
|
|
|
|
|
|
"startup_error": _startup_error,
|
2026-05-13 19:57:34 +09:00
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ─── /flush ───────────────────────────────────────────────────────
|
|
|
|
|
|
@app.post("/flush", tags=["admin"])
|
|
|
|
|
|
async def flush(
|
|
|
|
|
|
ctx: Annotated[AppContext, Depends(get_app_context)],
|
|
|
|
|
|
confirm: Annotated[
|
|
|
|
|
|
str | None,
|
|
|
|
|
|
Query(
|
|
|
|
|
|
description=(
|
|
|
|
|
|
"Must equal 'YES-I-WANT-TO-DELETE-EVERYTHING'. Guards "
|
|
|
|
|
|
"against accidental triple-store wipe (OntoCast 분석 §21.1 #6)."
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
] = None,
|
|
|
|
|
|
dataset: Annotated[
|
|
|
|
|
|
str | None,
|
|
|
|
|
|
Query(description="Fuseki only — specific dataset to clean."),
|
|
|
|
|
|
] = None,
|
|
|
|
|
|
) -> JSONResponse:
|
|
|
|
|
|
if confirm != "YES-I-WANT-TO-DELETE-EVERYTHING":
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=400,
|
|
|
|
|
|
detail=(
|
|
|
|
|
|
"/flush requires confirm=YES-I-WANT-TO-DELETE-EVERYTHING "
|
|
|
|
|
|
"to guard against accidental data loss."
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
if ctx.tools.triple_store_manager is None:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=400,
|
|
|
|
|
|
detail="No triple store manager configured.",
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
await ctx.tools.triple_store_manager.clean(dataset=dataset)
|
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
|
logger.exception("Error flushing triple store")
|
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
|
status_code=500,
|
|
|
|
|
|
content={
|
|
|
|
|
|
"status": "error",
|
|
|
|
|
|
"error": str(exc),
|
|
|
|
|
|
"error_type": type(exc).__name__,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
|
status_code=200,
|
|
|
|
|
|
content={
|
|
|
|
|
|
"status": "success",
|
|
|
|
|
|
"message": (
|
|
|
|
|
|
f"Triple store flushed (dataset={dataset!r})"
|
|
|
|
|
|
if dataset
|
|
|
|
|
|
else "Triple store flushed (all datasets)"
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ─── /process ─────────────────────────────────────────────────────
|
|
|
|
|
|
@app.post("/process", tags=["pipeline"])
|
|
|
|
|
|
async def process(
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
ctx: Annotated[AppContext, Depends(get_app_context)],
|
|
|
|
|
|
# Query params — these mirror the Robyn semantics.
|
|
|
|
|
|
dataset: Annotated[str | None, Query()] = None,
|
|
|
|
|
|
render_mode: Annotated[str | None, Query()] = None,
|
|
|
|
|
|
ontology_user_instruction: Annotated[str, Query()] = "",
|
|
|
|
|
|
facts_user_instruction: Annotated[str, Query()] = "",
|
|
|
|
|
|
# Multipart fields — optional; when used, JSON body is rejected.
|
|
|
|
|
|
file: Annotated[UploadFile | None, File()] = None,
|
|
|
|
|
|
form_ontology_user_instruction: Annotated[str | None, Form(alias="ontology_user_instruction")] = None,
|
|
|
|
|
|
form_facts_user_instruction: Annotated[str | None, Form(alias="facts_user_instruction")] = None,
|
|
|
|
|
|
) -> JSONResponse:
|
|
|
|
|
|
"""Run the full OntoCast workflow over a single document.
|
|
|
|
|
|
|
|
|
|
|
|
Accepts either ``application/json`` (body = the JSON envelope OntoCast
|
|
|
|
|
|
already understands) or ``multipart/form-data`` (one ``file`` field).
|
|
|
|
|
|
Returns the produced ontology + facts Turtle, plus pipeline metadata
|
|
|
|
|
|
and the budget tracker snapshot.
|
|
|
|
|
|
"""
|
|
|
|
|
|
content_type = (request.headers.get("content-type") or "").lower()
|
|
|
|
|
|
files: dict[str, bytes]
|
|
|
|
|
|
|
|
|
|
|
|
if content_type.startswith("application/json"):
|
|
|
|
|
|
if file is not None:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=400,
|
|
|
|
|
|
detail="Use either JSON body or multipart, not both.",
|
|
|
|
|
|
)
|
|
|
|
|
|
body = await request.body()
|
|
|
|
|
|
if not body:
|
|
|
|
|
|
raise HTTPException(status_code=400, detail="Empty JSON body.")
|
|
|
|
|
|
files = {"input.json": body}
|
|
|
|
|
|
|
|
|
|
|
|
elif content_type.startswith("multipart/form-data"):
|
|
|
|
|
|
if file is None:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=400,
|
|
|
|
|
|
detail="multipart/form-data requires a 'file' field.",
|
|
|
|
|
|
)
|
|
|
|
|
|
filename = file.filename or "upload.bin"
|
|
|
|
|
|
content = await file.read()
|
|
|
|
|
|
files = {filename: content}
|
|
|
|
|
|
|
|
|
|
|
|
# Form fields override query-string instructions, matching the
|
|
|
|
|
|
# original Robyn precedence.
|
|
|
|
|
|
if form_ontology_user_instruction:
|
|
|
|
|
|
ontology_user_instruction = form_ontology_user_instruction
|
|
|
|
|
|
if form_facts_user_instruction:
|
|
|
|
|
|
facts_user_instruction = form_facts_user_instruction
|
|
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=415,
|
|
|
|
|
|
detail=(
|
|
|
|
|
|
"Unsupported content type. Use application/json or "
|
|
|
|
|
|
"multipart/form-data."
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Dataset switch (mostly a no-op on filesystem backend).
|
|
|
|
|
|
if dataset:
|
|
|
|
|
|
await ctx.tools.update_dataset(dataset)
|
|
|
|
|
|
|
|
|
|
|
|
# Parse render mode.
|
|
|
|
|
|
try:
|
|
|
|
|
|
render_mode_value = (
|
|
|
|
|
|
RenderMode(render_mode.lower().strip())
|
|
|
|
|
|
if render_mode
|
|
|
|
|
|
else ctx.server_config.render_mode
|
|
|
|
|
|
)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
"Invalid render_mode %r; using default %r",
|
|
|
|
|
|
render_mode,
|
|
|
|
|
|
ctx.server_config.render_mode.value,
|
|
|
|
|
|
)
|
|
|
|
|
|
render_mode_value = ctx.server_config.render_mode
|
|
|
|
|
|
|
|
|
|
|
|
initial_state = AgentState(
|
|
|
|
|
|
files=files,
|
|
|
|
|
|
max_visits=ctx.server_config.max_visits_per_node,
|
|
|
|
|
|
render_mode=render_mode_value,
|
|
|
|
|
|
ontology_max_triples=ctx.server_config.ontology_max_triples,
|
|
|
|
|
|
dataset=dataset,
|
|
|
|
|
|
ontology_user_instruction=ontology_user_instruction,
|
|
|
|
|
|
facts_user_instruction=facts_user_instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
workflow_state: dict[str, Any] | None = None
|
|
|
|
|
|
try:
|
|
|
|
|
|
async for chunk in ctx.workflow.astream(
|
|
|
|
|
|
initial_state,
|
|
|
|
|
|
stream_mode="values",
|
|
|
|
|
|
config=RunnableConfig(recursion_limit=ctx.recursion_limit),
|
|
|
|
|
|
):
|
|
|
|
|
|
workflow_state = chunk
|
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
|
logger.exception("Workflow execution failed")
|
|
|
|
|
|
error_details = None
|
|
|
|
|
|
if workflow_state:
|
|
|
|
|
|
error_details = {
|
|
|
|
|
|
"stage": workflow_state.get("failure_stage", "unknown"),
|
|
|
|
|
|
"reason": workflow_state.get("failure_reason", "unknown"),
|
|
|
|
|
|
}
|
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
|
status_code=500,
|
|
|
|
|
|
content={
|
|
|
|
|
|
"status": "error",
|
|
|
|
|
|
"error": str(exc),
|
|
|
|
|
|
"error_type": type(exc).__name__,
|
|
|
|
|
|
"error_details": error_details,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if workflow_state is None:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=500,
|
|
|
|
|
|
detail="Workflow did not return a valid state.",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Budget snapshot.
|
|
|
|
|
|
budget_tracker_data: dict[str, Any] = {}
|
|
|
|
|
|
if workflow_state.get("budget_tracker"):
|
|
|
|
|
|
budget_tracker_data = workflow_state["budget_tracker"].model_dump()
|
|
|
|
|
|
|
|
|
|
|
|
total_units = len(workflow_state.get("content_units", []))
|
|
|
|
|
|
rm = workflow_state.get("render_mode")
|
|
|
|
|
|
render_facts_enabled = rm in (
|
|
|
|
|
|
RenderMode.FACTS,
|
|
|
|
|
|
RenderMode.ONTOLOGY_AND_FACTS,
|
|
|
|
|
|
getattr(RenderMode.FACTS, "value", None),
|
|
|
|
|
|
getattr(RenderMode.ONTOLOGY_AND_FACTS, "value", None),
|
|
|
|
|
|
)
|
|
|
|
|
|
processed_units = (
|
|
|
|
|
|
len(workflow_state.get("parallel_facts_units", []))
|
|
|
|
|
|
if render_facts_enabled
|
|
|
|
|
|
else total_units
|
|
|
|
|
|
)
|
|
|
|
|
|
chunks_remaining = max(total_units - processed_units, 0)
|
|
|
|
|
|
|
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
|
status_code=200,
|
|
|
|
|
|
content={
|
|
|
|
|
|
"status": "success",
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"ontology": (
|
|
|
|
|
|
workflow_state["current_ontology"].graph.serialize(format="turtle")
|
|
|
|
|
|
if workflow_state.get("current_ontology")
|
|
|
|
|
|
else ""
|
|
|
|
|
|
),
|
|
|
|
|
|
"facts": (
|
|
|
|
|
|
workflow_state["aggregated_facts"].serialize(format="turtle")
|
|
|
|
|
|
if workflow_state.get("aggregated_facts")
|
|
|
|
|
|
else ""
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
"metadata": {
|
|
|
|
|
|
"status": str(workflow_state.get("status", "unknown")),
|
|
|
|
|
|
"chunks_processed": processed_units,
|
|
|
|
|
|
"chunks_remaining": chunks_remaining,
|
|
|
|
|
|
"budget": budget_tracker_data,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-19 20:31:52 +09:00
|
|
|
|
_include_phase_routers(app)
|
2026-05-14 10:35:31 +09:00
|
|
|
|
|
2026-05-13 19:57:34 +09:00
|
|
|
|
return app
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-22 20:16:28 +09:00
|
|
|
|
def _request_app_context(request: Request) -> AppContext:
|
|
|
|
|
|
override = request.app.dependency_overrides.get(get_app_context)
|
|
|
|
|
|
if override is not None:
|
|
|
|
|
|
return override()
|
|
|
|
|
|
return get_app_context()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-13 19:57:34 +09:00
|
|
|
|
# Top-level instance for `uvicorn platform.api.main:app`.
|
|
|
|
|
|
app = create_app()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@click.command()
|
|
|
|
|
|
@click.option("--host", default="0.0.0.0", show_default=True)
|
|
|
|
|
|
@click.option("--port", default=8000, show_default=True, type=int)
|
|
|
|
|
|
@click.option("--reload", is_flag=True, default=False)
|
|
|
|
|
|
def cli(host: str, port: int, reload: bool) -> None: # noqa: FBT001
|
|
|
|
|
|
"""Console entry point: ``ontology-platform`` (see pyproject.toml)."""
|
2026-05-14 09:05:24 +09:00
|
|
|
|
uvicorn.run("ont_platform.api.main:app", host=host, port=port, reload=reload)
|
2026-05-13 19:57:34 +09:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
cli()
|