diff --git a/agent_core/__init__.py b/agent_core/__init__.py index bf0eddf..ffd607b 100644 --- a/agent_core/__init__.py +++ b/agent_core/__init__.py @@ -2,4 +2,4 @@ from __future__ import annotations -__version__ = "0.1.0" +__version__ = "0.4.0" diff --git a/agent_core/collector/app.py b/agent_core/collector/app.py index 6776b05..15d1c60 100644 --- a/agent_core/collector/app.py +++ b/agent_core/collector/app.py @@ -8,10 +8,11 @@ from collections.abc import AsyncIterator from contextlib import asynccontextmanager +from importlib.metadata import PackageNotFoundError, version from typing import Any -from fastapi import FastAPI -from sqlalchemy import select +from fastapi import FastAPI, HTTPException +from sqlalchemy import select, text from agent_core.collector.db import ( TraceEventRow, @@ -23,6 +24,15 @@ from agent_core.contracts.tracing import TraceEvent +def _package_version() -> str: + try: + return version("agent-core") + except PackageNotFoundError: + from agent_core import __version__ + + return __version__ + + def _row_from_event(event: TraceEvent) -> TraceEventRow: cost = event.cost return TraceEventRow( @@ -57,10 +67,16 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]: yield await engine.dispose() - app = FastAPI(title="agent-core trace collector", version="0.2.0", lifespan=lifespan) + app = FastAPI(title="agent-core trace collector", version=_package_version(), lifespan=lifespan) + app.state.collector_sessionmaker = sessionmaker @app.get("/healthz") async def healthz() -> dict[str, str]: + try: + async with app.state.collector_sessionmaker() as session: + await session.execute(text("SELECT 1")) + except Exception as exc: + raise HTTPException(status_code=503, detail="database unavailable") from exc return {"status": "ok"} @app.post("/v1/trace") diff --git a/pyproject.toml b/pyproject.toml index 07e3a53..8f3eb48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agent-core" -version = "0.3.0" +version = "0.4.0" description = "Shared typed contracts for the AS215932 Agent Runtime Framework (Phase 1 / ยง31 safe milestone)." requires-python = ">=3.11" dependencies = ["pydantic>=2,<3"] @@ -16,6 +16,12 @@ dev = [ "fastapi>=0.110", "uvicorn>=0.29", "sqlalchemy>=2", "httpx>=0.27", "aiosqlite>=0.20", ] +[dependency-groups] +dev = [ + "pytest>=8", "pyyaml>=6", "ruff>=0.5", "mypy>=1.8", "types-PyYAML", + "fastapi>=0.110", "uvicorn>=0.29", "sqlalchemy>=2", "httpx>=0.27", "aiosqlite>=0.20", +] + [tool.hatch.build.targets.wheel] packages = ["agent_core"] diff --git a/tests/collector/test_collector.py b/tests/collector/test_collector.py index 6b4aacc..f76b1da 100644 --- a/tests/collector/test_collector.py +++ b/tests/collector/test_collector.py @@ -1,5 +1,7 @@ from __future__ import annotations +from types import TracebackType + from fastapi.testclient import TestClient from agent_core.collector.app import create_app @@ -14,6 +16,27 @@ def test_healthz(tmp_path) -> None: assert client.get("/healthz").json() == {"status": "ok"} +def test_healthz_checks_database_connectivity(tmp_path) -> None: + class BrokenSession: + async def __aenter__(self) -> BrokenSession: + raise RuntimeError("database down") + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + return None + + app = _app(tmp_path) + app.state.collector_sessionmaker = lambda: BrokenSession() + with TestClient(app) as client: + response = client.get("/healthz") + assert response.status_code == 503 + assert response.json() == {"detail": "database unavailable"} + + def test_ingest_and_read(tmp_path) -> None: event = { "event_type": "model_call",