optional FastAPI service skeleton

This commit is contained in:
2026-05-06 19:30:49 +02:00
parent f4f77b2eeb
commit e53bc4144d
8 changed files with 352 additions and 10 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -19,6 +19,7 @@ dev = [
]
service = [
"fastapi>=0.110",
"httpx>=0.27",
"uvicorn>=0.27",
]
storage = [

View File

@@ -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",
]

View File

@@ -0,0 +1,5 @@
"""Optional FastAPI service adapter for Kontextual Engine."""
from .app import ServiceRuntime, create_app
__all__ = ["ServiceRuntime", "create_app"]

View File

@@ -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

66
tests/test_service_api.py Normal file
View File

@@ -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"

View File

@@ -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