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
|
|
|
|
|
|
2026-05-06 21:24:38 +02:00
|
|
|
import json
|
2026-05-06 19:30:49 +02:00
|
|
|
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,
|
2026-05-06 21:24:38 +02:00
|
|
|
AuditEvent,
|
|
|
|
|
AuditOutcome,
|
2026-05-06 20:39:29 +02:00
|
|
|
Classification,
|
|
|
|
|
ContextEntity,
|
|
|
|
|
ContextEntityType,
|
2026-05-06 21:24:38 +02:00
|
|
|
IngestionIdentityPolicy,
|
|
|
|
|
IngestionJobStatus,
|
2026-05-06 20:39:29 +02:00
|
|
|
LifecycleState,
|
|
|
|
|
MetadataRecord,
|
|
|
|
|
OperationContext,
|
|
|
|
|
PolicyDecision,
|
2026-05-06 21:24:38 +02:00
|
|
|
PolicyEffect,
|
2026-05-06 20:39:29 +02:00
|
|
|
RelationshipTargetKind,
|
2026-05-06 21:24:38 +02:00
|
|
|
RetrievalFeedbackLabel,
|
2026-05-06 20:39:29 +02:00
|
|
|
SourceReference,
|
2026-05-06 21:24:38 +02:00
|
|
|
TransformationRunStatus,
|
|
|
|
|
WorkflowExceptionKind,
|
|
|
|
|
WorkflowExceptionStatus,
|
|
|
|
|
WorkflowInputDefinition,
|
|
|
|
|
WorkflowReviewDecisionType,
|
|
|
|
|
WorkflowReviewStatus,
|
|
|
|
|
WorkflowRunStatus,
|
|
|
|
|
WorkflowStepDefinition,
|
|
|
|
|
WorkflowTemplate,
|
|
|
|
|
new_id,
|
2026-05-06 20:39:29 +02:00
|
|
|
utc_now,
|
|
|
|
|
)
|
|
|
|
|
from kontextual_engine.errors import AuthorizationError, KontextualError, NotFoundError, ValidationError
|
|
|
|
|
from kontextual_engine.ports import AllowAllPolicyGateway, AssetRegistryRepository, PolicyGateway
|
2026-05-06 21:24:38 +02:00
|
|
|
from kontextual_engine.services import (
|
|
|
|
|
AssetIngestionService,
|
|
|
|
|
AssetQueryRequest,
|
|
|
|
|
AssetRegistryService,
|
|
|
|
|
AssetRetrievalService,
|
|
|
|
|
ContextEntityQueryRequest,
|
|
|
|
|
RelationshipQueryRequest,
|
|
|
|
|
RetrievalFeedbackRequest,
|
|
|
|
|
TransformationRequest,
|
|
|
|
|
TransformationService,
|
|
|
|
|
WorkflowInvocation,
|
|
|
|
|
WorkflowService,
|
|
|
|
|
)
|
2026-05-06 19:30:49 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
API_VERSION = "v1"
|
|
|
|
|
OPENAPI_VERSION = "1.0.0"
|
|
|
|
|
|
|
|
|
|
|
2026-05-06 21:24:38 +02:00
|
|
|
AGENT_OPERATION_CATALOG: tuple[dict[str, Any], ...] = (
|
|
|
|
|
{
|
|
|
|
|
"operation_id": "inspect_asset",
|
|
|
|
|
"description": "Read one asset envelope by ID.",
|
|
|
|
|
"input_schema": {"required": ["asset_id"]},
|
|
|
|
|
"output_schema": {"type": "asset"},
|
|
|
|
|
"required_permissions": ["agent.operation.inspect_asset", "asset.retrieve"],
|
|
|
|
|
"audit_operation": "agent.operation.inspect_asset",
|
|
|
|
|
"failure_modes": ["not_found", "permission_denied"],
|
|
|
|
|
"dry_run_supported": True,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"operation_id": "retrieve_asset",
|
|
|
|
|
"description": "Read one source-grounded asset bundle with metadata, representations, and relationships.",
|
|
|
|
|
"input_schema": {"required": ["asset_id"]},
|
|
|
|
|
"output_schema": {"type": "asset_bundle"},
|
|
|
|
|
"required_permissions": ["agent.operation.retrieve_asset", "asset.retrieve"],
|
|
|
|
|
"audit_operation": "agent.operation.retrieve_asset",
|
|
|
|
|
"failure_modes": ["not_found", "permission_denied"],
|
|
|
|
|
"dry_run_supported": True,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"operation_id": "search_assets",
|
|
|
|
|
"description": "Run a governed retrieval query over assets.",
|
|
|
|
|
"input_schema": {"required": ["query"]},
|
|
|
|
|
"output_schema": {"type": "asset_query_result"},
|
|
|
|
|
"required_permissions": ["agent.operation.search_assets", "retrieval.assets.query"],
|
|
|
|
|
"audit_operation": "agent.operation.search_assets",
|
|
|
|
|
"failure_modes": ["permission_denied", "validation_error", "zero_results"],
|
|
|
|
|
"dry_run_supported": True,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"operation_id": "assemble_context",
|
|
|
|
|
"description": "Assemble a non-durable source-grounded context preview from a bounded asset query.",
|
|
|
|
|
"input_schema": {"required": ["query"], "optional": ["intent", "instructions", "constraints"]},
|
|
|
|
|
"output_schema": {"type": "context_preview"},
|
|
|
|
|
"required_permissions": ["agent.operation.assemble_context", "retrieval.assets.query"],
|
|
|
|
|
"audit_operation": "agent.operation.assemble_context",
|
|
|
|
|
"failure_modes": ["permission_denied", "validation_error", "zero_results"],
|
|
|
|
|
"dry_run_supported": True,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"operation_id": "enrich_metadata",
|
|
|
|
|
"description": "Add one metadata record to an asset.",
|
|
|
|
|
"input_schema": {"required": ["asset_id", "metadata"]},
|
|
|
|
|
"output_schema": {"type": "asset_change"},
|
|
|
|
|
"required_permissions": ["agent.operation.enrich_metadata", "asset.metadata.add"],
|
|
|
|
|
"audit_operation": "agent.operation.enrich_metadata",
|
|
|
|
|
"failure_modes": ["not_found", "permission_denied", "validation_error", "version_conflict"],
|
|
|
|
|
"dry_run_supported": True,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"operation_id": "classify_asset",
|
|
|
|
|
"description": "Request the classify transformation operation for an asset.",
|
|
|
|
|
"input_schema": {"required": ["asset_id"]},
|
|
|
|
|
"output_schema": {"type": "transformation_run_result"},
|
|
|
|
|
"required_permissions": ["agent.operation.classify_asset", "transformation.run.execute"],
|
|
|
|
|
"audit_operation": "agent.operation.classify_asset",
|
|
|
|
|
"failure_modes": ["permission_denied", "adapter_unavailable", "operation_failed"],
|
|
|
|
|
"dry_run_supported": True,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"operation_id": "transform_asset",
|
|
|
|
|
"description": "Execute a registered transformation operation.",
|
|
|
|
|
"input_schema": {"required": ["transformation"]},
|
|
|
|
|
"output_schema": {"type": "transformation_run_result"},
|
|
|
|
|
"required_permissions": ["agent.operation.transform_asset", "transformation.run.execute"],
|
|
|
|
|
"audit_operation": "agent.operation.transform_asset",
|
|
|
|
|
"failure_modes": ["permission_denied", "adapter_unavailable", "operation_failed"],
|
|
|
|
|
"dry_run_supported": True,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"operation_id": "invoke_workflow",
|
|
|
|
|
"description": "Invoke a registered workflow template.",
|
|
|
|
|
"input_schema": {"required": ["workflow"]},
|
|
|
|
|
"output_schema": {"type": "workflow_run_result"},
|
|
|
|
|
"required_permissions": ["agent.operation.invoke_workflow", "workflow.run.execute"],
|
|
|
|
|
"audit_operation": "agent.operation.invoke_workflow",
|
|
|
|
|
"failure_modes": ["not_found", "permission_denied", "operation_failed", "review_required"],
|
|
|
|
|
"dry_run_supported": True,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"operation_id": "submit_review",
|
|
|
|
|
"description": "Submit a decision for one open workflow review task.",
|
|
|
|
|
"input_schema": {"required": ["run_id", "review_id", "decision"]},
|
|
|
|
|
"output_schema": {"type": "workflow_run_result"},
|
|
|
|
|
"required_permissions": ["agent.operation.submit_review", "workflow.review.decide"],
|
|
|
|
|
"audit_operation": "agent.operation.submit_review",
|
|
|
|
|
"failure_modes": ["not_found", "permission_denied", "review_not_open", "operation_failed"],
|
|
|
|
|
"dry_run_supported": True,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"operation_id": "report_result",
|
|
|
|
|
"description": "Record an agent result report without mutating domain assets.",
|
|
|
|
|
"input_schema": {"required": ["summary"], "optional": ["result_ref", "metadata"]},
|
|
|
|
|
"output_schema": {"type": "agent_report"},
|
|
|
|
|
"required_permissions": ["agent.operation.report_result"],
|
|
|
|
|
"audit_operation": "agent.operation.report_result",
|
|
|
|
|
"failure_modes": ["permission_denied", "validation_error"],
|
|
|
|
|
"dry_run_supported": True,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-05-06 19:30:49 +02:00
|
|
|
@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)
|
|
|
|
|
|
2026-05-06 21:24:38 +02:00
|
|
|
def ingestion_service(self) -> AssetIngestionService:
|
|
|
|
|
return AssetIngestionService(self.repository, asset_service=self.asset_service())
|
|
|
|
|
|
|
|
|
|
def retrieval_service(self) -> AssetRetrievalService:
|
|
|
|
|
return AssetRetrievalService(self.repository, policy_gateway=self.policy_gateway)
|
|
|
|
|
|
|
|
|
|
def transformation_service(self) -> TransformationService:
|
|
|
|
|
return TransformationService(
|
|
|
|
|
self.repository,
|
|
|
|
|
policy_gateway=self.policy_gateway,
|
|
|
|
|
asset_service=self.asset_service(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def workflow_service(self) -> WorkflowService:
|
|
|
|
|
return WorkflowService(
|
|
|
|
|
self.repository,
|
|
|
|
|
transformation_service=self.transformation_service(),
|
|
|
|
|
policy_gateway=self.policy_gateway,
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-06 20:39:29 +02:00
|
|
|
def operation_context(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
actor_id: str = "api-user",
|
|
|
|
|
actor_type: str = "human",
|
|
|
|
|
display_name: str | None = None,
|
2026-05-06 21:24:38 +02:00
|
|
|
external_ref: str | None = None,
|
2026-05-06 20:39:29 +02:00
|
|
|
correlation_id: str | None = None,
|
|
|
|
|
groups: list[str] | None = None,
|
2026-05-06 21:24:38 +02:00
|
|
|
delegated_actor_id: str | None = None,
|
|
|
|
|
delegated_actor_type: str = "human",
|
|
|
|
|
delegated_actor_display_name: str | None = None,
|
|
|
|
|
delegated_actor_external_ref: str | None = None,
|
|
|
|
|
delegated_actor_groups: list[str] | None = None,
|
|
|
|
|
request_scope: dict[str, Any] | None = None,
|
|
|
|
|
policy_scope: dict[str, Any] | None = None,
|
|
|
|
|
agent_id: str | None = None,
|
|
|
|
|
agent_name: str | None = None,
|
|
|
|
|
agent_run_id: str | None = None,
|
|
|
|
|
agent_tool: str | None = None,
|
2026-05-06 20:39:29 +02:00
|
|
|
metadata: dict[str, Any] | None = None,
|
|
|
|
|
) -> OperationContext:
|
2026-05-06 21:24:38 +02:00
|
|
|
actor_metadata = dict(metadata or {})
|
|
|
|
|
agent_metadata = _agent_metadata(
|
|
|
|
|
agent_id=agent_id,
|
|
|
|
|
agent_name=agent_name,
|
|
|
|
|
agent_run_id=agent_run_id,
|
|
|
|
|
agent_tool=agent_tool,
|
|
|
|
|
)
|
|
|
|
|
if agent_metadata:
|
|
|
|
|
actor_metadata["agent"] = agent_metadata
|
2026-05-06 20:39:29 +02:00
|
|
|
actor = Actor.create(
|
|
|
|
|
ActorType(actor_type),
|
|
|
|
|
actor_id=actor_id,
|
|
|
|
|
display_name=display_name,
|
2026-05-06 21:24:38 +02:00
|
|
|
external_ref=external_ref,
|
2026-05-06 20:39:29 +02:00
|
|
|
groups=groups,
|
2026-05-06 21:24:38 +02:00
|
|
|
metadata=actor_metadata,
|
|
|
|
|
)
|
|
|
|
|
delegated_actor = None
|
|
|
|
|
if delegated_actor_id:
|
|
|
|
|
delegated_actor = Actor.create(
|
|
|
|
|
ActorType(delegated_actor_type),
|
|
|
|
|
actor_id=delegated_actor_id,
|
|
|
|
|
display_name=delegated_actor_display_name,
|
|
|
|
|
external_ref=delegated_actor_external_ref,
|
|
|
|
|
groups=delegated_actor_groups,
|
|
|
|
|
)
|
|
|
|
|
context_metadata: dict[str, Any] = {}
|
|
|
|
|
if agent_metadata:
|
|
|
|
|
context_metadata["agent"] = agent_metadata
|
|
|
|
|
if delegated_actor is not None:
|
|
|
|
|
context_metadata["delegation"] = {
|
|
|
|
|
"mode": "on_behalf_of",
|
|
|
|
|
"actor_id": actor.id,
|
|
|
|
|
"delegated_actor_id": delegated_actor.id,
|
|
|
|
|
}
|
|
|
|
|
return OperationContext.create(
|
|
|
|
|
actor,
|
|
|
|
|
correlation_id=correlation_id,
|
|
|
|
|
delegated_actor=delegated_actor,
|
|
|
|
|
request_scope=request_scope,
|
|
|
|
|
policy_scope=policy_scope,
|
|
|
|
|
metadata=context_metadata,
|
2026-05-06 20:39:29 +02:00
|
|
|
)
|
|
|
|
|
|
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 21:24:38 +02:00
|
|
|
def ingestion_capabilities(self) -> dict[str, Any]:
|
|
|
|
|
service = self.ingestion_service()
|
|
|
|
|
return {
|
|
|
|
|
"connectors": service.connector_capabilities(),
|
|
|
|
|
"extractors": service.extractor_capabilities(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def start_ingestion_job(self, payload: dict[str, Any], context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
service = self.ingestion_service()
|
|
|
|
|
mode = payload.get("mode", "file")
|
|
|
|
|
classification = _optional_classification(payload.get("classification"))
|
|
|
|
|
identity_policy = payload.get("identity_policy", IngestionIdentityPolicy.SOURCE_LOCATION.value)
|
|
|
|
|
if mode == "directory":
|
|
|
|
|
job = service.ingest_directory(
|
|
|
|
|
payload["path"],
|
|
|
|
|
context,
|
|
|
|
|
recursive=bool(payload.get("recursive", True)),
|
|
|
|
|
classification=classification,
|
|
|
|
|
identity_policy=identity_policy,
|
|
|
|
|
skip_unchanged=bool(payload.get("skip_unchanged", True)),
|
|
|
|
|
)
|
|
|
|
|
return _ingestion_job_envelope(job)
|
|
|
|
|
if mode != "file":
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
"Unsupported ingestion mode",
|
|
|
|
|
details={"mode": mode, "supported": ["file", "directory"]},
|
|
|
|
|
)
|
|
|
|
|
result = service.ingest_file(
|
|
|
|
|
payload["path"],
|
|
|
|
|
context,
|
|
|
|
|
asset_id=payload.get("asset_id"),
|
|
|
|
|
title=payload.get("title"),
|
|
|
|
|
classification=classification,
|
|
|
|
|
idempotency_key=payload.get("idempotency_key"),
|
|
|
|
|
identity_policy=identity_policy,
|
|
|
|
|
skip_unchanged=bool(payload.get("skip_unchanged", True)),
|
|
|
|
|
)
|
|
|
|
|
return _ingestion_result_envelope(result)
|
|
|
|
|
|
|
|
|
|
def get_ingestion_job(self, job_id: str) -> dict[str, Any]:
|
|
|
|
|
return _ingestion_job_envelope(self.ingestion_service().get_job(job_id))
|
|
|
|
|
|
|
|
|
|
def list_ingestion_jobs(self, *, status: str | None = None) -> dict[str, Any]:
|
|
|
|
|
parsed_status = _enum_filter(IngestionJobStatus, status, "ingestion job status")
|
|
|
|
|
jobs = self.ingestion_service().list_jobs(status=parsed_status)
|
|
|
|
|
return {"items": [_ingestion_job_envelope(job) for job in jobs], "count": len(jobs)}
|
|
|
|
|
|
|
|
|
|
def refresh_retrieval_index(self) -> dict[str, Any]:
|
|
|
|
|
return self.retrieval_service().refresh_index().to_dict()
|
|
|
|
|
|
|
|
|
|
def query_assets(self, payload: dict[str, Any], context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
service = self.retrieval_service()
|
|
|
|
|
service.refresh_index()
|
|
|
|
|
return service.query_assets(_asset_query_request(payload), context).to_dict()
|
|
|
|
|
|
|
|
|
|
def query_context_entities(self, payload: dict[str, Any], context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
return self.retrieval_service().query_context_entities(
|
|
|
|
|
_context_entity_query_request(payload),
|
|
|
|
|
context,
|
|
|
|
|
).to_dict()
|
|
|
|
|
|
|
|
|
|
def query_relationships(self, payload: dict[str, Any], context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
return self.retrieval_service().query_relationships(
|
|
|
|
|
_relationship_query_request(payload),
|
|
|
|
|
context,
|
|
|
|
|
).to_dict()
|
|
|
|
|
|
|
|
|
|
def record_retrieval_feedback(self, payload: dict[str, Any], context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
return self.retrieval_service().record_feedback(
|
|
|
|
|
RetrievalFeedbackRequest(
|
|
|
|
|
label=payload["label"],
|
|
|
|
|
query=dict(payload.get("query", {})),
|
|
|
|
|
result_ref=dict(payload.get("result_ref", {})),
|
|
|
|
|
notes=payload.get("notes"),
|
|
|
|
|
metadata=dict(payload.get("metadata", {})),
|
|
|
|
|
),
|
|
|
|
|
context,
|
|
|
|
|
).to_dict()
|
|
|
|
|
|
|
|
|
|
def list_retrieval_feedback(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
correlation_id: str | None = None,
|
|
|
|
|
label: str | None = None,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
parsed_label = _enum_filter(RetrievalFeedbackLabel, label, "retrieval feedback label")
|
|
|
|
|
records = self.retrieval_service().list_feedback(correlation_id=correlation_id, label=parsed_label)
|
|
|
|
|
return {"items": [record.to_dict() for record in records], "count": len(records)}
|
|
|
|
|
|
|
|
|
|
def retrieval_quality_metrics(self) -> dict[str, Any]:
|
|
|
|
|
return self.retrieval_service().quality_metrics().to_dict()
|
|
|
|
|
|
|
|
|
|
def list_transformation_operations(self) -> dict[str, Any]:
|
|
|
|
|
operations = self.transformation_service().list_operations()
|
|
|
|
|
return {"items": [operation.to_dict() for operation in operations], "count": len(operations)}
|
|
|
|
|
|
|
|
|
|
def execute_transformation(self, payload: dict[str, Any], context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
result = self.transformation_service().execute_transformation(
|
|
|
|
|
_transformation_request(payload),
|
|
|
|
|
context,
|
|
|
|
|
)
|
|
|
|
|
return _transformation_result_envelope(result)
|
|
|
|
|
|
|
|
|
|
def get_transformation_run(self, run_id: str) -> dict[str, Any]:
|
|
|
|
|
return _transformation_run_envelope(self.transformation_service().get_run(run_id))
|
|
|
|
|
|
|
|
|
|
def list_transformation_runs(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
status: str | None = None,
|
|
|
|
|
operation_id: str | None = None,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
parsed_status = _enum_filter(TransformationRunStatus, status, "transformation run status")
|
|
|
|
|
runs = self.transformation_service().list_runs(status=parsed_status, operation_id=operation_id)
|
|
|
|
|
return {"items": [_transformation_run_envelope(run) for run in runs], "count": len(runs)}
|
|
|
|
|
|
|
|
|
|
def retry_transformation_run(self, run_id: str, context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
return _transformation_result_envelope(self.transformation_service().retry_run(run_id, context))
|
|
|
|
|
|
|
|
|
|
def cancel_transformation_run(
|
|
|
|
|
self,
|
|
|
|
|
run_id: str,
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
run = self.transformation_service().cancel_run(run_id, context, reason=payload.get("reason"))
|
|
|
|
|
return _transformation_run_envelope(run)
|
|
|
|
|
|
|
|
|
|
def register_workflow_template(self, payload: dict[str, Any], context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
return self.workflow_service().register_template(_workflow_template(payload), context).to_dict()
|
|
|
|
|
|
|
|
|
|
def get_workflow_template(self, template_id: str, *, version: str | None = None) -> dict[str, Any]:
|
|
|
|
|
return self.workflow_service().get_template(template_id, version=version).to_dict()
|
|
|
|
|
|
|
|
|
|
def list_workflow_templates(self, *, template_id: str | None = None) -> dict[str, Any]:
|
|
|
|
|
templates = self.workflow_service().list_templates(template_id=template_id)
|
|
|
|
|
return {"items": [template.to_dict() for template in templates], "count": len(templates)}
|
|
|
|
|
|
|
|
|
|
def queue_workflow_run(self, payload: dict[str, Any], context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
return _workflow_result_envelope(
|
|
|
|
|
self.workflow_service().queue_template(_workflow_invocation(payload), context)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def invoke_workflow_run(self, payload: dict[str, Any], context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
return _workflow_result_envelope(
|
|
|
|
|
self.workflow_service().invoke_template(_workflow_invocation(payload), context)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def get_workflow_run(self, run_id: str) -> dict[str, Any]:
|
|
|
|
|
return _workflow_run_envelope(self.repository.get_workflow_run(run_id))
|
|
|
|
|
|
|
|
|
|
def list_workflow_runs(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
status: str | None = None,
|
|
|
|
|
template_id: str | None = None,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
parsed_status = _enum_filter(WorkflowRunStatus, status, "workflow run status")
|
|
|
|
|
runs = self.repository.list_workflow_runs(status=parsed_status, template_id=template_id)
|
|
|
|
|
return {"items": [_workflow_run_envelope(run) for run in runs], "count": len(runs)}
|
|
|
|
|
|
|
|
|
|
def resume_workflow_run(self, run_id: str, context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
return _workflow_result_envelope(self.workflow_service().resume_run(run_id, context))
|
|
|
|
|
|
|
|
|
|
def retry_workflow_run(self, run_id: str, context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
return _workflow_result_envelope(self.workflow_service().retry_run(run_id, context))
|
|
|
|
|
|
|
|
|
|
def cancel_workflow_run(
|
|
|
|
|
self,
|
|
|
|
|
run_id: str,
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
run = self.workflow_service().cancel_run(run_id, context, reason=payload.get("reason"))
|
|
|
|
|
return _workflow_run_envelope(run)
|
|
|
|
|
|
|
|
|
|
def reconstruct_workflow_run(self, run_id: str) -> dict[str, Any]:
|
|
|
|
|
return self.workflow_service().reconstruct_run(run_id).to_dict()
|
|
|
|
|
|
|
|
|
|
def list_workflow_review_tasks(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
status: str | None = WorkflowReviewStatus.OPEN.value,
|
|
|
|
|
workflow_run_id: str | None = None,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
parsed_status = _enum_filter(WorkflowReviewStatus, status, "workflow review status")
|
|
|
|
|
reviews = self.workflow_service().list_review_tasks(
|
|
|
|
|
status=parsed_status,
|
|
|
|
|
workflow_run_id=workflow_run_id,
|
|
|
|
|
)
|
|
|
|
|
return {"items": [review.to_dict() for review in reviews], "count": len(reviews)}
|
|
|
|
|
|
|
|
|
|
def list_workflow_exceptions(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
status: str | None = WorkflowExceptionStatus.OPEN.value,
|
|
|
|
|
kind: str | None = None,
|
|
|
|
|
workflow_run_id: str | None = None,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
parsed_status = _enum_filter(WorkflowExceptionStatus, status, "workflow exception status")
|
|
|
|
|
parsed_kind = _enum_filter(WorkflowExceptionKind, kind, "workflow exception kind")
|
|
|
|
|
exceptions = self.workflow_service().list_exception_queue(
|
|
|
|
|
status=parsed_status,
|
|
|
|
|
kind=parsed_kind,
|
|
|
|
|
workflow_run_id=workflow_run_id,
|
|
|
|
|
)
|
|
|
|
|
return {"items": [exception.to_dict() for exception in exceptions], "count": len(exceptions)}
|
|
|
|
|
|
|
|
|
|
def record_workflow_review_decision(
|
|
|
|
|
self,
|
|
|
|
|
run_id: str,
|
|
|
|
|
review_id: str,
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
decision = payload.get("decision", WorkflowReviewDecisionType.CONTINUE.value)
|
|
|
|
|
return _workflow_result_envelope(
|
|
|
|
|
self.workflow_service().record_review_decision(
|
|
|
|
|
run_id,
|
|
|
|
|
review_id,
|
|
|
|
|
decision,
|
|
|
|
|
context,
|
|
|
|
|
note=payload.get("note", ""),
|
|
|
|
|
correction=dict(payload.get("correction", {})),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def list_agent_operations(self) -> dict[str, Any]:
|
|
|
|
|
return {"items": [dict(item) for item in AGENT_OPERATION_CATALOG], "count": len(AGENT_OPERATION_CATALOG)}
|
|
|
|
|
|
|
|
|
|
def get_agent_operation(self, operation_id: str) -> dict[str, Any]:
|
|
|
|
|
return dict(_agent_operation(operation_id))
|
|
|
|
|
|
|
|
|
|
def execute_agent_operation(
|
|
|
|
|
self,
|
|
|
|
|
operation_id: str,
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
operation = _agent_operation(operation_id)
|
|
|
|
|
dry_run = bool(payload.get("dry_run", False))
|
|
|
|
|
operation_payload = dict(payload.get("payload", payload))
|
|
|
|
|
operation_payload.pop("dry_run", None)
|
|
|
|
|
decision = self._authorize_agent_operation(operation, operation_payload, context)
|
|
|
|
|
effect = decision.effect
|
|
|
|
|
if effect == PolicyEffect.REQUIRE_REVIEW:
|
|
|
|
|
event = self._audit_agent_operation(
|
|
|
|
|
operation,
|
|
|
|
|
AuditOutcome.REVIEW_REQUIRED,
|
|
|
|
|
context,
|
|
|
|
|
decision,
|
|
|
|
|
details={"phase": "review_required", "payload_keys": sorted(operation_payload)},
|
|
|
|
|
)
|
|
|
|
|
return _agent_review_required_envelope(operation, decision, event, context)
|
|
|
|
|
if effect == PolicyEffect.DRY_RUN_ONLY and not dry_run:
|
|
|
|
|
event = self._audit_agent_operation(
|
|
|
|
|
operation,
|
|
|
|
|
AuditOutcome.DRY_RUN,
|
|
|
|
|
context,
|
|
|
|
|
decision,
|
|
|
|
|
details={"phase": "dry_run_required", "payload_keys": sorted(operation_payload)},
|
|
|
|
|
)
|
|
|
|
|
return _agent_dry_run_required_envelope(operation, decision, event, context)
|
|
|
|
|
queued_event = self._audit_agent_operation(
|
|
|
|
|
operation,
|
|
|
|
|
AuditOutcome.DRY_RUN if dry_run else AuditOutcome.SUCCESS,
|
|
|
|
|
context,
|
|
|
|
|
decision,
|
|
|
|
|
details={
|
|
|
|
|
"phase": "accepted",
|
|
|
|
|
"dry_run": dry_run,
|
|
|
|
|
"payload_keys": sorted(operation_payload),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if dry_run:
|
|
|
|
|
return {
|
|
|
|
|
"operation_id": operation_id,
|
|
|
|
|
"dry_run": True,
|
|
|
|
|
"success": True,
|
|
|
|
|
"would_execute": operation,
|
|
|
|
|
"correlation_id": context.correlation_id,
|
|
|
|
|
"policy_decision": decision.to_dict(),
|
|
|
|
|
"audit_event": queued_event.to_dict(),
|
|
|
|
|
}
|
|
|
|
|
try:
|
|
|
|
|
result = self._dispatch_agent_operation(operation_id, operation_payload, context)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
failed_event = self._audit_agent_operation(
|
|
|
|
|
operation,
|
|
|
|
|
AuditOutcome.FAILED,
|
|
|
|
|
context,
|
|
|
|
|
decision,
|
|
|
|
|
details={
|
|
|
|
|
"phase": "failed",
|
|
|
|
|
"error_type": type(exc).__name__,
|
|
|
|
|
"payload_keys": sorted(operation_payload),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if isinstance(exc, KontextualError):
|
|
|
|
|
exc.details.setdefault("agent_audit_event_id", failed_event.event_id)
|
|
|
|
|
raise
|
|
|
|
|
completed_event = self._audit_agent_operation(
|
|
|
|
|
operation,
|
|
|
|
|
AuditOutcome.SUCCESS,
|
|
|
|
|
context,
|
|
|
|
|
decision,
|
|
|
|
|
details={
|
|
|
|
|
"phase": "completed",
|
|
|
|
|
"result_keys": sorted(result) if isinstance(result, dict) else [],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
return {
|
|
|
|
|
"operation_id": operation_id,
|
|
|
|
|
"dry_run": False,
|
|
|
|
|
"success": True,
|
|
|
|
|
"correlation_id": context.correlation_id,
|
|
|
|
|
"result": result,
|
|
|
|
|
"policy_decision": decision.to_dict(),
|
|
|
|
|
"audit_event": completed_event.to_dict(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def _dispatch_agent_operation(
|
|
|
|
|
self,
|
|
|
|
|
operation_id: str,
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
if operation_id == "inspect_asset":
|
|
|
|
|
return self.get_asset(payload["asset_id"])
|
|
|
|
|
if operation_id == "retrieve_asset":
|
|
|
|
|
return self._asset_bundle(payload["asset_id"])
|
|
|
|
|
if operation_id == "search_assets":
|
|
|
|
|
return self.query_assets(dict(payload.get("query", {})), context)
|
|
|
|
|
if operation_id == "assemble_context":
|
|
|
|
|
query_result = self.query_assets(dict(payload.get("query", {})), context)
|
|
|
|
|
return {
|
|
|
|
|
"context_preview": {
|
|
|
|
|
"intent": payload.get("intent", "Support a bounded agent task with source-grounded context."),
|
|
|
|
|
"instructions": payload.get("instructions", ""),
|
|
|
|
|
"constraints": dict(payload.get("constraints", {})),
|
|
|
|
|
"correlation_id": query_result["correlation_id"],
|
|
|
|
|
"source_grounded": True,
|
|
|
|
|
"items": query_result["results"],
|
|
|
|
|
"result_count": query_result["result_count"],
|
|
|
|
|
"total": query_result["total"],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if operation_id == "enrich_metadata":
|
|
|
|
|
return self.add_metadata_record(payload["asset_id"], dict(payload["metadata"]), context)
|
|
|
|
|
if operation_id == "classify_asset":
|
|
|
|
|
request = {
|
|
|
|
|
"operation_id": "classify",
|
|
|
|
|
"source_asset_ids": [payload["asset_id"]],
|
|
|
|
|
"parameters": dict(payload.get("parameters", {})),
|
|
|
|
|
"metadata": dict(payload.get("metadata", {})),
|
|
|
|
|
}
|
|
|
|
|
return self.execute_transformation(request, context)
|
|
|
|
|
if operation_id == "transform_asset":
|
|
|
|
|
return self.execute_transformation(dict(payload["transformation"]), context)
|
|
|
|
|
if operation_id == "invoke_workflow":
|
|
|
|
|
return self.invoke_workflow_run(dict(payload["workflow"]), context)
|
|
|
|
|
if operation_id == "submit_review":
|
|
|
|
|
return self.record_workflow_review_decision(
|
|
|
|
|
payload["run_id"],
|
|
|
|
|
payload["review_id"],
|
|
|
|
|
payload,
|
|
|
|
|
context,
|
|
|
|
|
)
|
|
|
|
|
if operation_id == "report_result":
|
|
|
|
|
return self._agent_report(payload, context)
|
|
|
|
|
raise ValidationError("Unsupported agent operation", details={"operation_id": operation_id})
|
|
|
|
|
|
|
|
|
|
def _authorize_agent_operation(
|
|
|
|
|
self,
|
|
|
|
|
operation: dict[str, Any],
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext,
|
|
|
|
|
) -> PolicyDecision:
|
|
|
|
|
action = f"agent.operation.{operation['operation_id']}"
|
|
|
|
|
resource = f"agent_operation:{operation['operation_id']}"
|
|
|
|
|
try:
|
|
|
|
|
decision = self.policy_gateway.authorize(
|
|
|
|
|
context,
|
|
|
|
|
action,
|
|
|
|
|
resource,
|
|
|
|
|
resource_metadata={
|
|
|
|
|
"operation": operation,
|
|
|
|
|
"payload_keys": sorted(payload),
|
|
|
|
|
"dry_run_supported": operation["dry_run_supported"],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
decision = PolicyDecision.fail_closed(
|
|
|
|
|
context.actor.id,
|
|
|
|
|
action,
|
|
|
|
|
resource,
|
|
|
|
|
reason=str(exc) or "Agent operation policy gateway failed",
|
|
|
|
|
context={"gateway_error": type(exc).__name__},
|
|
|
|
|
)
|
|
|
|
|
if not decision.allowed and decision.effect not in (
|
|
|
|
|
PolicyEffect.REQUIRE_REVIEW,
|
|
|
|
|
PolicyEffect.DRY_RUN_ONLY,
|
|
|
|
|
):
|
|
|
|
|
event = self._audit_agent_operation(operation, AuditOutcome.DENIED, context, decision)
|
|
|
|
|
raise AuthorizationError(
|
|
|
|
|
"Operation denied by policy",
|
|
|
|
|
details={
|
|
|
|
|
"action": action,
|
|
|
|
|
"resource": resource,
|
|
|
|
|
"correlation_id": context.correlation_id,
|
|
|
|
|
"agent_audit_event_id": event.event_id,
|
|
|
|
|
"policy_decision": decision.to_dict(),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
return decision
|
|
|
|
|
|
|
|
|
|
def _audit_agent_operation(
|
|
|
|
|
self,
|
|
|
|
|
operation: dict[str, Any],
|
|
|
|
|
outcome: AuditOutcome,
|
|
|
|
|
context: OperationContext,
|
|
|
|
|
policy_decision: PolicyDecision,
|
|
|
|
|
*,
|
|
|
|
|
details: dict[str, Any] | None = None,
|
|
|
|
|
) -> AuditEvent:
|
|
|
|
|
event = AuditEvent.from_context(
|
|
|
|
|
operation["audit_operation"],
|
|
|
|
|
f"agent_operation:{operation['operation_id']}",
|
|
|
|
|
outcome,
|
|
|
|
|
context,
|
|
|
|
|
policy_decision=policy_decision,
|
|
|
|
|
details=details,
|
|
|
|
|
)
|
|
|
|
|
return self.repository.save_audit_event(event)
|
|
|
|
|
|
|
|
|
|
def _asset_bundle(self, asset_id: str) -> dict[str, Any]:
|
|
|
|
|
asset = self.repository.get_asset(asset_id)
|
|
|
|
|
metadata_records = self.repository.list_metadata_records(asset_id)
|
|
|
|
|
representations = self.repository.list_representations(asset_id=asset_id)
|
|
|
|
|
relationships = self.repository.list_relationships(source_id=asset_id)
|
|
|
|
|
return {
|
|
|
|
|
"asset": asset.to_dict(),
|
|
|
|
|
"metadata_records": [record.to_dict() for record in metadata_records],
|
|
|
|
|
"representations": [representation.to_dict() for representation in representations],
|
|
|
|
|
"relationships": [relationship.to_dict() for relationship in relationships],
|
|
|
|
|
"source_grounded": bool(asset.source_refs or representations),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def _agent_report(self, payload: dict[str, Any], context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
if not payload.get("summary"):
|
|
|
|
|
raise ValidationError("Agent result report requires a summary", details={"required": ["summary"]})
|
|
|
|
|
decision = PolicyDecision.allow(
|
|
|
|
|
context.actor.id,
|
|
|
|
|
"agent.operation.report_result.record",
|
|
|
|
|
f"agent:{context.actor.id}",
|
|
|
|
|
context={"correlation_id": context.correlation_id},
|
|
|
|
|
)
|
|
|
|
|
event = AuditEvent.from_context(
|
|
|
|
|
"agent.report.recorded",
|
|
|
|
|
f"agent:{context.actor.id}",
|
|
|
|
|
AuditOutcome.SUCCESS,
|
|
|
|
|
context,
|
|
|
|
|
policy_decision=decision,
|
|
|
|
|
details={
|
|
|
|
|
"summary": payload["summary"],
|
|
|
|
|
"result_ref": dict(payload.get("result_ref", {})),
|
|
|
|
|
"metadata": dict(payload.get("metadata", {})),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
saved = self.repository.save_audit_event(event)
|
|
|
|
|
return {
|
|
|
|
|
"summary": payload["summary"],
|
|
|
|
|
"result_ref": dict(payload.get("result_ref", {})),
|
|
|
|
|
"metadata": dict(payload.get("metadata", {})),
|
|
|
|
|
"audit_event": saved.to_dict(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def context_package_schema(self) -> dict[str, Any]:
|
|
|
|
|
return {
|
|
|
|
|
"kind": "kontextual.context_package",
|
|
|
|
|
"version": "1",
|
|
|
|
|
"required": ["query"],
|
|
|
|
|
"optional": [
|
|
|
|
|
"package_id",
|
|
|
|
|
"title",
|
|
|
|
|
"intent",
|
|
|
|
|
"instructions",
|
|
|
|
|
"constraints",
|
|
|
|
|
"external_memory_refs",
|
|
|
|
|
"metadata",
|
|
|
|
|
"format",
|
|
|
|
|
],
|
|
|
|
|
"formats": ["kontextual", "markitect"],
|
|
|
|
|
"source_grounding": ["source_refs", "representations", "snippets", "relationships", "metadata_records"],
|
|
|
|
|
"memory_boundary": "external_memory_refs are opaque pointers; memory graph contents are not embedded",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def assemble_context_package(self, payload: dict[str, Any], context: OperationContext) -> dict[str, Any]:
|
|
|
|
|
query = dict(payload.get("query", {}))
|
|
|
|
|
query.setdefault("include_snippets", True)
|
|
|
|
|
query.setdefault("include_relationships", True)
|
|
|
|
|
query.setdefault("max_snippets", 5)
|
|
|
|
|
constraints = dict(payload.get("constraints", {}))
|
|
|
|
|
decision = self._authorize_context_package(query, constraints, context)
|
|
|
|
|
query_result = self.query_assets(query, context)
|
|
|
|
|
package = _context_package_payload(
|
|
|
|
|
payload,
|
|
|
|
|
query_result,
|
|
|
|
|
context,
|
|
|
|
|
constraints=constraints,
|
|
|
|
|
)
|
|
|
|
|
event = self._audit_context_package(package, context, decision)
|
|
|
|
|
package["audit_event"] = event.to_dict()
|
|
|
|
|
package["policy_decision"] = decision.to_dict()
|
|
|
|
|
return package
|
|
|
|
|
|
|
|
|
|
def _authorize_context_package(
|
|
|
|
|
self,
|
|
|
|
|
query: dict[str, Any],
|
|
|
|
|
constraints: dict[str, Any],
|
|
|
|
|
context: OperationContext,
|
|
|
|
|
) -> PolicyDecision:
|
|
|
|
|
try:
|
|
|
|
|
decision = self.policy_gateway.authorize(
|
|
|
|
|
context,
|
|
|
|
|
"context_package.assemble",
|
|
|
|
|
"context_package:new",
|
|
|
|
|
resource_metadata={
|
|
|
|
|
"query": query,
|
|
|
|
|
"constraints": constraints,
|
|
|
|
|
"external_memory_refs": "opaque",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
decision = PolicyDecision.fail_closed(
|
|
|
|
|
context.actor.id,
|
|
|
|
|
"context_package.assemble",
|
|
|
|
|
"context_package:new",
|
|
|
|
|
reason=str(exc) or "Context package policy gateway failed",
|
|
|
|
|
context={"gateway_error": type(exc).__name__},
|
|
|
|
|
)
|
|
|
|
|
if not decision.allowed:
|
|
|
|
|
event = AuditEvent.from_context(
|
|
|
|
|
"context_package.assemble",
|
|
|
|
|
"context_package:new",
|
|
|
|
|
AuditOutcome.DENIED,
|
|
|
|
|
context,
|
|
|
|
|
policy_decision=decision,
|
|
|
|
|
details={"query": query, "constraints": constraints},
|
|
|
|
|
)
|
|
|
|
|
saved = self.repository.save_audit_event(event)
|
|
|
|
|
raise AuthorizationError(
|
|
|
|
|
"Operation denied by policy",
|
|
|
|
|
details={
|
|
|
|
|
"action": "context_package.assemble",
|
|
|
|
|
"resource": "context_package:new",
|
|
|
|
|
"correlation_id": context.correlation_id,
|
|
|
|
|
"audit_event_id": saved.event_id,
|
|
|
|
|
"policy_decision": decision.to_dict(),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
return decision
|
|
|
|
|
|
|
|
|
|
def _audit_context_package(
|
|
|
|
|
self,
|
|
|
|
|
package: dict[str, Any],
|
|
|
|
|
context: OperationContext,
|
|
|
|
|
decision: PolicyDecision,
|
|
|
|
|
) -> AuditEvent:
|
|
|
|
|
event = AuditEvent.from_context(
|
|
|
|
|
"context_package.assemble",
|
|
|
|
|
f"context_package:{package['package_id']}",
|
|
|
|
|
AuditOutcome.SUCCESS,
|
|
|
|
|
context,
|
|
|
|
|
policy_decision=decision,
|
|
|
|
|
details={
|
|
|
|
|
"result_count": package["result_count"],
|
|
|
|
|
"source_grounded": package["source_grounded"],
|
|
|
|
|
"format": package["format"],
|
|
|
|
|
"external_memory_ref_count": len(package["external_memory_refs"]),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
return self.repository.save_audit_event(event)
|
|
|
|
|
|
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 21:24:38 +02:00
|
|
|
from fastapi.responses import JSONResponse
|
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
|
|
|
|
|
|
2026-05-06 21:24:38 +02:00
|
|
|
@app.exception_handler(NotFoundError)
|
|
|
|
|
async def not_found_error_handler(_request, exc: NotFoundError) -> JSONResponse:
|
|
|
|
|
return JSONResponse(status_code=404, content=_error_payload(exc))
|
|
|
|
|
|
|
|
|
|
@app.exception_handler(AuthorizationError)
|
|
|
|
|
async def authorization_error_handler(_request, exc: AuthorizationError) -> JSONResponse:
|
|
|
|
|
return JSONResponse(status_code=403, content=_authorization_error_payload(exc))
|
|
|
|
|
|
|
|
|
|
@app.exception_handler(ValidationError)
|
|
|
|
|
async def validation_error_handler(_request, exc: ValidationError) -> JSONResponse:
|
|
|
|
|
return JSONResponse(status_code=422, content=_error_payload(exc))
|
|
|
|
|
|
|
|
|
|
@app.exception_handler(KontextualError)
|
|
|
|
|
async def kontextual_error_handler(_request, exc: KontextualError) -> JSONResponse:
|
|
|
|
|
return JSONResponse(status_code=400, content=_error_payload(exc))
|
|
|
|
|
|
2026-05-06 19:30:49 +02:00
|
|
|
@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(
|
2026-05-06 21:24:38 +02:00
|
|
|
x_actor_id: str | None = Header(None),
|
|
|
|
|
x_actor_type: str | None = Header(None),
|
2026-05-06 20:39:29 +02:00
|
|
|
x_actor_display_name: str | None = Header(None),
|
2026-05-06 21:24:38 +02:00
|
|
|
x_actor_external_ref: str | None = Header(None),
|
|
|
|
|
x_actor_groups: str | None = Header(None),
|
2026-05-06 20:39:29 +02:00
|
|
|
x_correlation_id: str | None = Header(None),
|
2026-05-06 21:24:38 +02:00
|
|
|
x_delegated_actor_id: str | None = Header(None),
|
|
|
|
|
x_delegated_actor_type: str | None = Header(None),
|
|
|
|
|
x_delegated_actor_display_name: str | None = Header(None),
|
|
|
|
|
x_delegated_actor_external_ref: str | None = Header(None),
|
|
|
|
|
x_delegated_actor_groups: str | None = Header(None),
|
|
|
|
|
x_agent_id: str | None = Header(None),
|
|
|
|
|
x_agent_name: str | None = Header(None),
|
|
|
|
|
x_agent_run_id: str | None = Header(None),
|
|
|
|
|
x_agent_tool: str | None = Header(None),
|
|
|
|
|
x_request_scope: str | None = Header(None),
|
|
|
|
|
x_policy_scope: str | None = Header(None),
|
2026-05-06 20:39:29 +02:00
|
|
|
) -> OperationContext:
|
2026-05-06 21:24:38 +02:00
|
|
|
actor_id = x_actor_id or x_agent_id or "api-user"
|
|
|
|
|
actor_type = x_actor_type or ("ai_agent" if x_agent_id else "human")
|
2026-05-06 20:39:29 +02:00
|
|
|
return runtime.operation_context(
|
2026-05-06 21:24:38 +02:00
|
|
|
actor_id=actor_id,
|
|
|
|
|
actor_type=actor_type,
|
2026-05-06 20:39:29 +02:00
|
|
|
display_name=x_actor_display_name,
|
2026-05-06 21:24:38 +02:00
|
|
|
external_ref=x_actor_external_ref,
|
2026-05-06 20:39:29 +02:00
|
|
|
correlation_id=x_correlation_id,
|
2026-05-06 21:24:38 +02:00
|
|
|
groups=_split_header_list(x_actor_groups),
|
|
|
|
|
delegated_actor_id=x_delegated_actor_id,
|
|
|
|
|
delegated_actor_type=x_delegated_actor_type or "human",
|
|
|
|
|
delegated_actor_display_name=x_delegated_actor_display_name,
|
|
|
|
|
delegated_actor_external_ref=x_delegated_actor_external_ref,
|
|
|
|
|
delegated_actor_groups=_split_header_list(x_delegated_actor_groups),
|
|
|
|
|
request_scope=_json_header(x_request_scope, "X-Request-Scope"),
|
|
|
|
|
policy_scope=_json_header(x_policy_scope, "X-Policy-Scope"),
|
|
|
|
|
agent_id=x_agent_id,
|
|
|
|
|
agent_name=x_agent_name,
|
|
|
|
|
agent_run_id=x_agent_run_id,
|
|
|
|
|
agent_tool=x_agent_tool,
|
2026-05-06 20:39:29 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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:
|
2026-05-06 21:24:38 +02:00
|
|
|
raise HTTPException(status_code=403, detail=_authorization_error_payload(exc)) from exc
|
2026-05-06 20:39:29 +02:00
|
|
|
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
|
2026-05-06 21:24:38 +02:00
|
|
|
except (KeyError, TypeError, ValueError) as exc:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=422,
|
|
|
|
|
detail={
|
|
|
|
|
"code": "kontextual.validation",
|
|
|
|
|
"message": "Invalid request payload",
|
|
|
|
|
"details": {"error_type": type(exc).__name__, "message": str(exc)},
|
|
|
|
|
},
|
|
|
|
|
) from exc
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/context", tags=["context"])
|
|
|
|
|
def current_context(
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return context.to_dict()
|
2026-05-06 20:39:29 +02:00
|
|
|
|
|
|
|
|
@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 21:24:38 +02:00
|
|
|
@app.get(f"{prefix}/ingestion/capabilities", tags=["ingestion"])
|
|
|
|
|
def ingestion_capabilities() -> dict[str, Any]:
|
|
|
|
|
return response(runtime.ingestion_capabilities)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/ingestion/jobs", tags=["ingestion"])
|
|
|
|
|
def start_ingestion_job(
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.start_ingestion_job, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/ingestion/jobs", tags=["ingestion"])
|
|
|
|
|
def list_ingestion_jobs(status: str | None = Query(None)) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.list_ingestion_jobs, status=status)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/ingestion/jobs/{{job_id}}", tags=["ingestion"])
|
|
|
|
|
def get_ingestion_job(job_id: str) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.get_ingestion_job, job_id)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/retrieval/index/refresh", tags=["retrieval"])
|
|
|
|
|
def refresh_retrieval_index() -> dict[str, Any]:
|
|
|
|
|
return response(runtime.refresh_retrieval_index)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/retrieval/assets", tags=["retrieval"])
|
|
|
|
|
def query_assets(
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.query_assets, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/retrieval/context-entities", tags=["retrieval"])
|
|
|
|
|
def query_context_entities(
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.query_context_entities, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/retrieval/relationships", tags=["retrieval"])
|
|
|
|
|
def query_retrieval_relationships(
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.query_relationships, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/retrieval/feedback", tags=["retrieval"])
|
|
|
|
|
def record_retrieval_feedback(
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.record_retrieval_feedback, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/retrieval/feedback", tags=["retrieval"])
|
|
|
|
|
def list_retrieval_feedback(
|
|
|
|
|
correlation_id: str | None = Query(None),
|
|
|
|
|
label: str | None = Query(None),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.list_retrieval_feedback, correlation_id=correlation_id, label=label)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/retrieval/quality", tags=["retrieval"])
|
|
|
|
|
def retrieval_quality_metrics() -> dict[str, Any]:
|
|
|
|
|
return response(runtime.retrieval_quality_metrics)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/transformations/operations", tags=["transformations"])
|
|
|
|
|
def list_transformation_operations() -> dict[str, Any]:
|
|
|
|
|
return response(runtime.list_transformation_operations)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/transformations/runs", tags=["transformations"])
|
|
|
|
|
def execute_transformation(
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.execute_transformation, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/transformations/runs", tags=["transformations"])
|
|
|
|
|
def list_transformation_runs(
|
|
|
|
|
status: str | None = Query(None),
|
|
|
|
|
operation_id: str | None = Query(None),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.list_transformation_runs, status=status, operation_id=operation_id)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/transformations/runs/{{run_id}}", tags=["transformations"])
|
|
|
|
|
def get_transformation_run(run_id: str) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.get_transformation_run, run_id)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/transformations/runs/{{run_id}}/retry", tags=["transformations"])
|
|
|
|
|
def retry_transformation_run(
|
|
|
|
|
run_id: str,
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.retry_transformation_run, run_id, context)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/transformations/runs/{{run_id}}/cancel", tags=["transformations"])
|
|
|
|
|
def cancel_transformation_run(
|
|
|
|
|
run_id: str,
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.cancel_transformation_run, run_id, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/workflows/templates", tags=["workflows"])
|
|
|
|
|
def register_workflow_template(
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.register_workflow_template, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/workflows/templates", tags=["workflows"])
|
|
|
|
|
def list_workflow_templates(template_id: str | None = Query(None)) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.list_workflow_templates, template_id=template_id)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/workflows/templates/{{template_id}}", tags=["workflows"])
|
|
|
|
|
def get_workflow_template(
|
|
|
|
|
template_id: str,
|
|
|
|
|
version: str | None = Query(None),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.get_workflow_template, template_id, version=version)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/workflows/runs", tags=["workflows"])
|
|
|
|
|
def invoke_workflow_run(
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.invoke_workflow_run, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/workflows/runs/queue", tags=["workflows"])
|
|
|
|
|
def queue_workflow_run(
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.queue_workflow_run, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/workflows/runs", tags=["workflows"])
|
|
|
|
|
def list_workflow_runs(
|
|
|
|
|
status: str | None = Query(None),
|
|
|
|
|
template_id: str | None = Query(None),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.list_workflow_runs, status=status, template_id=template_id)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/workflows/runs/{{run_id}}", tags=["workflows"])
|
|
|
|
|
def get_workflow_run(run_id: str) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.get_workflow_run, run_id)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/workflows/runs/{{run_id}}/resume", tags=["workflows"])
|
|
|
|
|
def resume_workflow_run(
|
|
|
|
|
run_id: str,
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.resume_workflow_run, run_id, context)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/workflows/runs/{{run_id}}/retry", tags=["workflows"])
|
|
|
|
|
def retry_workflow_run(
|
|
|
|
|
run_id: str,
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.retry_workflow_run, run_id, context)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/workflows/runs/{{run_id}}/cancel", tags=["workflows"])
|
|
|
|
|
def cancel_workflow_run(
|
|
|
|
|
run_id: str,
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.cancel_workflow_run, run_id, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/workflows/runs/{{run_id}}/reconstruction", tags=["workflows"])
|
|
|
|
|
def reconstruct_workflow_run(run_id: str) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.reconstruct_workflow_run, run_id)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/workflows/reviews", tags=["workflows"])
|
|
|
|
|
def list_workflow_review_tasks(
|
|
|
|
|
status: str | None = Query(WorkflowReviewStatus.OPEN.value),
|
|
|
|
|
workflow_run_id: str | None = Query(None),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.list_workflow_review_tasks, status=status, workflow_run_id=workflow_run_id)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/workflows/exceptions", tags=["workflows"])
|
|
|
|
|
def list_workflow_exceptions(
|
|
|
|
|
status: str | None = Query(WorkflowExceptionStatus.OPEN.value),
|
|
|
|
|
kind: str | None = Query(None),
|
|
|
|
|
workflow_run_id: str | None = Query(None),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(
|
|
|
|
|
runtime.list_workflow_exceptions,
|
|
|
|
|
status=status,
|
|
|
|
|
kind=kind,
|
|
|
|
|
workflow_run_id=workflow_run_id,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/workflows/runs/{{run_id}}/reviews/{{review_id}}/decision", tags=["workflows"])
|
|
|
|
|
def record_workflow_review_decision(
|
|
|
|
|
run_id: str,
|
|
|
|
|
review_id: str,
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.record_workflow_review_decision, run_id, review_id, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/agents/operations", tags=["agents"])
|
|
|
|
|
def list_agent_operations() -> dict[str, Any]:
|
|
|
|
|
return response(runtime.list_agent_operations)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/agents/operations/{{operation_id}}", tags=["agents"])
|
|
|
|
|
def get_agent_operation(operation_id: str) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.get_agent_operation, operation_id)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/agents/operations/{{operation_id}}", tags=["agents"])
|
|
|
|
|
def execute_agent_operation(
|
|
|
|
|
operation_id: str,
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.execute_agent_operation, operation_id, payload, context)
|
|
|
|
|
|
|
|
|
|
@app.get(f"{prefix}/context-packages/schema", tags=["context-packages"])
|
|
|
|
|
def context_package_schema() -> dict[str, Any]:
|
|
|
|
|
return response(runtime.context_package_schema)
|
|
|
|
|
|
|
|
|
|
@app.post(f"{prefix}/context-packages", tags=["context-packages"])
|
|
|
|
|
def assemble_context_package(
|
|
|
|
|
payload: dict[str, Any],
|
|
|
|
|
context: OperationContext = Depends(context_from_headers),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return response(runtime.assemble_context_package, payload, context)
|
|
|
|
|
|
2026-05-06 19:30:49 +02:00
|
|
|
return app
|
2026-05-06 20:39:29 +02:00
|
|
|
|
|
|
|
|
|
2026-05-06 21:24:38 +02:00
|
|
|
def _agent_metadata(
|
|
|
|
|
*,
|
|
|
|
|
agent_id: str | None,
|
|
|
|
|
agent_name: str | None,
|
|
|
|
|
agent_run_id: str | None,
|
|
|
|
|
agent_tool: str | None,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return {
|
|
|
|
|
key: value
|
|
|
|
|
for key, value in {
|
|
|
|
|
"agent_id": agent_id,
|
|
|
|
|
"agent_name": agent_name,
|
|
|
|
|
"agent_run_id": agent_run_id,
|
|
|
|
|
"agent_tool": agent_tool,
|
|
|
|
|
}.items()
|
|
|
|
|
if value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _agent_operation(operation_id: str) -> dict[str, Any]:
|
|
|
|
|
for operation in AGENT_OPERATION_CATALOG:
|
|
|
|
|
if operation["operation_id"] == operation_id:
|
|
|
|
|
return operation
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
"Unsupported agent operation",
|
|
|
|
|
details={
|
|
|
|
|
"operation_id": operation_id,
|
|
|
|
|
"supported": [operation["operation_id"] for operation in AGENT_OPERATION_CATALOG],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _agent_review_required_envelope(
|
|
|
|
|
operation: dict[str, Any],
|
|
|
|
|
decision: PolicyDecision,
|
|
|
|
|
event: AuditEvent,
|
|
|
|
|
context: OperationContext,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return {
|
|
|
|
|
"operation_id": operation["operation_id"],
|
|
|
|
|
"dry_run": False,
|
|
|
|
|
"success": False,
|
|
|
|
|
"review_required": True,
|
|
|
|
|
"correlation_id": context.correlation_id,
|
|
|
|
|
"review": {
|
|
|
|
|
"reason": decision.reason,
|
|
|
|
|
"obligations": dict(decision.obligations),
|
|
|
|
|
"required_permissions": list(operation["required_permissions"]),
|
|
|
|
|
},
|
|
|
|
|
"policy_decision": decision.to_dict(),
|
|
|
|
|
"audit_event": event.to_dict(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _agent_dry_run_required_envelope(
|
|
|
|
|
operation: dict[str, Any],
|
|
|
|
|
decision: PolicyDecision,
|
|
|
|
|
event: AuditEvent,
|
|
|
|
|
context: OperationContext,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return {
|
|
|
|
|
"operation_id": operation["operation_id"],
|
|
|
|
|
"dry_run": False,
|
|
|
|
|
"success": False,
|
|
|
|
|
"dry_run_required": True,
|
|
|
|
|
"correlation_id": context.correlation_id,
|
|
|
|
|
"reason": decision.reason,
|
|
|
|
|
"policy_decision": decision.to_dict(),
|
|
|
|
|
"audit_event": event.to_dict(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _context_package_payload(
|
|
|
|
|
request_payload: dict[str, Any],
|
|
|
|
|
query_result: dict[str, Any],
|
|
|
|
|
context: OperationContext,
|
|
|
|
|
*,
|
|
|
|
|
constraints: dict[str, Any],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
package_format = request_payload.get("format", "kontextual")
|
|
|
|
|
if package_format not in {"kontextual", "markitect"}:
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
"Unsupported context package format",
|
|
|
|
|
details={"format": package_format, "supported": ["kontextual", "markitect"]},
|
|
|
|
|
)
|
|
|
|
|
items = [_context_package_item(item) for item in query_result.get("results", ())]
|
|
|
|
|
package = {
|
|
|
|
|
"kind": "kontextual.context_package",
|
|
|
|
|
"version": "1",
|
|
|
|
|
"package_id": request_payload.get("package_id") or new_id("ctxpkg"),
|
|
|
|
|
"title": request_payload.get("title", "Kontextual Context Package"),
|
|
|
|
|
"intent": request_payload.get("intent", "Provide bounded, source-grounded context."),
|
|
|
|
|
"instructions": request_payload.get("instructions", ""),
|
|
|
|
|
"format": package_format,
|
|
|
|
|
"correlation_id": context.correlation_id,
|
|
|
|
|
"query": query_result.get("query", {}),
|
|
|
|
|
"result_count": query_result.get("result_count", len(items)),
|
|
|
|
|
"total": query_result.get("total", len(items)),
|
|
|
|
|
"source_grounded": bool(items) and all(item["source_refs"] or item["snippets"] for item in items),
|
|
|
|
|
"policy_constraints": constraints,
|
|
|
|
|
"external_memory_refs": [
|
|
|
|
|
_opaque_memory_ref(item) for item in request_payload.get("external_memory_refs", ())
|
|
|
|
|
],
|
|
|
|
|
"items": items,
|
|
|
|
|
"metadata": dict(request_payload.get("metadata", {})),
|
|
|
|
|
}
|
|
|
|
|
if package_format == "markitect":
|
|
|
|
|
package["markitect_payload"] = _markitect_context_payload(package)
|
|
|
|
|
return package
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _context_package_item(result: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
return {
|
|
|
|
|
"asset_id": result["asset_id"],
|
|
|
|
|
"title": result.get("title"),
|
|
|
|
|
"classification": dict(result.get("classification", {})),
|
|
|
|
|
"lifecycle": result.get("lifecycle"),
|
|
|
|
|
"source_refs": list(result.get("source_refs", ())),
|
|
|
|
|
"snippets": list(result.get("snippets", ())),
|
|
|
|
|
"metadata_records": list(result.get("metadata_records", ())),
|
|
|
|
|
"relationships": list(result.get("relationships", ())),
|
|
|
|
|
"representations": [
|
|
|
|
|
{
|
|
|
|
|
"representation_id": item.get("representation_id"),
|
|
|
|
|
"kind": item.get("kind"),
|
|
|
|
|
"media_type": item.get("media_type"),
|
|
|
|
|
"source_ref_id": item.get("source_ref_id"),
|
|
|
|
|
"storage_ref": item.get("storage_ref"),
|
|
|
|
|
"producer": item.get("producer"),
|
|
|
|
|
"metadata": {
|
|
|
|
|
key: value
|
|
|
|
|
for key, value in dict(item.get("metadata", {})).items()
|
|
|
|
|
if key in {"extractor", "producer", "normalized_hash", "search_text_length"}
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
for item in result.get("representations", ())
|
|
|
|
|
],
|
|
|
|
|
"relevance": dict(result.get("relevance", {})),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _opaque_memory_ref(data: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
ref_id = data.get("ref_id") or data.get("id") or data.get("uri")
|
|
|
|
|
if not ref_id:
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
"External memory reference requires ref_id, id, or uri",
|
|
|
|
|
details={"required": ["ref_id"]},
|
|
|
|
|
)
|
|
|
|
|
return {
|
|
|
|
|
"ref_id": str(ref_id),
|
|
|
|
|
"system": data.get("system", "phase-memory"),
|
|
|
|
|
"kind": data.get("kind", "memory_ref"),
|
|
|
|
|
"opaque": True,
|
|
|
|
|
"metadata": dict(data.get("metadata", {})),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _markitect_context_payload(package: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
return {
|
|
|
|
|
"kind": "markitect.context_package",
|
|
|
|
|
"version": "1",
|
|
|
|
|
"id": package["package_id"],
|
|
|
|
|
"title": package["title"],
|
|
|
|
|
"intent": package["intent"],
|
|
|
|
|
"instructions": package["instructions"],
|
|
|
|
|
"policy_constraints": dict(package["policy_constraints"]),
|
|
|
|
|
"external_memory_refs": list(package["external_memory_refs"]),
|
|
|
|
|
"items": [
|
|
|
|
|
{
|
|
|
|
|
"asset_id": item["asset_id"],
|
|
|
|
|
"title": item["title"],
|
|
|
|
|
"source_refs": list(item["source_refs"]),
|
|
|
|
|
"snippets": list(item["snippets"]),
|
|
|
|
|
"metadata_records": list(item["metadata_records"]),
|
|
|
|
|
"relationships": list(item["relationships"]),
|
|
|
|
|
}
|
|
|
|
|
for item in package["items"]
|
|
|
|
|
],
|
|
|
|
|
"adapter_boundary": "markdown rendering and selector semantics are delegated to markitect-tool",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _split_header_list(value: str | None) -> list[str] | None:
|
|
|
|
|
if value is None:
|
|
|
|
|
return None
|
|
|
|
|
return [item.strip() for item in value.split(",") if item.strip()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _json_header(value: str | None, header_name: str) -> dict[str, Any] | None:
|
|
|
|
|
if value is None or not value.strip():
|
|
|
|
|
return None
|
|
|
|
|
try:
|
|
|
|
|
parsed = json.loads(value)
|
|
|
|
|
except json.JSONDecodeError as exc:
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
"Header must contain a JSON object",
|
|
|
|
|
details={"header": header_name, "message": str(exc)},
|
|
|
|
|
) from exc
|
|
|
|
|
if not isinstance(parsed, dict):
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
"Header must contain a JSON object",
|
|
|
|
|
details={"header": header_name, "actual_type": type(parsed).__name__},
|
|
|
|
|
)
|
|
|
|
|
return parsed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _optional_classification(data: dict[str, Any] | None) -> Classification | None:
|
|
|
|
|
return Classification.from_dict(data) if data else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _enum_filter(enum_type: Any, value: Any, label: str) -> Any:
|
|
|
|
|
if value is None or isinstance(value, enum_type):
|
|
|
|
|
return value
|
|
|
|
|
try:
|
|
|
|
|
return enum_type(value)
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
f"Unsupported {label}",
|
|
|
|
|
details={"value": value, "supported": [item.value for item in enum_type]},
|
|
|
|
|
) from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _dataclass_request(request_type: Any, data: dict[str, Any]) -> Any:
|
|
|
|
|
try:
|
|
|
|
|
return request_type(**data)
|
|
|
|
|
except TypeError as exc:
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
"Invalid request payload",
|
|
|
|
|
details={"request_type": request_type.__name__, "message": str(exc)},
|
|
|
|
|
) from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _asset_query_request(data: dict[str, Any]) -> AssetQueryRequest:
|
|
|
|
|
payload = dict(data)
|
|
|
|
|
if "tags" in payload:
|
|
|
|
|
payload["tags"] = tuple(payload["tags"])
|
|
|
|
|
return _dataclass_request(AssetQueryRequest, payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _context_entity_query_request(data: dict[str, Any]) -> ContextEntityQueryRequest:
|
|
|
|
|
return _dataclass_request(ContextEntityQueryRequest, dict(data))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _relationship_query_request(data: dict[str, Any]) -> RelationshipQueryRequest:
|
|
|
|
|
return _dataclass_request(RelationshipQueryRequest, dict(data))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _transformation_request(data: dict[str, Any]) -> TransformationRequest:
|
|
|
|
|
payload = dict(data)
|
|
|
|
|
if "source_asset_ids" in payload:
|
|
|
|
|
payload["source_asset_ids"] = tuple(payload["source_asset_ids"])
|
|
|
|
|
if "parameters" in payload:
|
|
|
|
|
payload["parameters"] = dict(payload["parameters"])
|
|
|
|
|
if "metadata" in payload:
|
|
|
|
|
payload["metadata"] = dict(payload["metadata"])
|
|
|
|
|
return _dataclass_request(TransformationRequest, payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _workflow_template(data: dict[str, Any]) -> WorkflowTemplate:
|
|
|
|
|
if "created_at" in data and "updated_at" in data and "template_id" in data:
|
|
|
|
|
return WorkflowTemplate.from_dict(data)
|
|
|
|
|
kwargs: dict[str, Any] = {
|
|
|
|
|
"name": data["name"],
|
|
|
|
|
"version": data.get("version", "1"),
|
|
|
|
|
"description": data.get("description", ""),
|
|
|
|
|
"inputs": tuple(WorkflowInputDefinition.from_dict(item) for item in data.get("inputs", ())),
|
|
|
|
|
"steps": tuple(WorkflowStepDefinition.from_dict(item) for item in data.get("steps", ())),
|
|
|
|
|
"policy_checks": tuple(dict(item) for item in data.get("policy_checks", ())),
|
|
|
|
|
"failure_behavior": data.get("failure_behavior", "fail_workflow"),
|
|
|
|
|
"metadata": dict(data.get("metadata", {})),
|
|
|
|
|
}
|
|
|
|
|
for key in ("template_id", "created_by"):
|
|
|
|
|
if data.get(key) is not None:
|
|
|
|
|
kwargs[key] = data[key]
|
|
|
|
|
return WorkflowTemplate(**kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _workflow_invocation(data: dict[str, Any]) -> WorkflowInvocation:
|
|
|
|
|
return WorkflowInvocation(
|
|
|
|
|
template_id=data["template_id"],
|
|
|
|
|
template_version=data.get("template_version"),
|
|
|
|
|
inputs=dict(data.get("inputs", {})),
|
|
|
|
|
metadata=dict(data.get("metadata", {})),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ingestion_result_envelope(result: Any) -> dict[str, Any]:
|
|
|
|
|
payload = _ingestion_job_envelope(result.job)
|
|
|
|
|
payload["action"] = result.action
|
|
|
|
|
payload["asset"] = result.asset.to_dict() if result.asset else None
|
|
|
|
|
payload["asset_change"] = _asset_change_result(result.asset_change) if result.asset_change else None
|
|
|
|
|
return payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ingestion_job_envelope(job: Any) -> dict[str, Any]:
|
|
|
|
|
data = job.to_dict()
|
|
|
|
|
retry_options = _ingestion_retry_options(job)
|
|
|
|
|
data["retry_options"] = retry_options
|
|
|
|
|
return {
|
|
|
|
|
"job_id": job.job_id,
|
|
|
|
|
"status": job.status.value,
|
|
|
|
|
"correlation_id": job.correlation_id,
|
|
|
|
|
"output_asset_ids": list(job.output_asset_ids),
|
|
|
|
|
"failures": [failure.to_dict() for failure in job.failures],
|
|
|
|
|
"retry_options": retry_options,
|
|
|
|
|
"job": data,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ingestion_retry_options(job: Any) -> dict[str, Any]:
|
|
|
|
|
if job.retry_options:
|
|
|
|
|
return dict(job.retry_options)
|
|
|
|
|
return {
|
|
|
|
|
"retryable": any(failure.retriable for failure in job.failures),
|
|
|
|
|
"retryable_failure_codes": [failure.code for failure in job.failures if failure.retriable],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _transformation_result_envelope(result: Any) -> dict[str, Any]:
|
|
|
|
|
payload = result.to_dict()
|
|
|
|
|
if result.run is not None:
|
|
|
|
|
payload["run"] = _transformation_run_envelope(result.run)
|
|
|
|
|
payload["correlation_id"] = result.run.correlation_id
|
|
|
|
|
payload["retry_options"] = payload["run"]["retry_options"]
|
|
|
|
|
return payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _transformation_run_envelope(run: Any) -> dict[str, Any]:
|
|
|
|
|
data = run.to_dict()
|
|
|
|
|
retryable = run.status in (TransformationRunStatus.FAILED, TransformationRunStatus.CANCELED)
|
|
|
|
|
cancelable = run.status in (TransformationRunStatus.QUEUED, TransformationRunStatus.RUNNING)
|
|
|
|
|
data["retry_options"] = {
|
|
|
|
|
"retryable": retryable,
|
|
|
|
|
"retry_endpoint": f"/api/v1/transformations/runs/{run.run_id}/retry" if retryable else None,
|
|
|
|
|
"cancelable": cancelable,
|
|
|
|
|
"cancel_endpoint": f"/api/v1/transformations/runs/{run.run_id}/cancel" if cancelable else None,
|
|
|
|
|
}
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _workflow_result_envelope(result: Any) -> dict[str, Any]:
|
|
|
|
|
payload = result.to_dict()
|
|
|
|
|
payload["run"] = _workflow_run_envelope(result.run)
|
|
|
|
|
payload["correlation_id"] = result.run.correlation_id
|
|
|
|
|
payload["retry_options"] = payload["run"]["retry_options"]
|
|
|
|
|
return payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _workflow_run_envelope(run: Any) -> dict[str, Any]:
|
|
|
|
|
data = run.to_dict()
|
|
|
|
|
retryable = run.status in (
|
|
|
|
|
WorkflowRunStatus.FAILED,
|
|
|
|
|
WorkflowRunStatus.CANCELED,
|
|
|
|
|
WorkflowRunStatus.PARTIALLY_COMPLETED,
|
|
|
|
|
)
|
|
|
|
|
cancelable = run.status in (WorkflowRunStatus.QUEUED, WorkflowRunStatus.RUNNING, WorkflowRunStatus.WAITING)
|
|
|
|
|
data["retry_options"] = {
|
|
|
|
|
"retryable": retryable,
|
|
|
|
|
"retry_endpoint": f"/api/v1/workflows/runs/{run.run_id}/retry" if retryable else None,
|
|
|
|
|
"cancelable": cancelable,
|
|
|
|
|
"cancel_endpoint": f"/api/v1/workflows/runs/{run.run_id}/cancel" if cancelable else None,
|
|
|
|
|
}
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
}
|
2026-05-06 21:24:38 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _authorization_error_payload(error: AuthorizationError) -> dict[str, Any]:
|
|
|
|
|
payload = _error_payload(error)
|
|
|
|
|
details = dict(payload.get("details", {}))
|
|
|
|
|
decision = details.get("policy_decision")
|
|
|
|
|
if isinstance(decision, dict):
|
|
|
|
|
details["policy_decision"] = _public_policy_decision(decision)
|
|
|
|
|
payload["details"] = details
|
|
|
|
|
return payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _public_policy_decision(decision: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
allowed_fields = {
|
|
|
|
|
"decision_id",
|
|
|
|
|
"effect",
|
|
|
|
|
"subject_id",
|
|
|
|
|
"action",
|
|
|
|
|
"resource",
|
|
|
|
|
"reason",
|
|
|
|
|
"obligations",
|
|
|
|
|
"decided_at",
|
|
|
|
|
}
|
|
|
|
|
public = {key: value for key, value in decision.items() if key in allowed_fields}
|
|
|
|
|
context = decision.get("context")
|
|
|
|
|
if isinstance(context, dict):
|
|
|
|
|
public_context = {
|
|
|
|
|
key: value
|
|
|
|
|
for key, value in context.items()
|
|
|
|
|
if key not in {"resource_metadata", "protected_metadata", "source_payload"}
|
|
|
|
|
}
|
|
|
|
|
if public_context:
|
|
|
|
|
public["context"] = public_context
|
|
|
|
|
return public
|