generated from coulomb/repo-seed
optional FastAPI service skeleton
This commit is contained in:
@@ -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.
|
||||||
|
|
||||||
|
|||||||
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 = [
|
service = [
|
||||||
"fastapi>=0.110",
|
"fastapi>=0.110",
|
||||||
|
"httpx>=0.27",
|
||||||
"uvicorn>=0.27",
|
"uvicorn>=0.27",
|
||||||
]
|
]
|
||||||
storage = [
|
storage = [
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
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"
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user