artenschutz-router/src/artenschutz_router/main.py
m2 (AI Agent) 6da874b2e6 Initial commit — phase 1 + 2 of artenschutz-digest concept
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.
2026-05-14 16:36:00 +02:00

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,
)