generated from coulomb/repo-seed
optional FastAPI service skeleton
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
86
docs/service-api-implementation.md
Normal file
86
docs/service-api-implementation.md
Normal 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.
|
||||
@@ -19,6 +19,7 @@ dev = [
|
||||
]
|
||||
service = [
|
||||
"fastapi>=0.110",
|
||||
"httpx>=0.27",
|
||||
"uvicorn>=0.27",
|
||||
]
|
||||
storage = [
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
5
src/kontextual_engine/api/__init__.py
Normal file
5
src/kontextual_engine/api/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Optional FastAPI service adapter for Kontextual Engine."""
|
||||
|
||||
from .app import ServiceRuntime, create_app
|
||||
|
||||
__all__ = ["ServiceRuntime", "create_app"]
|
||||
123
src/kontextual_engine/api/app.py
Normal file
123
src/kontextual_engine/api/app.py
Normal 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
66
tests/test_service_api.py
Normal 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"
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user