Phase 1: router skeleton + Qdrant read path - FastAPI app with /health, /search/generic, /ingest endpoints - Qdrant reader (hybrid dense+sparse w/ RRF, plus payload scroll) - BGE-M3 TEI client for query-side embedding - memory-api client proxying writes to /memory/store (m2-memory untouched) - Pydantic Fact + Exemplar payloads, discriminated by `kind` - Dockerfile + docker-compose joining the coolify Docker network Phase 2: authoritative-source ingest pipeline - PDF text extraction + paragraph-aware (§ N) and size chunkers - Loaders for bnatschg, mhbasp, biotopwertliste, state-<slug> - CLI: artenschutz-ingest --source <name> [--states ...] [--dry-run] - Helper script to fetch BNatSchG from gesetze-im-internet.de - End-to-end smoke script (scripts/smoke.sh) for Coolify validation 22/22 unit tests pass. Real-world dry-runs verified against mhbasp Anhang 4, biotopwertlisteNEU and Berlin Kartierstandards PDFs from GST-DATA. See COOLIFY-DEPLOY.md for staging deploy + smoke procedure.
93 lines
2.8 KiB
Python
93 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI
|
|
|
|
from .clients.bge import BGEClient
|
|
from .clients.memory_api import MemoryApiClient
|
|
from .clients.qdrant import QdrantReader
|
|
from .config import get_settings
|
|
from .models import HealthResponse
|
|
from .routes import ingest as ingest_routes
|
|
from .routes import search as search_routes
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
settings = get_settings()
|
|
logging.basicConfig(
|
|
level=getattr(logging, settings.log_level.upper(), logging.INFO),
|
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
logger.info("Starting artenschutz-router")
|
|
logger.info(" Qdrant: %s (collection: %s)", settings.qdrant_url, settings.qdrant_collection)
|
|
logger.info(" BGE TEI: %s", settings.bge_tei_url)
|
|
logger.info(" memory-api: %s", settings.memory_api_url)
|
|
logger.info(" agent_id: %s", settings.artenschutz_agent_id)
|
|
|
|
qdrant = QdrantReader(url=settings.qdrant_url, collection=settings.qdrant_collection)
|
|
bge = BGEClient(base_url=settings.bge_tei_url, timeout_s=settings.request_timeout_s)
|
|
memory_api = MemoryApiClient(base_url=settings.memory_api_url, timeout_s=settings.request_timeout_s)
|
|
|
|
app.state.settings = settings
|
|
app.state.qdrant = qdrant
|
|
app.state.bge = bge
|
|
app.state.memory_api = memory_api
|
|
|
|
try:
|
|
yield
|
|
finally:
|
|
await qdrant.close()
|
|
await bge.close()
|
|
await memory_api.close()
|
|
|
|
|
|
app = FastAPI(
|
|
title="artenschutz-router",
|
|
description=(
|
|
"Sidecar router for Artenschutz domain queries against agent.memory.system. "
|
|
"Writes proxy memory-api /memory/store; reads go directly to Qdrant."
|
|
),
|
|
version="0.1.0",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
|
|
app.include_router(search_routes.router)
|
|
app.include_router(ingest_routes.router)
|
|
|
|
|
|
@app.get("/health", response_model=HealthResponse, tags=["health"])
|
|
async def health() -> HealthResponse:
|
|
"""Live-probes Qdrant, BGE-M3 TEI, memory-api. `degraded` if any fail."""
|
|
qdrant_ok = True
|
|
bge_ok = True
|
|
memory_api_ok = False
|
|
|
|
try:
|
|
await app.state.qdrant._client.get_collections() # noqa: SLF001
|
|
except Exception:
|
|
qdrant_ok = False
|
|
|
|
try:
|
|
# TEI exposes /health; falling back to /info on older versions.
|
|
resp = await app.state.bge._client.get("/health") # noqa: SLF001
|
|
bge_ok = resp.status_code == 200
|
|
except Exception:
|
|
bge_ok = False
|
|
|
|
try:
|
|
memory_api_ok = await app.state.memory_api.health()
|
|
except Exception:
|
|
memory_api_ok = False
|
|
|
|
overall = "ok" if all([qdrant_ok, bge_ok, memory_api_ok]) else "degraded"
|
|
return HealthResponse(
|
|
status=overall,
|
|
qdrant=qdrant_ok,
|
|
bge=bge_ok,
|
|
memory_api=memory_api_ok,
|
|
)
|