generated from coulomb/repo-seed
transformation registry, transformation runs, and derived artifact lineage
This commit is contained in:
@@ -23,6 +23,15 @@ from .retrieval_service import (
|
||||
RetrievalQualityMetrics,
|
||||
RetrievalSnippet,
|
||||
)
|
||||
from .transformation_service import (
|
||||
TransformationExecutionContext,
|
||||
TransformationOperationRegistry,
|
||||
TransformationOutput,
|
||||
TransformationRequest,
|
||||
TransformationRunResult,
|
||||
TransformationService,
|
||||
default_transformation_registry,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AssetChangeResult",
|
||||
@@ -45,4 +54,11 @@ __all__ = [
|
||||
"RetrievalFeedbackResult",
|
||||
"RetrievalQualityMetrics",
|
||||
"RetrievalSnippet",
|
||||
"TransformationExecutionContext",
|
||||
"TransformationOperationRegistry",
|
||||
"TransformationOutput",
|
||||
"TransformationRequest",
|
||||
"TransformationRunResult",
|
||||
"TransformationService",
|
||||
"default_transformation_registry",
|
||||
]
|
||||
|
||||
@@ -76,6 +76,10 @@ class AssetRegistryService:
|
||||
metadata_records: list[MetadataRecord] | None = None,
|
||||
asset_id: str | None = None,
|
||||
idempotency_key: str | None = None,
|
||||
version_change_type: VersionChangeType = VersionChangeType.CREATED,
|
||||
operation_id: str | None = None,
|
||||
parent_version_id: str | None = None,
|
||||
metadata_delta: dict[str, Any] | None = None,
|
||||
) -> AssetChangeResult:
|
||||
request_hash = mapping_digest(
|
||||
{
|
||||
@@ -85,6 +89,10 @@ class AssetRegistryService:
|
||||
"representations": [representation.to_dict() for representation in representations or []],
|
||||
"metadata_records": [record.to_dict() for record in metadata_records or []],
|
||||
"asset_id": asset_id,
|
||||
"version_change_type": version_change_type.value,
|
||||
"operation_id": operation_id,
|
||||
"parent_version_id": parent_version_id,
|
||||
"metadata_delta": dict(metadata_delta or {}),
|
||||
}
|
||||
)
|
||||
if idempotency_key:
|
||||
@@ -111,9 +119,12 @@ class AssetRegistryService:
|
||||
version = AssetVersion(
|
||||
asset_id=asset.id,
|
||||
sequence=1,
|
||||
change_type=VersionChangeType.CREATED,
|
||||
change_type=version_change_type,
|
||||
representation_ids=tuple(item.representation_id for item in representations or []),
|
||||
actor_id=context.actor.id,
|
||||
operation_id=operation_id,
|
||||
parent_version_id=parent_version_id,
|
||||
metadata_delta=dict(metadata_delta or {}),
|
||||
lifecycle=classification.lifecycle.value,
|
||||
)
|
||||
asset = asset.with_current_version(version.version_id)
|
||||
|
||||
728
src/kontextual_engine/services/transformation_service.py
Normal file
728
src/kontextual_engine/services/transformation_service.py
Normal file
@@ -0,0 +1,728 @@
|
||||
"""Traceable transformation operations over governed assets."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable
|
||||
|
||||
from kontextual_engine.core import (
|
||||
AssetRepresentation,
|
||||
AuditEvent,
|
||||
AuditOutcome,
|
||||
Classification,
|
||||
DerivedArtifactLineage,
|
||||
KnowledgeAsset,
|
||||
MetadataRecord,
|
||||
OperationContext,
|
||||
PolicyDecision,
|
||||
RepresentationKind,
|
||||
Sensitivity,
|
||||
TransformationOperation,
|
||||
TransformationRun,
|
||||
TransformationRunStatus,
|
||||
VersionChangeType,
|
||||
new_id,
|
||||
)
|
||||
from kontextual_engine.errors import Diagnostic
|
||||
from kontextual_engine.ports import AllowAllPolicyGateway, AssetRegistryRepository, PolicyGateway
|
||||
|
||||
from .asset_service import AssetChangeResult, AssetRegistryService
|
||||
|
||||
|
||||
OperationHandler = Callable[["TransformationExecutionContext"], "TransformationOutput"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TransformationRequest:
|
||||
operation_id: str
|
||||
source_asset_ids: tuple[str, ...] = ()
|
||||
parameters: dict[str, Any] = field(default_factory=dict)
|
||||
output_title: str | None = None
|
||||
output_asset_id: str | None = None
|
||||
output_asset_type: str = "derived_artifact"
|
||||
output_media_type: str | None = None
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "source_asset_ids", tuple(self.source_asset_ids))
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"operation_id": self.operation_id,
|
||||
"source_asset_ids": list(self.source_asset_ids),
|
||||
"parameters": dict(self.parameters),
|
||||
"output_title": self.output_title,
|
||||
"output_asset_id": self.output_asset_id,
|
||||
"output_asset_type": self.output_asset_type,
|
||||
"output_media_type": self.output_media_type,
|
||||
"metadata": dict(self.metadata),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TransformationOutput:
|
||||
content: str | bytes
|
||||
media_type: str
|
||||
title: str
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
adapter_provenance: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TransformationExecutionContext:
|
||||
operation: TransformationOperation
|
||||
request: TransformationRequest
|
||||
run: TransformationRun
|
||||
source_assets: tuple[KnowledgeAsset, ...]
|
||||
source_representations: dict[str, tuple[AssetRepresentation, ...]]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TransformationRunResult:
|
||||
run: TransformationRun | None
|
||||
success: bool
|
||||
diagnostics: tuple[Diagnostic, ...] = ()
|
||||
output_asset: KnowledgeAsset | None = None
|
||||
output_representation: AssetRepresentation | None = None
|
||||
lineage: DerivedArtifactLineage | None = None
|
||||
audit_event: AuditEvent | None = None
|
||||
policy_decision: PolicyDecision | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "diagnostics", tuple(self.diagnostics))
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"success": self.success,
|
||||
"run": self.run.to_dict() if self.run else None,
|
||||
"diagnostics": [diagnostic.to_dict() for diagnostic in self.diagnostics],
|
||||
"output_asset": self.output_asset.to_dict() if self.output_asset else None,
|
||||
"output_representation": self.output_representation.to_dict() if self.output_representation else None,
|
||||
"lineage": self.lineage.to_dict() if self.lineage else None,
|
||||
"audit_event": self.audit_event.to_dict() if self.audit_event else None,
|
||||
"policy_decision": self.policy_decision.to_dict() if self.policy_decision else None,
|
||||
}
|
||||
|
||||
|
||||
class TransformationOperationRegistry:
|
||||
def __init__(self) -> None:
|
||||
self._operations: dict[str, TransformationOperation] = {}
|
||||
self._handlers: dict[str, OperationHandler] = {}
|
||||
|
||||
def register(
|
||||
self,
|
||||
operation: TransformationOperation,
|
||||
*,
|
||||
handler: OperationHandler | None = None,
|
||||
) -> TransformationOperation:
|
||||
self._operations[operation.operation_id] = operation
|
||||
if handler is not None:
|
||||
self._handlers[operation.operation_id] = handler
|
||||
return operation
|
||||
|
||||
def get(self, operation_id: str) -> TransformationOperation | None:
|
||||
return self._operations.get(operation_id)
|
||||
|
||||
def handler_for(self, operation_id: str) -> OperationHandler | None:
|
||||
return self._handlers.get(operation_id)
|
||||
|
||||
def list_operations(self) -> tuple[TransformationOperation, ...]:
|
||||
return tuple(sorted(self._operations.values(), key=lambda item: item.operation_id))
|
||||
|
||||
def supported_operation_ids(self) -> tuple[str, ...]:
|
||||
return tuple(operation.operation_id for operation in self.list_operations())
|
||||
|
||||
|
||||
class TransformationService:
|
||||
def __init__(
|
||||
self,
|
||||
repository: AssetRegistryRepository,
|
||||
*,
|
||||
registry: TransformationOperationRegistry | None = None,
|
||||
policy_gateway: PolicyGateway | None = None,
|
||||
asset_service: AssetRegistryService | None = None,
|
||||
) -> None:
|
||||
self.repository = repository
|
||||
self.registry = registry or default_transformation_registry()
|
||||
self.policy_gateway = policy_gateway or AllowAllPolicyGateway()
|
||||
self.asset_service = asset_service or AssetRegistryService(
|
||||
repository,
|
||||
policy_gateway=self.policy_gateway,
|
||||
)
|
||||
|
||||
def list_operations(self) -> tuple[TransformationOperation, ...]:
|
||||
return self.registry.list_operations()
|
||||
|
||||
def execute_transformation(
|
||||
self,
|
||||
request: TransformationRequest,
|
||||
context: OperationContext,
|
||||
) -> TransformationRunResult:
|
||||
operation = self.registry.get(request.operation_id)
|
||||
if operation is None:
|
||||
return TransformationRunResult(
|
||||
run=None,
|
||||
success=False,
|
||||
diagnostics=(
|
||||
Diagnostic(
|
||||
severity="error",
|
||||
code="transformation.operation_unsupported",
|
||||
message="Transformation operation is not registered",
|
||||
details={
|
||||
"operation_id": request.operation_id,
|
||||
"supported": list(self.registry.supported_operation_ids()),
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
self.repository.save_actor(context.actor)
|
||||
source_assets = tuple(self.repository.get_asset(asset_id) for asset_id in request.source_asset_ids)
|
||||
source_version_ids = tuple(asset.current_version_id for asset in source_assets if asset.current_version_id)
|
||||
decision = self._authorize(
|
||||
context,
|
||||
"transformation.run.execute",
|
||||
f"transformation:{operation.operation_id}",
|
||||
resource_metadata={
|
||||
"operation": operation.to_dict(),
|
||||
"request": request.to_dict(),
|
||||
"source_version_ids": list(source_version_ids),
|
||||
},
|
||||
)
|
||||
source_decisions: tuple[PolicyDecision, ...] = ()
|
||||
policy_context = {"run_execute": decision.to_dict()}
|
||||
if decision.allowed:
|
||||
source_decisions = tuple(
|
||||
self._authorize(
|
||||
context,
|
||||
"asset.retrieve",
|
||||
f"asset:{asset.id}",
|
||||
resource_metadata={
|
||||
"asset_id": asset.id,
|
||||
"title": asset.title,
|
||||
"classification": asset.classification.to_dict(),
|
||||
"current_version_id": asset.current_version_id,
|
||||
},
|
||||
)
|
||||
for asset in source_assets
|
||||
)
|
||||
policy_context["source_reads"] = [item.to_dict() for item in source_decisions]
|
||||
capability_diagnostics = _capability_diagnostics(operation, source_assets, request)
|
||||
run = TransformationRun(
|
||||
operation_id=operation.operation_id,
|
||||
source_asset_ids=request.source_asset_ids,
|
||||
source_version_ids=source_version_ids,
|
||||
parameters=dict(request.parameters),
|
||||
actor_id=context.actor.id,
|
||||
correlation_id=context.correlation_id,
|
||||
policy_context=policy_context,
|
||||
)
|
||||
self.repository.save_transformation_run(run)
|
||||
self._audit(
|
||||
"transformation.run.queued",
|
||||
f"transformation_run:{run.run_id}",
|
||||
AuditOutcome.SUCCESS,
|
||||
context,
|
||||
decision,
|
||||
details={"operation_id": operation.operation_id},
|
||||
)
|
||||
|
||||
if not decision.allowed:
|
||||
failed = run.failed((_diagnostic_dict(_permission_diagnostic(decision)),))
|
||||
self.repository.save_transformation_run(failed)
|
||||
event = self._audit(
|
||||
"transformation.run.execute",
|
||||
f"transformation_run:{run.run_id}",
|
||||
AuditOutcome.DENIED,
|
||||
context,
|
||||
decision,
|
||||
details={"operation_id": operation.operation_id},
|
||||
)
|
||||
return TransformationRunResult(
|
||||
run=failed,
|
||||
success=False,
|
||||
diagnostics=(_permission_diagnostic(decision),),
|
||||
audit_event=event,
|
||||
policy_decision=decision,
|
||||
)
|
||||
|
||||
denied_source_decisions = tuple(item for item in source_decisions if not item.allowed)
|
||||
if denied_source_decisions:
|
||||
diagnostics = tuple(_permission_diagnostic(item) for item in denied_source_decisions)
|
||||
failed = run.failed(tuple(_diagnostic_dict(item) for item in diagnostics))
|
||||
self.repository.save_transformation_run(failed)
|
||||
event = self._audit(
|
||||
"transformation.run.execute",
|
||||
f"transformation_run:{run.run_id}",
|
||||
AuditOutcome.DENIED,
|
||||
context,
|
||||
denied_source_decisions[0],
|
||||
details={
|
||||
"operation_id": operation.operation_id,
|
||||
"denied_source_reads": [item.to_dict() for item in denied_source_decisions],
|
||||
},
|
||||
)
|
||||
return TransformationRunResult(
|
||||
run=failed,
|
||||
success=False,
|
||||
diagnostics=diagnostics,
|
||||
audit_event=event,
|
||||
policy_decision=denied_source_decisions[0],
|
||||
)
|
||||
|
||||
if capability_diagnostics:
|
||||
failed = run.failed(tuple(_diagnostic_dict(item) for item in capability_diagnostics))
|
||||
self.repository.save_transformation_run(failed)
|
||||
event = self._audit(
|
||||
"transformation.run.failed",
|
||||
f"transformation_run:{run.run_id}",
|
||||
AuditOutcome.FAILED,
|
||||
context,
|
||||
decision,
|
||||
details={"diagnostics": [item.to_dict() for item in capability_diagnostics]},
|
||||
)
|
||||
return TransformationRunResult(
|
||||
run=failed,
|
||||
success=False,
|
||||
diagnostics=tuple(capability_diagnostics),
|
||||
audit_event=event,
|
||||
policy_decision=decision,
|
||||
)
|
||||
|
||||
handler = self.registry.handler_for(operation.operation_id)
|
||||
if handler is None:
|
||||
diagnostic = Diagnostic(
|
||||
severity="error",
|
||||
code="transformation.operation_not_executable",
|
||||
message="Transformation operation is registered but has no executable adapter",
|
||||
details={
|
||||
"operation_id": operation.operation_id,
|
||||
"adapter_ref": operation.adapter_ref,
|
||||
},
|
||||
)
|
||||
failed = run.failed((_diagnostic_dict(diagnostic),))
|
||||
self.repository.save_transformation_run(failed)
|
||||
event = self._audit(
|
||||
"transformation.run.failed",
|
||||
f"transformation_run:{run.run_id}",
|
||||
AuditOutcome.FAILED,
|
||||
context,
|
||||
decision,
|
||||
details={"diagnostics": [diagnostic.to_dict()]},
|
||||
)
|
||||
return TransformationRunResult(
|
||||
run=failed,
|
||||
success=False,
|
||||
diagnostics=(diagnostic,),
|
||||
audit_event=event,
|
||||
policy_decision=decision,
|
||||
)
|
||||
|
||||
running = run.running()
|
||||
self.repository.save_transformation_run(running)
|
||||
self._audit(
|
||||
"transformation.run.started",
|
||||
f"transformation_run:{run.run_id}",
|
||||
AuditOutcome.SUCCESS,
|
||||
context,
|
||||
decision,
|
||||
details={"operation_id": operation.operation_id},
|
||||
)
|
||||
source_representations = {
|
||||
asset.id: tuple(self.repository.list_representations(asset_id=asset.id))
|
||||
for asset in source_assets
|
||||
}
|
||||
try:
|
||||
output = handler(
|
||||
TransformationExecutionContext(
|
||||
operation=operation,
|
||||
request=request,
|
||||
run=running,
|
||||
source_assets=source_assets,
|
||||
source_representations=source_representations,
|
||||
)
|
||||
)
|
||||
asset_change, output_representation, lineage = self._persist_output(
|
||||
request,
|
||||
output,
|
||||
running,
|
||||
source_assets,
|
||||
context,
|
||||
decision,
|
||||
)
|
||||
completed = running.completed(output_asset_ids=(asset_change.asset.id,))
|
||||
self.repository.save_transformation_run(completed)
|
||||
event = self._audit(
|
||||
"transformation.run.completed",
|
||||
f"transformation_run:{completed.run_id}",
|
||||
AuditOutcome.SUCCESS,
|
||||
context,
|
||||
decision,
|
||||
details={
|
||||
"operation_id": operation.operation_id,
|
||||
"output_asset_id": asset_change.asset.id,
|
||||
"lineage_id": lineage.lineage_id,
|
||||
"version_id": asset_change.version.version_id,
|
||||
},
|
||||
)
|
||||
return TransformationRunResult(
|
||||
run=completed,
|
||||
success=True,
|
||||
output_asset=asset_change.asset,
|
||||
output_representation=output_representation,
|
||||
lineage=lineage,
|
||||
audit_event=event,
|
||||
policy_decision=decision,
|
||||
)
|
||||
except Exception as exc:
|
||||
diagnostic = Diagnostic(
|
||||
severity="error",
|
||||
code="transformation.execution_failed",
|
||||
message="Transformation execution failed",
|
||||
details={"error_type": type(exc).__name__, "error": str(exc)},
|
||||
)
|
||||
failed = running.failed((_diagnostic_dict(diagnostic),))
|
||||
self.repository.save_transformation_run(failed)
|
||||
event = self._audit(
|
||||
"transformation.run.failed",
|
||||
f"transformation_run:{failed.run_id}",
|
||||
AuditOutcome.FAILED,
|
||||
context,
|
||||
decision,
|
||||
details={"diagnostics": [diagnostic.to_dict()]},
|
||||
)
|
||||
return TransformationRunResult(
|
||||
run=failed,
|
||||
success=False,
|
||||
diagnostics=(diagnostic,),
|
||||
audit_event=event,
|
||||
policy_decision=decision,
|
||||
)
|
||||
|
||||
def get_run(self, run_id: str) -> TransformationRun:
|
||||
return self.repository.get_transformation_run(run_id)
|
||||
|
||||
def list_runs(
|
||||
self,
|
||||
*,
|
||||
status: TransformationRunStatus | None = None,
|
||||
operation_id: str | None = None,
|
||||
) -> tuple[TransformationRun, ...]:
|
||||
return tuple(self.repository.list_transformation_runs(status=status, operation_id=operation_id))
|
||||
|
||||
def retry_run(self, run_id: str, context: OperationContext) -> TransformationRunResult:
|
||||
previous = self.repository.get_transformation_run(run_id)
|
||||
marked = previous.retried()
|
||||
self.repository.save_transformation_run(marked)
|
||||
retry_request = TransformationRequest(
|
||||
operation_id=previous.operation_id,
|
||||
source_asset_ids=previous.source_asset_ids,
|
||||
parameters=dict(previous.parameters),
|
||||
)
|
||||
return self.execute_transformation(retry_request, context)
|
||||
|
||||
def cancel_run(self, run_id: str, context: OperationContext, *, reason: str | None = None) -> TransformationRun:
|
||||
run = self.repository.get_transformation_run(run_id)
|
||||
diagnostic = Diagnostic(
|
||||
severity="warning",
|
||||
code="transformation.run_canceled",
|
||||
message="Transformation run was canceled",
|
||||
details={"reason": reason} if reason else {},
|
||||
)
|
||||
canceled = run.canceled((_diagnostic_dict(diagnostic),))
|
||||
self.repository.save_actor(context.actor)
|
||||
decision = PolicyDecision.allow(context.actor.id, "transformation.run.cancel", f"transformation_run:{run_id}")
|
||||
self.repository.save_transformation_run(canceled)
|
||||
self._audit(
|
||||
"transformation.run.canceled",
|
||||
f"transformation_run:{run_id}",
|
||||
AuditOutcome.SUCCESS,
|
||||
context,
|
||||
decision,
|
||||
details={"reason": reason} if reason else {},
|
||||
)
|
||||
return canceled
|
||||
|
||||
def _persist_output(
|
||||
self,
|
||||
request: TransformationRequest,
|
||||
output: TransformationOutput,
|
||||
run: TransformationRun,
|
||||
source_assets: tuple[KnowledgeAsset, ...],
|
||||
context: OperationContext,
|
||||
decision: PolicyDecision,
|
||||
) -> tuple[AssetChangeResult, AssetRepresentation, DerivedArtifactLineage]:
|
||||
output_asset_id = request.output_asset_id or new_id("asset")
|
||||
output_representation = AssetRepresentation.from_content(
|
||||
output_asset_id,
|
||||
RepresentationKind.DERIVED,
|
||||
request.output_media_type or output.media_type,
|
||||
output.content,
|
||||
producer=request.operation_id,
|
||||
metadata={
|
||||
"transformation_run_id": run.run_id,
|
||||
"operation_id": run.operation_id,
|
||||
"adapter_provenance": dict(output.adapter_provenance),
|
||||
**output.metadata,
|
||||
},
|
||||
)
|
||||
lineage = DerivedArtifactLineage(
|
||||
source_asset_ids=run.source_asset_ids,
|
||||
source_version_ids=run.source_version_ids,
|
||||
transformation_run_id=run.run_id,
|
||||
output_asset_id=output_asset_id,
|
||||
output_representation_id=output_representation.representation_id,
|
||||
actor_id=context.actor.id,
|
||||
parameters=dict(run.parameters),
|
||||
policy_context=dict(run.policy_context),
|
||||
adapter_provenance=dict(output.adapter_provenance),
|
||||
)
|
||||
classification = Classification(
|
||||
asset_type=request.output_asset_type,
|
||||
sensitivity=_highest_sensitivity(source_assets),
|
||||
owner=context.actor.id,
|
||||
topics=("derived", run.operation_id),
|
||||
metadata={
|
||||
"transformation_run_id": run.run_id,
|
||||
"operation_id": run.operation_id,
|
||||
**request.metadata,
|
||||
},
|
||||
)
|
||||
asset_change = self.asset_service.create_asset(
|
||||
request.output_title or output.title,
|
||||
classification,
|
||||
context,
|
||||
asset_id=output_asset_id,
|
||||
representations=[output_representation],
|
||||
metadata_records=[
|
||||
MetadataRecord(
|
||||
"lineage",
|
||||
lineage.to_dict(),
|
||||
provenance={"producer": "kontextual-engine"},
|
||||
confirmed=True,
|
||||
),
|
||||
MetadataRecord(
|
||||
"transformation_run_id",
|
||||
run.run_id,
|
||||
provenance={"producer": "kontextual-engine"},
|
||||
confirmed=True,
|
||||
),
|
||||
],
|
||||
version_change_type=VersionChangeType.DERIVED_OUTPUT,
|
||||
operation_id=run.run_id,
|
||||
parent_version_id=run.source_version_ids[0] if run.source_version_ids else None,
|
||||
metadata_delta={"lineage_id": lineage.lineage_id, "operation_id": run.operation_id},
|
||||
)
|
||||
self.repository.save_derived_lineage(lineage)
|
||||
return asset_change, output_representation, lineage
|
||||
|
||||
def _authorize(
|
||||
self,
|
||||
context: OperationContext,
|
||||
action: str,
|
||||
resource: str,
|
||||
*,
|
||||
resource_metadata: dict[str, Any] | None = None,
|
||||
) -> PolicyDecision:
|
||||
self.repository.save_actor(context.actor)
|
||||
try:
|
||||
return self.policy_gateway.authorize(
|
||||
context,
|
||||
action,
|
||||
resource,
|
||||
resource_metadata=resource_metadata,
|
||||
)
|
||||
except Exception as exc:
|
||||
return PolicyDecision.fail_closed(
|
||||
context.actor.id,
|
||||
action,
|
||||
resource,
|
||||
reason=str(exc) or "Transformation policy gateway failed",
|
||||
context={"resource_metadata": resource_metadata or {}, "gateway_error": type(exc).__name__},
|
||||
)
|
||||
|
||||
def _audit(
|
||||
self,
|
||||
operation: str,
|
||||
target: str,
|
||||
outcome: AuditOutcome,
|
||||
context: OperationContext,
|
||||
policy_decision: PolicyDecision,
|
||||
*,
|
||||
details: dict[str, Any] | None = None,
|
||||
) -> AuditEvent:
|
||||
event = AuditEvent.from_context(
|
||||
operation,
|
||||
target,
|
||||
outcome,
|
||||
context,
|
||||
policy_decision=policy_decision,
|
||||
details=details,
|
||||
)
|
||||
return self.repository.save_audit_event(event)
|
||||
|
||||
|
||||
def default_transformation_registry() -> TransformationOperationRegistry:
|
||||
registry = TransformationOperationRegistry()
|
||||
registry.register(
|
||||
TransformationOperation(
|
||||
operation_id="structured_view",
|
||||
name="Produce Structured View",
|
||||
description="Create a JSON structured view of source asset metadata and representation references.",
|
||||
input_spec=("knowledge_asset",),
|
||||
output_spec=("derived_asset:application/json",),
|
||||
parameter_schema={"required": []},
|
||||
required_permissions=("asset.retrieve", "asset.create"),
|
||||
),
|
||||
handler=_structured_view_handler,
|
||||
)
|
||||
registry.register(
|
||||
TransformationOperation(
|
||||
operation_id="summarize",
|
||||
name="Summarize",
|
||||
description="Summarize source assets through a provider adapter.",
|
||||
input_spec=("knowledge_asset",),
|
||||
output_spec=("derived_asset:text/markdown",),
|
||||
parameter_schema={"required": ["style"]},
|
||||
required_permissions=("asset.retrieve", "asset.create"),
|
||||
adapter_ref="llm-connect",
|
||||
)
|
||||
)
|
||||
registry.register(
|
||||
TransformationOperation(
|
||||
operation_id="classify",
|
||||
name="Classify",
|
||||
description="Classify assets through a deterministic or provider-backed classifier adapter.",
|
||||
input_spec=("knowledge_asset",),
|
||||
output_spec=("metadata_record",),
|
||||
required_permissions=("asset.retrieve", "asset.metadata.add"),
|
||||
adapter_ref="classifier-adapter",
|
||||
)
|
||||
)
|
||||
for operation_id, name in (
|
||||
("markdown_compose", "Markdown Compose"),
|
||||
("markdown_include", "Markdown Include Resolution"),
|
||||
("markdown_transform", "Markdown Transform"),
|
||||
("markdown_validate", "Markdown Validate"),
|
||||
):
|
||||
registry.register(
|
||||
TransformationOperation(
|
||||
operation_id=operation_id,
|
||||
name=name,
|
||||
description="Markdown-specific operation delegated to markitect-tool.",
|
||||
input_spec=("markdown_asset",),
|
||||
output_spec=("derived_asset:text/markdown",),
|
||||
required_permissions=("asset.retrieve", "asset.create"),
|
||||
supported_asset_types=("document", "markdown", "markdown_proxy"),
|
||||
adapter_ref="markitect-tool",
|
||||
metadata={"boundary": "adapter-backed; do not reimplement markdown syntax in engine"},
|
||||
)
|
||||
)
|
||||
registry.register(
|
||||
TransformationOperation(
|
||||
operation_id="generate_report",
|
||||
name="Generate Report",
|
||||
description="Generate a report from source assets through a report adapter.",
|
||||
input_spec=("knowledge_asset",),
|
||||
output_spec=("derived_asset",),
|
||||
required_permissions=("asset.retrieve", "asset.create"),
|
||||
adapter_ref="report-adapter",
|
||||
)
|
||||
)
|
||||
return registry
|
||||
|
||||
|
||||
def _structured_view_handler(context: TransformationExecutionContext) -> TransformationOutput:
|
||||
source_payload = []
|
||||
for asset in context.source_assets:
|
||||
source_payload.append(
|
||||
{
|
||||
"asset_id": asset.id,
|
||||
"title": asset.title,
|
||||
"classification": asset.classification.to_dict(),
|
||||
"lifecycle": asset.lifecycle.value,
|
||||
"current_version_id": asset.current_version_id,
|
||||
"representations": [
|
||||
representation.to_dict()
|
||||
for representation in context.source_representations.get(asset.id, ())
|
||||
],
|
||||
"source_refs": [source_ref.to_dict() for source_ref in asset.source_refs],
|
||||
}
|
||||
)
|
||||
content = json.dumps(
|
||||
{
|
||||
"operation_id": context.operation.operation_id,
|
||||
"parameters": dict(context.request.parameters),
|
||||
"source_assets": source_payload,
|
||||
},
|
||||
sort_keys=True,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
return TransformationOutput(
|
||||
content=content,
|
||||
media_type="application/json",
|
||||
title=context.request.output_title or "Structured View",
|
||||
metadata={"source_count": len(context.source_assets)},
|
||||
adapter_provenance={"operation": "structured_view", "adapter": "kontextual-engine"},
|
||||
)
|
||||
|
||||
|
||||
def _capability_diagnostics(
|
||||
operation: TransformationOperation,
|
||||
source_assets: tuple[KnowledgeAsset, ...],
|
||||
request: TransformationRequest,
|
||||
) -> list[Diagnostic]:
|
||||
diagnostics: list[Diagnostic] = []
|
||||
for asset in source_assets:
|
||||
if not operation.supports_asset_type(asset.classification.asset_type):
|
||||
diagnostics.append(
|
||||
Diagnostic(
|
||||
severity="error",
|
||||
code="transformation.asset_type_unsupported",
|
||||
message="Transformation operation does not support source asset type",
|
||||
details={
|
||||
"operation_id": operation.operation_id,
|
||||
"asset_id": asset.id,
|
||||
"asset_type": asset.classification.asset_type,
|
||||
"supported_asset_types": list(operation.supported_asset_types),
|
||||
},
|
||||
)
|
||||
)
|
||||
required_parameters = operation.parameter_schema.get("required", ())
|
||||
for key in required_parameters:
|
||||
if key not in request.parameters:
|
||||
diagnostics.append(
|
||||
Diagnostic(
|
||||
severity="error",
|
||||
code="transformation.parameter_missing",
|
||||
message="Transformation operation requires a missing parameter",
|
||||
details={"operation_id": operation.operation_id, "parameter": key},
|
||||
)
|
||||
)
|
||||
return diagnostics
|
||||
|
||||
|
||||
def _highest_sensitivity(source_assets: tuple[KnowledgeAsset, ...]) -> Sensitivity:
|
||||
if not source_assets:
|
||||
return Sensitivity.INTERNAL
|
||||
order = {
|
||||
Sensitivity.PUBLIC: 0,
|
||||
Sensitivity.INTERNAL: 1,
|
||||
Sensitivity.CONFIDENTIAL: 2,
|
||||
Sensitivity.RESTRICTED: 3,
|
||||
}
|
||||
return max((asset.classification.sensitivity for asset in source_assets), key=lambda item: order[item])
|
||||
|
||||
|
||||
def _permission_diagnostic(decision: PolicyDecision) -> Diagnostic:
|
||||
return Diagnostic(
|
||||
severity="error",
|
||||
code="transformation.permission_denied",
|
||||
message="Transformation operation denied by policy",
|
||||
details={"policy_decision": decision.to_dict()},
|
||||
)
|
||||
|
||||
|
||||
def _diagnostic_dict(diagnostic: Diagnostic) -> dict[str, Any]:
|
||||
return diagnostic.to_dict()
|
||||
Reference in New Issue
Block a user