2026-05-06 19:30:49 +02:00
|
|
|
"""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
|
2026-05-06 20:39:29 +02:00
|
|
|
from kontextual_engine.core import (
|
|
|
|
|
Actor,
|
|
|
|
|
ActorType,
|
|
|
|
|
Classification,
|
|
|
|
|
ContextEntity,
|
|
|
|
|
ContextEntityType,
|
|
|
|
|
LifecycleState,
|
|
|
|
|
MetadataRecord,
|
|
|
|
|
OperationContext,
|
|
|
|
|
PolicyDecision,
|
|
|
|
|
RelationshipTargetKind,
|
|
|
|
|
SourceReference,
|
|
|
|
|
utc_now,
|
|
|
|
|
)
|
|
|
|
|
from kontextual_engine.errors import AuthorizationError, KontextualError, NotFoundError, ValidationError
|
|
|
|
|
from kontextual_engine.ports import AllowAllPolicyGateway, AssetRegistryRepository, PolicyGateway
|
|
|
|
|
from kontextual_engine.services import AssetRegistryService
|
2026-05-06 19:30:49 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
API_VERSION = "v1"
|
|
|
|
|
OPENAPI_VERSION = "1.0.0"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class ServiceRuntime:
|
|
|
|
|
repository: AssetRegistryRepository = field(default_factory=InMemoryAssetRegistryRepository)
|
2026-05-06 20:39:29 +02:00
|
|
|
policy_gateway: PolicyGateway = field(default_factory=AllowAllPolicyGateway)
|
2026-05-06 19:30:49 +02:00
|
|
|
api_version: str = API_VERSION
|
|
|
|
|
service_name: str = "kontextual-engine"
|
|
|
|
|
started_at: str = field(default_factory=lambda: utc_now().isoformat())
|
|
|
|
|
|
2026-05-06 20:39:29 +02:00
|
|
|
def asset_service(self) -> AssetRegistryService:
|
|
|
|
|
return AssetRegistryService(self.repository, policy_gateway=self.policy_gateway)
|
|
|
|
|
|
|
|
|
|
def operation_context(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
actor_id: str = "api-user",
|
|
|
|
|
actor_type: str = "human",
|
|
|
|
|
display_name: str | None = None,
|
|
|
|
|
correlation_id: str | None = None,
|
|
|
|
|
groups: list[str] | None = None,
|
|
|
|
|
metadata: dict[str, Any] | None = None,
|
|
|
|
|
) -> OperationContext:
|
|
|
|
|
actor = Actor.create(
|
|
|
|
|
ActorType(actor_type),
|
|
|
|
|
actor_id=actor_id,
|
|
|
|
|
display_name=display_name,
|
|
|
|
|
groups=groups,
|
|
|
|
|
metadata=metadata,
|
|
|
|
|
)
|
|
|
|
|
return OperationContext.create(actor, correlation_id=correlation_id)
|
|
|
|
|
|
2026-05-06 19:30:49 +02:00
|
|
|
@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,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 20:39:29 +02:00
|
|
|
def create_asset(self, payload: dict[str, Any], context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
classification = Classification.from_dict(payload["classification"])
|
|
|
|
|
result = self.asset_service().create_asset(
|
|
|
|
|
payload["title"],
|
|
|
|
|
classification,
|
|
|
|
|
context,
|
|
|
|
|
asset_id=payload.get("asset_id"),
|
|
|
|
|
source_refs=[_source_reference(item) for item in payload.get("source_refs", ())],
|
|
|
|
|
metadata_records=[_metadata_record(item) for item in payload.get("metadata_records", ())],
|
|
|
|
|
idempotency_key=payload.get("idempotency_key"),
|
|
|
|
|
)
|
|
|
|
|
return _asset_change_result(result)
|
|
|
|
|
|
|
|
|
|
def get_asset(self, asset_id: str) -> dict[str, Any]:
|
|
|
|
|
return self.asset_service().get_asset(asset_id).to_dict()
|
|
|
|
|
|
|
|
|
|
def list_assets(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
lifecycle: str | None = None,
|
|
|
|
|
asset_type: str | None = None,
|
|
|
|
|
sensitivity: str | None = None,
|
|
|
|
|
owner: str | None = None,
|
|
|
|
|
topic: str | None = None,
|
|
|
|
|
review_state: str | None = None,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
assets = self.asset_service().list_assets(
|
|
|
|
|
lifecycle=LifecycleState(lifecycle) if lifecycle else None,
|
|
|
|
|
asset_type=asset_type,
|
|
|
|
|
sensitivity=sensitivity,
|
|
|
|
|
owner=owner,
|
|
|
|
|
topic=topic,
|
|
|
|
|
review_state=review_state,
|
|
|
|
|
)
|
|
|
|
|
return {"items": [asset.to_dict() for asset in assets], "count": len(assets)}
|
|
|
|
|
|
|
|
|
|
def add_metadata_record(
|
|
|
|
|
self,
|
|
|
|
|
asset_id: str,
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
result = self.asset_service().add_metadata_record(
|
|
|
|
|
asset_id,
|
|
|
|
|
_metadata_record(payload),
|
|
|
|
|
context,
|
|
|
|
|
expected_current_version_id=payload.get("expected_current_version_id"),
|
|
|
|
|
)
|
|
|
|
|
return _asset_change_result(result)
|
|
|
|
|
|
|
|
|
|
def list_metadata_records(self, asset_id: str) -> dict[str, Any]:
|
|
|
|
|
records = self.repository.list_metadata_records(asset_id)
|
|
|
|
|
return {"items": [record.to_dict() for record in records], "count": len(records)}
|
|
|
|
|
|
|
|
|
|
def transition_lifecycle(
|
|
|
|
|
self,
|
|
|
|
|
asset_id: str,
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
result = self.asset_service().transition_lifecycle(
|
|
|
|
|
asset_id,
|
|
|
|
|
LifecycleState(payload["lifecycle"]),
|
|
|
|
|
context,
|
|
|
|
|
expected_current_version_id=payload.get("expected_current_version_id"),
|
|
|
|
|
)
|
|
|
|
|
return _asset_change_result(result)
|
|
|
|
|
|
|
|
|
|
def create_relationship(self, payload: dict[str, Any], context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
target_kind = RelationshipTargetKind(payload.get("target_kind", RelationshipTargetKind.ASSET.value))
|
|
|
|
|
service = self.asset_service()
|
|
|
|
|
if target_kind == RelationshipTargetKind.CONTEXT_ENTITY:
|
|
|
|
|
entity_payload = payload.get("context_entity") or {}
|
|
|
|
|
entity = ContextEntity(
|
|
|
|
|
entity_id=payload["target_id"],
|
|
|
|
|
entity_type=ContextEntityType(entity_payload.get("entity_type", ContextEntityType.TOPIC.value)),
|
|
|
|
|
name=entity_payload.get("name", payload["target_id"]),
|
|
|
|
|
external_ref=entity_payload.get("external_ref"),
|
|
|
|
|
metadata=dict(entity_payload.get("metadata", {})),
|
|
|
|
|
)
|
|
|
|
|
result = service.link_asset_to_context_entity(
|
|
|
|
|
payload["source_asset_id"],
|
|
|
|
|
entity,
|
|
|
|
|
payload["predicate"],
|
|
|
|
|
context,
|
|
|
|
|
confidence=payload.get("confidence"),
|
|
|
|
|
provenance=dict(payload.get("provenance", {})),
|
|
|
|
|
expected_current_version_id=payload.get("expected_current_version_id"),
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
result = service.link_asset_to_asset(
|
|
|
|
|
payload["source_asset_id"],
|
|
|
|
|
payload["target_id"],
|
|
|
|
|
payload["predicate"],
|
|
|
|
|
context,
|
|
|
|
|
confidence=payload.get("confidence"),
|
|
|
|
|
provenance=dict(payload.get("provenance", {})),
|
|
|
|
|
expected_current_version_id=payload.get("expected_current_version_id"),
|
|
|
|
|
)
|
|
|
|
|
return {
|
|
|
|
|
"relationship": result.relationship.to_dict(),
|
|
|
|
|
"version": result.version.to_dict(),
|
|
|
|
|
"audit_event": result.audit_event.to_dict(),
|
|
|
|
|
"policy_decision": result.policy_decision.to_dict(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def list_relationships(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
source_id: str | None = None,
|
|
|
|
|
target_id: str | None = None,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
relationships = self.repository.list_relationships(source_id=source_id, target_id=target_id)
|
|
|
|
|
return {
|
|
|
|
|
"items": [relationship.to_dict() for relationship in relationships],
|
|
|
|
|
"count": len(relationships),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def list_audit_events(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
target: str | None = None,
|
|
|
|
|
correlation_id: str | None = None,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
events = self.repository.list_audit_events(target=target, correlation_id=correlation_id)
|
|
|
|
|
return {"items": [event.to_dict() for event in events], "count": len(events)}
|
|
|
|
|
|
|
|
|
|
def evaluate_policy(self, payload: dict[str, Any], context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
try:
|
|
|
|
|
decision = self.policy_gateway.authorize(
|
|
|
|
|
context,
|
|
|
|
|
payload["action"],
|
|
|
|
|
payload["resource"],
|
|
|
|
|
resource_metadata=dict(payload.get("resource_metadata", {})),
|
|
|
|
|
)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
decision = PolicyDecision.fail_closed(
|
|
|
|
|
context.actor.id,
|
|
|
|
|
payload.get("action", "unknown"),
|
|
|
|
|
payload.get("resource", "unknown"),
|
|
|
|
|
reason=str(exc),
|
|
|
|
|
context={"gateway_error": type(exc).__name__},
|
|
|
|
|
)
|
|
|
|
|
return decision.to_dict()
|
|
|
|
|
|
2026-05-06 19:30:49 +02:00
|
|
|
|
|
|
|
|
def create_app(runtime: ServiceRuntime | None = None):
|
|
|
|
|
try:
|
2026-05-06 20:39:29 +02:00
|
|
|
from fastapi import Depends, FastAPI, Header, HTTPException, Query
|
2026-05-06 19:30:49 +02:00
|
|
|
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()
|
|
|
|
|
|
2026-05-06 20:39:29 +02:00
|
|
|
def context_from_headers(
|
|
|
|
|
x_actor_id: str = Header("api-user"),
|
|
|
|
|
x_actor_type: str = Header("human"),
|
|
|
|
|
x_actor_display_name: str | None = Header(None),
|
|
|
|
|
x_correlation_id: str | None = Header(None),
|
|
|
|
|
) -> OperationContext:
|
|
|
|
|
return runtime.operation_context(
|
|
|
|
|
actor_id=x_actor_id,
|
|
|
|
|
actor_type=x_actor_type,
|
|
|
|
|
display_name=x_actor_display_name,
|
|
|
|
|
correlation_id=x_correlation_id,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def response(callable_obj, *args: Any, **kwargs: Any) -> Any:
|
|
|
|
|
try:
|
|
|
|
|
return callable_obj(*args, **kwargs)
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=_error_payload(exc)) from exc
|
|
|
|
|
except AuthorizationError as exc:
|
|
|
|
|
raise HTTPException(status_code=403, detail=_error_payload(exc)) from exc
|
|
|
|
|
except ValidationError as exc:
|
|
|
|
|
raise HTTPException(status_code=422, detail=_error_payload(exc)) from exc
|
|
|
|
|
except KontextualError as exc:
|
|
|
|
|
raise HTTPException(status_code=400, detail=_error_payload(exc)) from exc
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/assets", tags=["assets"])
|
|
|
|
|
def create_asset(
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.create_asset, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/assets", tags=["assets"])
|
|
|
|
|
def list_assets(
|
|
|
|
|
lifecycle: str | None = Query(None),
|
|
|
|
|
asset_type: str | None = Query(None),
|
|
|
|
|
sensitivity: str | None = Query(None),
|
|
|
|
|
owner: str | None = Query(None),
|
|
|
|
|
topic: str | None = Query(None),
|
|
|
|
|
review_state: str | None = Query(None),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(
|
|
|
|
|
runtime.list_assets,
|
|
|
|
|
lifecycle=lifecycle,
|
|
|
|
|
asset_type=asset_type,
|
|
|
|
|
sensitivity=sensitivity,
|
|
|
|
|
owner=owner,
|
|
|
|
|
topic=topic,
|
|
|
|
|
review_state=review_state,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/assets/{{asset_id}}", tags=["assets"])
|
|
|
|
|
def get_asset(asset_id: str) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.get_asset, asset_id)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/assets/{{asset_id}}/metadata", tags=["metadata"])
|
|
|
|
|
def add_metadata(
|
|
|
|
|
asset_id: str,
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.add_metadata_record, asset_id, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/assets/{{asset_id}}/metadata", tags=["metadata"])
|
|
|
|
|
def list_metadata(asset_id: str) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.list_metadata_records, asset_id)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/assets/{{asset_id}}/lifecycle", tags=["assets"])
|
|
|
|
|
def transition_lifecycle(
|
|
|
|
|
asset_id: str,
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.transition_lifecycle, asset_id, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/relationships", tags=["relationships"])
|
|
|
|
|
def create_relationship(
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.create_relationship, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/relationships", tags=["relationships"])
|
|
|
|
|
def list_relationships(
|
|
|
|
|
source_id: str | None = Query(None),
|
|
|
|
|
target_id: str | None = Query(None),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.list_relationships, source_id=source_id, target_id=target_id)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/audit/events", tags=["audit"])
|
|
|
|
|
def list_audit_events(
|
|
|
|
|
target: str | None = Query(None),
|
|
|
|
|
correlation_id: str | None = Query(None),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.list_audit_events, target=target, correlation_id=correlation_id)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/policy/evaluate", tags=["policy"])
|
|
|
|
|
def evaluate_policy(
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.evaluate_policy, payload, context)
|
|
|
|
|
|
2026-05-06 19:30:49 +02:00
|
|
|
return app
|
2026-05-06 20:39:29 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _metadata_record(data: dict[str, Any]) -> MetadataRecord:
|
|
|
|
|
if "record_id" in data and "created_at" in data:
|
|
|
|
|
return MetadataRecord.from_dict(data)
|
|
|
|
|
return MetadataRecord(
|
|
|
|
|
key=data["key"],
|
|
|
|
|
value=data.get("value"),
|
|
|
|
|
provenance=dict(data.get("provenance", {})),
|
|
|
|
|
confidence=data.get("confidence"),
|
|
|
|
|
confirmed=bool(data.get("confirmed", False)),
|
|
|
|
|
record_id=data.get("record_id") or MetadataRecord(data["key"], data.get("value")).record_id,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _source_reference(data: dict[str, Any]) -> SourceReference:
|
|
|
|
|
if "id" in data:
|
|
|
|
|
return SourceReference.from_dict(data)
|
|
|
|
|
return SourceReference(
|
|
|
|
|
source_system=data["source_system"],
|
|
|
|
|
path=data.get("path"),
|
|
|
|
|
uri=data.get("uri"),
|
|
|
|
|
external_id=data.get("external_id"),
|
|
|
|
|
checksum=data.get("checksum"),
|
|
|
|
|
connector_ref=data.get("connector_ref"),
|
|
|
|
|
metadata=dict(data.get("metadata", {})),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _asset_change_result(result: Any) -> dict[str, Any]:
|
|
|
|
|
return {
|
|
|
|
|
"asset": result.asset.to_dict(),
|
|
|
|
|
"version": result.version.to_dict(),
|
|
|
|
|
"audit_event": result.audit_event.to_dict(),
|
|
|
|
|
"policy_decision": result.policy_decision.to_dict(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _error_payload(error: KontextualError) -> dict[str, Any]:
|
|
|
|
|
return {
|
|
|
|
|
"code": error.code,
|
|
|
|
|
"message": str(error),
|
|
|
|
|
"details": dict(error.details),
|
|
|
|
|
}
|