From e53bc4144d13599a70383ccb278d05f7656da51e Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 6 May 2026 19:30:49 +0200 Subject: [PATCH] optional FastAPI service skeleton --- docs/service-api-boundary.md | 54 +++++++- docs/service-api-implementation.md | 86 ++++++++++++ pyproject.toml | 1 + src/kontextual_engine/__init__.py | 3 + src/kontextual_engine/api/__init__.py | 5 + src/kontextual_engine/api/app.py | 123 ++++++++++++++++++ tests/test_service_api.py | 66 ++++++++++ ...P-0009-service-api-agent-safe-operation.md | 24 +++- 8 files changed, 352 insertions(+), 10 deletions(-) create mode 100644 docs/service-api-implementation.md create mode 100644 src/kontextual_engine/api/__init__.py create mode 100644 src/kontextual_engine/api/app.py create mode 100644 tests/test_service_api.py diff --git a/docs/service-api-boundary.md b/docs/service-api-boundary.md index 5e41307..a9f04bb 100644 --- a/docs/service-api-boundary.md +++ b/docs/service-api-boundary.md @@ -1,6 +1,6 @@ # Service API Boundary -Date: 2026-05-05 +Date: 2026-05-06 ## Decision @@ -10,11 +10,34 @@ define separate business behavior; it should expose the same artifact, collection, relationship, ingestion, query, workflow, and context operations as HTTP resources. -## First Resource Shape +## Implemented MVP Resource Shape + +Implemented in `KONT-WP-0009-T001`: + +- `GET /health` +- `GET /ready` +- `GET /version` +- `GET /api/v1/health` +- `GET /api/v1/ready` +- `GET /api/v1/version` +- `GET /openapi.json` + +The unversioned health/readiness/version endpoints are operational probes. The +versioned `/api/v1/*` endpoints establish the MVP API namespace. Future +domain-resource endpoints should live under `/api/v1`. + +The service dependency is optional. Importing `kontextual_engine` and +`kontextual_engine.api` does not require FastAPI; calling `create_app()` does. +Install the `service` extra to run the HTTP adapter: + +```bash +python3 -m pip install -e '.[service]' +``` + +## Planned Resource Shape Planned endpoint groups: -- `GET /health` - `POST /collections`, `GET /collections`, `GET /collections/{id}` - `POST /artifacts`, `GET /artifacts/{id}`, `GET /artifacts` - `POST /relationships`, `GET /relationships` @@ -23,6 +46,21 @@ Planned endpoint groups: - `POST /runs`, `GET /runs/{id}`, `GET /runs/{id}/manifest` - `POST /context/artifact/{id}` +For the governed asset registry architecture, these planned groups should be +translated to assets, metadata, relationships, ingestion jobs, retrieval, +transformations, workflow templates/runs, review queues, and reconstruction +resources. + +## MVP API Versioning Policy + +- `/api/v1` is the first stable namespace for implemented service resources. +- Backward-incompatible changes require a new namespace such as `/api/v2`. +- Additive response fields are allowed inside a version. +- Structured diagnostics and error codes are part of the contract and should + remain stable inside a version. +- Unversioned operational probes may remain as aliases for the active API + generation, but domain-resource endpoints must be versioned. + ## Rules - The Python API is canonical until contracts stabilize. @@ -34,9 +72,11 @@ Planned endpoint groups: ## Deferred -- Authentication and authorization. -- Durable database backend. -- OpenAPI model polishing. +- Asset, metadata, relationship, audit, and policy endpoints. +- Ingestion, retrieval, transformation, and workflow endpoints. +- Actor context, delegation, and authorization middleware. +- Agent-safe operation catalog. +- Context package API. +- Dry-run and review-gate response envelopes for high-impact operations. - Streaming run execution. - Provider-backed assisted steps. - diff --git a/docs/service-api-implementation.md b/docs/service-api-implementation.md new file mode 100644 index 0000000..5f8cba4 --- /dev/null +++ b/docs/service-api-implementation.md @@ -0,0 +1,86 @@ +# Service API Implementation Note + +Date: 2026-05-06 + +Status: active implementation note for `KONT-WP-0009`. + +## Purpose + +This note records the first optional FastAPI service adapter. The service layer +is intentionally thin: it exposes operational probes and API version metadata +while leaving domain behavior in the application services. + +## Implemented Package Shape + +```text +src/kontextual_engine/ + api/ + __init__.py + app.py +``` + +`kontextual_engine.api.create_app()` builds the FastAPI app when the optional +`service` extra is installed. Importing the package does not require FastAPI. + +## Implemented Endpoints + +- `GET /health` +- `GET /ready` +- `GET /version` +- `GET /api/v1/health` +- `GET /api/v1/ready` +- `GET /api/v1/version` +- `GET /openapi.json` + +Unversioned endpoints are operational probes. Versioned endpoints establish +the `/api/v1` namespace for future domain resources. + +## Runtime Contract + +`ServiceRuntime` owns the adapter runtime state: + +- repository reference, +- API version, +- service name, +- startup timestamp, +- package version discovery, +- health payload, +- readiness checks, +- version payload. + +Readiness currently checks that the configured asset registry repository can +list assets. It does not mutate state. + +## Dependency Boundary + +The `service` extra now includes FastAPI, Uvicorn, and HTTPX for test-client +execution: + +```text +kontextual-engine[service] +``` + +The current workspace does not have FastAPI installed, so HTTP adapter tests +are skipped locally until the extra is installed. The pure runtime contract and +missing-dependency behavior are tested without FastAPI. + +## Test Coverage + +`tests/test_service_api.py` covers: + +- service runtime health/readiness/version without FastAPI installed, +- `create_app()` missing-dependency behavior when the optional extra is absent, +- health/readiness/version/OpenAPI endpoint contracts when FastAPI and HTTPX + are installed, +- runtime attachment to FastAPI application state when the service extra is + installed. + +## Not Yet Implemented + +- Asset, metadata, relationship, audit, and policy endpoints. +- Ingestion, retrieval, transformation, workflow, review, and reconstruction + endpoints. +- Request actor context and delegation middleware. +- Bounded agent operation catalog. +- Context package API. +- Dry-run and review-gate response envelopes. diff --git a/pyproject.toml b/pyproject.toml index 2955cc0..7403ce3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dev = [ ] service = [ "fastapi>=0.110", + "httpx>=0.27", "uvicorn>=0.27", ] storage = [ diff --git a/src/kontextual_engine/__init__.py b/src/kontextual_engine/__init__.py index 04c440f..91484f0 100644 --- a/src/kontextual_engine/__init__.py +++ b/src/kontextual_engine/__init__.py @@ -13,6 +13,7 @@ from .artifacts import ( ) from .adapters.memory import InMemoryAssetRegistryRepository from .adapters.sqlite import SQLiteAssetRegistryRepository +from .api import ServiceRuntime, create_app from .context import ContextAssembler, ContextItem, ContextPackage from .core import ( Actor, @@ -235,6 +236,7 @@ __all__ = [ "RunManifest", "RunStatus", "Sensitivity", + "ServiceRuntime", "SourceReference", "SourceConnector", "SourcePayload", @@ -271,6 +273,7 @@ __all__ = [ "WorkflowTemplate", "bundle_digest", "content_digest", + "create_app", "default_transformation_registry", ] diff --git a/src/kontextual_engine/api/__init__.py b/src/kontextual_engine/api/__init__.py new file mode 100644 index 0000000..3a3d4ee --- /dev/null +++ b/src/kontextual_engine/api/__init__.py @@ -0,0 +1,5 @@ +"""Optional FastAPI service adapter for Kontextual Engine.""" + +from .app import ServiceRuntime, create_app + +__all__ = ["ServiceRuntime", "create_app"] diff --git a/src/kontextual_engine/api/app.py b/src/kontextual_engine/api/app.py new file mode 100644 index 0000000..1c67ee6 --- /dev/null +++ b/src/kontextual_engine/api/app.py @@ -0,0 +1,123 @@ +"""Versioned FastAPI service skeleton. + +The service layer is intentionally thin: route handlers translate HTTP +requests into service/runtime contracts and must not own domain behavior. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from importlib import metadata +from typing import Any + +from kontextual_engine.adapters.memory import InMemoryAssetRegistryRepository +from kontextual_engine.core import utc_now +from kontextual_engine.ports import AssetRegistryRepository + + +API_VERSION = "v1" +OPENAPI_VERSION = "1.0.0" + + +@dataclass +class ServiceRuntime: + repository: AssetRegistryRepository = field(default_factory=InMemoryAssetRegistryRepository) + api_version: str = API_VERSION + service_name: str = "kontextual-engine" + started_at: str = field(default_factory=lambda: utc_now().isoformat()) + + @property + def package_version(self) -> str: + try: + return metadata.version("kontextual-engine") + except metadata.PackageNotFoundError: + return "0.1.0" + + def health(self) -> dict[str, Any]: + return { + "status": "ok", + "service": self.service_name, + "api_version": self.api_version, + "package_version": self.package_version, + "started_at": self.started_at, + } + + def readiness(self) -> dict[str, Any]: + checks: dict[str, dict[str, Any]] = {} + try: + asset_count = len(self.repository.list_assets()) + checks["asset_registry"] = { + "status": "ok", + "repository": type(self.repository).__name__, + "asset_count": asset_count, + } + except Exception as exc: + checks["asset_registry"] = { + "status": "error", + "repository": type(self.repository).__name__, + "error_type": type(exc).__name__, + "message": str(exc), + } + ready = all(item["status"] == "ok" for item in checks.values()) + return { + "status": "ready" if ready else "not_ready", + "ready": ready, + "service": self.service_name, + "api_version": self.api_version, + "checks": checks, + } + + def version(self) -> dict[str, Any]: + return { + "service": self.service_name, + "api_version": self.api_version, + "package_version": self.package_version, + "openapi_version": OPENAPI_VERSION, + } + + +def create_app(runtime: ServiceRuntime | None = None): + try: + from fastapi import FastAPI + except ImportError as exc: # pragma: no cover - exercised when optional extra is absent + raise RuntimeError( + "FastAPI service dependencies are not installed. Install kontextual-engine[service]." + ) from exc + + runtime = runtime or ServiceRuntime() + app = FastAPI( + title="Kontextual Engine Service API", + version=OPENAPI_VERSION, + openapi_url="/openapi.json", + docs_url="/docs", + redoc_url="/redoc", + ) + app.state.kontextual_runtime = runtime + + @app.get("/health", tags=["system"]) + def health() -> dict[str, Any]: + return runtime.health() + + @app.get("/ready", tags=["system"]) + def ready() -> dict[str, Any]: + return runtime.readiness() + + @app.get("/version", tags=["system"]) + def version() -> dict[str, Any]: + return runtime.version() + + prefix = f"/api/{runtime.api_version}" + + @app.get(f"{prefix}/health", tags=["system"]) + def versioned_health() -> dict[str, Any]: + return runtime.health() + + @app.get(f"{prefix}/ready", tags=["system"]) + def versioned_ready() -> dict[str, Any]: + return runtime.readiness() + + @app.get(f"{prefix}/version", tags=["system"]) + def versioned_version() -> dict[str, Any]: + return runtime.version() + + return app diff --git a/tests/test_service_api.py b/tests/test_service_api.py new file mode 100644 index 0000000..f58a960 --- /dev/null +++ b/tests/test_service_api.py @@ -0,0 +1,66 @@ +import pytest + +from kontextual_engine import ServiceRuntime, create_app +from kontextual_engine.adapters.memory import InMemoryAssetRegistryRepository + + +def test_service_runtime_health_readiness_and_version_are_importable_without_fastapi() -> None: + runtime = ServiceRuntime(repository=InMemoryAssetRegistryRepository()) + + assert runtime.health()["status"] == "ok" + assert runtime.readiness()["ready"] is True + assert runtime.readiness()["checks"]["asset_registry"]["repository"] == ( + "InMemoryAssetRegistryRepository" + ) + assert runtime.version()["api_version"] == "v1" + + +def test_create_app_reports_missing_optional_dependency_when_fastapi_is_absent() -> None: + try: + import fastapi # noqa: F401 + except ImportError: + with pytest.raises(RuntimeError, match=r"kontextual-engine\[service\]"): + create_app() + else: + pytest.skip("FastAPI is installed; missing-dependency path is not active") + + +@pytest.fixture +def client(): + pytest.importorskip("fastapi") + pytest.importorskip("httpx") + from fastapi.testclient import TestClient + + app = create_app(ServiceRuntime(repository=InMemoryAssetRegistryRepository())) + with TestClient(app) as test_client: + yield test_client + + +def test_service_health_readiness_version_and_openapi_contracts(client) -> None: + health = client.get("/health") + ready = client.get("/ready") + version = client.get("/version") + versioned_health = client.get("/api/v1/health") + openapi = client.get("/openapi.json") + + assert health.status_code == 200 + assert health.json()["status"] == "ok" + assert ready.status_code == 200 + assert ready.json()["ready"] is True + assert ready.json()["checks"]["asset_registry"]["status"] == "ok" + assert version.status_code == 200 + assert version.json()["api_version"] == "v1" + assert versioned_health.status_code == 200 + assert versioned_health.json()["api_version"] == "v1" + assert openapi.status_code == 200 + paths = openapi.json()["paths"] + assert "/health" in paths + assert "/ready" in paths + assert "/version" in paths + assert "/api/v1/health" in paths + assert "/api/v1/ready" in paths + assert "/api/v1/version" in paths + + +def test_create_app_attaches_runtime_to_application_state(client) -> None: + assert client.app.state.kontextual_runtime.api_version == "v1" diff --git a/workplans/KONT-WP-0009-service-api-agent-safe-operation.md b/workplans/KONT-WP-0009-service-api-agent-safe-operation.md index 42e711d..0a0d572 100644 --- a/workplans/KONT-WP-0009-service-api-agent-safe-operation.md +++ b/workplans/KONT-WP-0009-service-api-agent-safe-operation.md @@ -4,13 +4,13 @@ type: workplan title: "Service API And Agent-Safe Operation" domain: markitect repo: kontextual-engine -status: todo +status: active owner: codex topic_slug: markitect planning_priority: high planning_order: 9 created: "2026-05-05" -updated: "2026-05-05" +updated: "2026-05-06" state_hub_workstream_id: "6e672b1a-2e57-489e-8516-cb75611d4354" --- @@ -46,11 +46,20 @@ deterministic markdown operations. They must not expose the `mkt` CLI as the engine control plane or let agents bypass engine policy, audit, lifecycle, and review gates through Markitect APIs. +## Implementation Status + +The first optional FastAPI service skeleton is implemented for health, +readiness, version, and OpenAPI contracts. See +`docs/service-api-implementation.md`. + +Domain-resource endpoints, actor context, agent operations, context packages, +and dry-run/review-gate response contracts remain open in this workplan. + ## S9.1 - Implement versioned FastAPI service skeleton and health contracts ```task id: KONT-WP-0009-T001 -status: todo +status: done priority: high state_hub_task_id: "bdb2380e-4ea1-4b8c-a6c9-fc8da2122813" ``` @@ -64,6 +73,15 @@ Acceptance: - Service code wraps core contracts rather than becoming the architecture. - API versioning policy is documented for MVP. +Implemented: + +- `kontextual_engine.api.create_app()` creates an optional FastAPI adapter. +- `ServiceRuntime` provides health, readiness, and version payloads without + requiring FastAPI at import time. +- Operational probes are exposed at `/health`, `/ready`, and `/version`; MVP + versioned aliases are exposed under `/api/v1`. +- API versioning policy is documented in `docs/service-api-boundary.md`. + ## S9.2 - Expose asset metadata relationship audit and policy APIs ```task