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 # Service API Boundary
Date: 2026-05-05 Date: 2026-05-06
## Decision ## Decision
@@ -10,11 +10,34 @@ define separate business behavior; it should expose the same artifact,
collection, relationship, ingestion, query, workflow, and context operations as collection, relationship, ingestion, query, workflow, and context operations as
HTTP resources. 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: Planned endpoint groups:
- `GET /health`
- `POST /collections`, `GET /collections`, `GET /collections/{id}` - `POST /collections`, `GET /collections`, `GET /collections/{id}`
- `POST /artifacts`, `GET /artifacts/{id}`, `GET /artifacts` - `POST /artifacts`, `GET /artifacts/{id}`, `GET /artifacts`
- `POST /relationships`, `GET /relationships` - `POST /relationships`, `GET /relationships`
@@ -23,6 +46,21 @@ Planned endpoint groups:
- `POST /runs`, `GET /runs/{id}`, `GET /runs/{id}/manifest` - `POST /runs`, `GET /runs/{id}`, `GET /runs/{id}/manifest`
- `POST /context/artifact/{id}` - `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 ## Rules
- The Python API is canonical until contracts stabilize. - The Python API is canonical until contracts stabilize.
@@ -34,9 +72,11 @@ Planned endpoint groups:
## Deferred ## Deferred
- Authentication and authorization. - Asset, metadata, relationship, audit, and policy endpoints.
- Durable database backend. - Ingestion, retrieval, transformation, and workflow endpoints.
- OpenAPI model polishing. - 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. - Streaming run execution.
- Provider-backed assisted steps. - 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 = [ service = [
"fastapi>=0.110", "fastapi>=0.110",
"httpx>=0.27",
"uvicorn>=0.27", "uvicorn>=0.27",
] ]
storage = [ storage = [

View File

@@ -13,6 +13,7 @@ from .artifacts import (
) )
from .adapters.memory import InMemoryAssetRegistryRepository from .adapters.memory import InMemoryAssetRegistryRepository
from .adapters.sqlite import SQLiteAssetRegistryRepository from .adapters.sqlite import SQLiteAssetRegistryRepository
from .api import ServiceRuntime, create_app
from .context import ContextAssembler, ContextItem, ContextPackage from .context import ContextAssembler, ContextItem, ContextPackage
from .core import ( from .core import (
Actor, Actor,
@@ -235,6 +236,7 @@ __all__ = [
"RunManifest", "RunManifest",
"RunStatus", "RunStatus",
"Sensitivity", "Sensitivity",
"ServiceRuntime",
"SourceReference", "SourceReference",
"SourceConnector", "SourceConnector",
"SourcePayload", "SourcePayload",
@@ -271,6 +273,7 @@ __all__ = [
"WorkflowTemplate", "WorkflowTemplate",
"bundle_digest", "bundle_digest",
"content_digest", "content_digest",
"create_app",
"default_transformation_registry", "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" title: "Service API And Agent-Safe Operation"
domain: markitect domain: markitect
repo: kontextual-engine repo: kontextual-engine
status: todo status: active
owner: codex owner: codex
topic_slug: markitect topic_slug: markitect
planning_priority: high planning_priority: high
planning_order: 9 planning_order: 9
created: "2026-05-05" created: "2026-05-05"
updated: "2026-05-05" updated: "2026-05-06"
state_hub_workstream_id: "6e672b1a-2e57-489e-8516-cb75611d4354" 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 engine control plane or let agents bypass engine policy, audit, lifecycle, and
review gates through Markitect APIs. 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 ## S9.1 - Implement versioned FastAPI service skeleton and health contracts
```task ```task
id: KONT-WP-0009-T001 id: KONT-WP-0009-T001
status: todo status: done
priority: high priority: high
state_hub_task_id: "bdb2380e-4ea1-4b8c-a6c9-fc8da2122813" state_hub_task_id: "bdb2380e-4ea1-4b8c-a6c9-fc8da2122813"
``` ```
@@ -64,6 +73,15 @@ Acceptance:
- Service code wraps core contracts rather than becoming the architecture. - Service code wraps core contracts rather than becoming the architecture.
- API versioning policy is documented for MVP. - 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 ## S9.2 - Expose asset metadata relationship audit and policy APIs
```task ```task