feat(memory): add graph runtime import store

This commit is contained in:
2026-05-15 01:56:51 +02:00
parent 5c450fcaa5
commit 8daab687b2
12 changed files with 1103 additions and 4 deletions

View File

@@ -13,6 +13,7 @@ from .artifacts import (
)
from .adapters.memory import InMemoryAssetRegistryRepository
from .adapters.memory import InMemoryBlobStorage
from .adapters.memory import InMemoryMemoryGraphRepository
from .adapters.local_files import LocalBlobStorage
from .adapters.s3 import S3BlobStorage
from .adapters.sqlite import SQLiteAssetRegistryRepository
@@ -49,12 +50,20 @@ from .core import (
IngestionJobStatus,
KnowledgeAsset,
LifecycleState,
MARKITECT_MEMORY_GRAPH_SCHEMA_VERSION,
MARKITECT_MEMORY_PROFILE_SCHEMA_VERSION,
MetadataFieldDefinition,
MetadataRecord,
MetadataSchema,
MetadataSchemaAssignment,
MetadataValidationIssue,
MetadataValueType,
MemoryEdgeRecord,
MemoryEventRecord,
MemoryGraphImportResult,
MemoryNodeRecord,
MemoryProfileRecord,
MemorySourceSpan,
NormalizedDocument,
OperationContext,
PolicyDecision,
@@ -107,6 +116,7 @@ from .ports import (
BlobWriteResult,
DirectorySourceConnector,
FormatExtractor,
MemoryGraphRepository,
PolicyGateway,
SourceConnector,
)
@@ -125,6 +135,8 @@ from .services import (
ContextEntityQueryRequest,
ContextEntityQueryResult,
LexicalIndexRefreshResult,
MemoryGraphImportSummary,
MemoryRuntimeService,
RelationshipChangeResult,
RelationshipQueryItem,
RelationshipQueryRequest,
@@ -217,6 +229,7 @@ __all__ = [
"FormatExtractor",
"InMemoryAssetRegistryRepository",
"InMemoryBlobStorage",
"InMemoryMemoryGraphRepository",
"InMemoryKnowledgeRepository",
"IngestionRequest",
"IngestionResult",
@@ -233,12 +246,23 @@ __all__ = [
"LexicalIndexRefreshResult",
"LifecycleState",
"LocalBlobStorage",
"MARKITECT_MEMORY_GRAPH_SCHEMA_VERSION",
"MARKITECT_MEMORY_PROFILE_SCHEMA_VERSION",
"MetadataFieldDefinition",
"MetadataRecord",
"MetadataSchema",
"MetadataSchemaAssignment",
"MetadataValidationIssue",
"MetadataValueType",
"MemoryEdgeRecord",
"MemoryEventRecord",
"MemoryGraphImportResult",
"MemoryGraphImportSummary",
"MemoryGraphRepository",
"MemoryNodeRecord",
"MemoryProfileRecord",
"MemoryRuntimeService",
"MemorySourceSpan",
"NormalizedDocument",
"NotFoundError",
"OperationFailure",

View File

@@ -2,5 +2,10 @@
from .asset_registry import InMemoryAssetRegistryRepository
from .blob_storage import InMemoryBlobStorage
from .graph_store import InMemoryMemoryGraphRepository
__all__ = ["InMemoryAssetRegistryRepository", "InMemoryBlobStorage"]
__all__ = [
"InMemoryAssetRegistryRepository",
"InMemoryBlobStorage",
"InMemoryMemoryGraphRepository",
]

View File

@@ -0,0 +1,126 @@
"""In-memory operational memory graph repository."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Iterable
from kontextual_engine.core import (
LifecycleState,
MemoryEdgeRecord,
MemoryEventRecord,
MemoryNodeRecord,
MemoryProfileRecord,
)
from kontextual_engine.errors import DuplicateResourceError, NotFoundError, ValidationError
@dataclass
class InMemoryMemoryGraphRepository:
profiles: dict[str, MemoryProfileRecord] = field(default_factory=dict)
nodes: dict[str, MemoryNodeRecord] = field(default_factory=dict)
edges: dict[str, MemoryEdgeRecord] = field(default_factory=dict)
events: dict[str, MemoryEventRecord] = field(default_factory=dict)
def save_memory_profile(self, profile: MemoryProfileRecord) -> MemoryProfileRecord:
self.profiles[str(profile.profile_id)] = profile
return profile
def get_memory_profile(self, profile_id: str) -> MemoryProfileRecord:
try:
return self.profiles[profile_id]
except KeyError as exc:
raise NotFoundError("Memory profile not found", details={"profile_id": profile_id}) from exc
def save_memory_node(self, node: MemoryNodeRecord) -> MemoryNodeRecord:
self.nodes[str(node.node_id)] = node
return node
def get_memory_node(self, node_id: str) -> MemoryNodeRecord:
try:
return self.nodes[node_id]
except KeyError as exc:
raise NotFoundError("Memory node not found", details={"node_id": node_id}) from exc
def list_memory_nodes(
self,
*,
graph_id: str | None = None,
kind: str | None = None,
lifecycle: LifecycleState | str | None = None,
) -> list[MemoryNodeRecord]:
nodes: Iterable[MemoryNodeRecord] = self.nodes.values()
if graph_id is not None:
nodes = [node for node in nodes if node.graph_id == graph_id]
if kind is not None:
nodes = [node for node in nodes if node.kind == kind]
if lifecycle is not None:
lifecycle = LifecycleState(lifecycle)
nodes = [node for node in nodes if node.lifecycle == lifecycle]
return sorted(nodes, key=lambda node: (node.graph_id, node.kind, node.contract_node_id))
def save_memory_edge(self, edge: MemoryEdgeRecord) -> MemoryEdgeRecord:
source = self.get_memory_node(edge.source_node_id)
target = self.get_memory_node(edge.target_node_id)
if source.graph_id != edge.graph_id or target.graph_id != edge.graph_id:
raise ValidationError(
"Memory edge endpoints must belong to the edge graph.",
details={
"edge_id": edge.edge_id,
"graph_id": edge.graph_id,
"source_graph_id": source.graph_id,
"target_graph_id": target.graph_id,
},
)
self.edges[str(edge.edge_id)] = edge
return edge
def get_memory_edge(self, edge_id: str) -> MemoryEdgeRecord:
try:
return self.edges[edge_id]
except KeyError as exc:
raise NotFoundError("Memory edge not found", details={"edge_id": edge_id}) from exc
def list_memory_edges(
self,
*,
graph_id: str | None = None,
source_node_id: str | None = None,
target_node_id: str | None = None,
) -> list[MemoryEdgeRecord]:
edges: Iterable[MemoryEdgeRecord] = self.edges.values()
if graph_id is not None:
edges = [edge for edge in edges if edge.graph_id == graph_id]
if source_node_id is not None:
edges = [edge for edge in edges if edge.source_node_id == source_node_id]
if target_node_id is not None:
edges = [edge for edge in edges if edge.target_node_id == target_node_id]
return sorted(edges, key=lambda edge: (edge.graph_id, edge.kind, edge.contract_edge_id))
def append_memory_event(self, event: MemoryEventRecord) -> MemoryEventRecord:
if event.event_id in self.events:
raise DuplicateResourceError(
"Memory event already exists; event storage is append-only.",
details={"event_id": event.event_id, "contract_event_id": event.contract_event_id},
)
self.events[str(event.event_id)] = event
return event
def get_memory_event(self, event_id: str) -> MemoryEventRecord:
try:
return self.events[event_id]
except KeyError as exc:
raise NotFoundError("Memory event not found", details={"event_id": event_id}) from exc
def list_memory_events(
self,
*,
graph_id: str | None = None,
kind: str | None = None,
) -> list[MemoryEventRecord]:
events: Iterable[MemoryEventRecord] = self.events.values()
if graph_id is not None:
events = [event for event in events if event.graph_id == graph_id]
if kind is not None:
events = [event for event in events if event.kind == kind]
return sorted(events, key=lambda event: (event.graph_id, event.timestamp, event.contract_event_id))

View File

@@ -36,6 +36,17 @@ from .metadata import (
MetadataValueType,
Sensitivity,
)
from .memory import (
MARKITECT_MEMORY_GRAPH_SCHEMA_VERSION,
MARKITECT_MEMORY_PROFILE_SCHEMA_VERSION,
MemoryEdgeRecord,
MemoryEventRecord,
MemoryGraphImportResult,
MemoryNodeRecord,
MemoryProfileRecord,
MemorySourceSpan,
runtime_id,
)
from .policy import PolicyDecision, PolicyEffect
from .primitives import content_digest, mapping_digest, new_id, stable_json_dumps, utc_now
from .provenance import (
@@ -100,12 +111,20 @@ __all__ = [
"IngestionJobStatus",
"KnowledgeAsset",
"LifecycleState",
"MARKITECT_MEMORY_GRAPH_SCHEMA_VERSION",
"MARKITECT_MEMORY_PROFILE_SCHEMA_VERSION",
"MetadataFieldDefinition",
"MetadataRecord",
"MetadataSchema",
"MetadataSchemaAssignment",
"MetadataValidationIssue",
"MetadataValueType",
"MemoryEdgeRecord",
"MemoryEventRecord",
"MemoryGraphImportResult",
"MemoryNodeRecord",
"MemoryProfileRecord",
"MemorySourceSpan",
"NormalizedDocument",
"OperationContext",
"PolicyDecision",
@@ -114,6 +133,7 @@ __all__ = [
"RepresentationKind",
"RetrievalFeedbackLabel",
"RetrievalFeedbackRecord",
"runtime_id",
"Sensitivity",
"SourceReference",
"SourcePayload",

View File

@@ -0,0 +1,556 @@
"""Runtime memory graph records mapped from Markitect contracts."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from .metadata import LifecycleState
from .primitives import compact_dict, mapping_digest, utc_now
MARKITECT_MEMORY_GRAPH_SCHEMA_VERSION = "markitect.memory.graph.v1"
MARKITECT_MEMORY_PROFILE_SCHEMA_VERSION = "markitect.memory.profile.v1"
@dataclass(frozen=True)
class MemorySourceSpan:
path: str
snapshot_id: str | None = None
unit_kind: str | None = None
unit_index: int | None = None
line_start: int | None = None
line_end: int | None = None
selector: str | None = None
engine: str | None = None
@classmethod
def from_contract(cls, data: dict[str, Any]) -> "MemorySourceSpan":
return cls(
path=str(data["path"]),
snapshot_id=_optional_str(data.get("snapshot_id")),
unit_kind=_optional_str(data.get("unit_kind")),
unit_index=_optional_int(data.get("unit_index")),
line_start=_optional_int(data.get("line_start")),
line_end=_optional_int(data.get("line_end")),
selector=_optional_str(data.get("selector")),
engine=_optional_str(data.get("engine")),
)
def to_dict(self) -> dict[str, Any]:
return compact_dict(
{
"path": self.path,
"snapshot_id": self.snapshot_id,
"unit_kind": self.unit_kind,
"unit_index": self.unit_index,
"line_start": self.line_start,
"line_end": self.line_end,
"selector": self.selector,
"engine": self.engine,
}
)
@dataclass(frozen=True)
class MemoryProfileRecord:
contract_profile_id: str
schema_version: str
title: str = ""
intent: str = ""
memory_kinds: tuple[str, ...] = ()
stores: dict[str, Any] = field(default_factory=dict)
limits: dict[str, Any] = field(default_factory=dict)
latency: dict[str, Any] = field(default_factory=dict)
retention: dict[str, Any] = field(default_factory=dict)
refresh: dict[str, Any] = field(default_factory=dict)
compaction: dict[str, Any] = field(default_factory=dict)
activation: dict[str, Any] = field(default_factory=dict)
policy: dict[str, Any] = field(default_factory=dict)
observability: dict[str, Any] = field(default_factory=dict)
failure: dict[str, Any] = field(default_factory=dict)
metadata: dict[str, Any] = field(default_factory=dict)
profile_id: str | None = None
imported_at: str = field(default_factory=lambda: utc_now().isoformat())
def __post_init__(self) -> None:
if self.profile_id is None:
object.__setattr__(
self,
"profile_id",
runtime_id("memprofile", self.schema_version, self.contract_profile_id),
)
object.__setattr__(self, "memory_kinds", tuple(self.memory_kinds))
@classmethod
def from_markitect_contract(cls, data: dict[str, Any]) -> "MemoryProfileRecord":
schema_version = str(data.get("schema_version") or data.get("schema") or "")
if schema_version != MARKITECT_MEMORY_PROFILE_SCHEMA_VERSION:
raise ValueError(f"Unsupported memory profile schema `{schema_version}`.")
contract_id = str(data.get("id") or "")
if not contract_id:
raise ValueError("Memory profile contract must declare an id.")
return cls(
contract_profile_id=contract_id,
schema_version=schema_version,
title=str(data.get("title") or data.get("name") or ""),
intent=str(data.get("intent") or data.get("description") or ""),
memory_kinds=tuple(str(item) for item in data.get("memory_kinds", ())),
stores=dict(data.get("stores") or {}),
limits=dict(data.get("limits") or {}),
latency=dict(data.get("latency") or data.get("latency_targets") or {}),
retention=dict(data.get("retention") or data.get("retention_policy") or {}),
refresh=dict(data.get("refresh") or {}),
compaction=dict(data.get("compaction") or {}),
activation=dict(data.get("activation") or data.get("context_budget") or {}),
policy=dict(data.get("policy") or {}),
observability=dict(data.get("observability") or {}),
failure=dict(data.get("failure") or {}),
metadata=dict(data.get("metadata") or {}),
)
def to_dict(self) -> dict[str, Any]:
return compact_dict(
{
"profile_id": self.profile_id,
"contract_profile_id": self.contract_profile_id,
"schema_version": self.schema_version,
"title": self.title,
"intent": self.intent,
"memory_kinds": list(self.memory_kinds),
"stores": dict(self.stores),
"limits": dict(self.limits),
"latency": dict(self.latency),
"retention": dict(self.retention),
"refresh": dict(self.refresh),
"compaction": dict(self.compaction),
"activation": dict(self.activation),
"policy": dict(self.policy),
"observability": dict(self.observability),
"failure": dict(self.failure),
"metadata": dict(self.metadata),
"imported_at": self.imported_at,
}
)
@dataclass(frozen=True)
class MemoryNodeRecord:
graph_id: str
contract_graph_id: str
contract_node_id: str
kind: str
text: str = ""
namespace: dict[str, Any] = field(default_factory=dict)
source_spans: tuple[MemorySourceSpan, ...] = ()
provenance: tuple[dict[str, Any], ...] = ()
freshness: dict[str, Any] = field(default_factory=dict)
confidence: float | None = None
policy: dict[str, Any] = field(default_factory=dict)
lifecycle: LifecycleState = LifecycleState.ACTIVE
metadata: dict[str, Any] = field(default_factory=dict)
node_id: str | None = None
created_at: str = field(default_factory=lambda: utc_now().isoformat())
updated_at: str = field(default_factory=lambda: utc_now().isoformat())
def __post_init__(self) -> None:
if self.node_id is None:
object.__setattr__(
self,
"node_id",
runtime_id("memnode", self.contract_graph_id, self.contract_node_id),
)
object.__setattr__(self, "source_spans", tuple(self.source_spans))
object.__setattr__(self, "provenance", tuple(dict(item) for item in self.provenance))
object.__setattr__(self, "lifecycle", LifecycleState(self.lifecycle))
@classmethod
def from_markitect_contract(
cls,
*,
graph_id: str,
contract_graph_id: str,
data: dict[str, Any],
graph_namespace: dict[str, Any] | None = None,
) -> "MemoryNodeRecord":
contract_node_id = str(data.get("id") or "")
if not contract_node_id:
raise ValueError("Memory graph node must declare an id.")
spans = data.get("source_spans") or data.get("spans") or []
return cls(
graph_id=graph_id,
contract_graph_id=contract_graph_id,
contract_node_id=contract_node_id,
kind=str(data.get("kind") or data.get("type") or ""),
text=str(data.get("text") or data.get("content") or data.get("summary") or ""),
namespace=dict(data.get("namespace") or graph_namespace or {}),
source_spans=tuple(MemorySourceSpan.from_contract(span) for span in _mapping_list(spans)),
provenance=tuple(_mapping_list(data.get("provenance"))),
freshness=dict(data.get("freshness") or {}),
confidence=_optional_float(data.get("confidence")),
policy=dict(data.get("policy") or {}),
metadata=dict(data.get("metadata") or {}),
)
@property
def resource_id(self) -> str:
return f"memory-node:{self.node_id}"
def to_dict(self) -> dict[str, Any]:
return compact_dict(
{
"node_id": self.node_id,
"graph_id": self.graph_id,
"contract_graph_id": self.contract_graph_id,
"contract_node_id": self.contract_node_id,
"kind": self.kind,
"text": self.text,
"namespace": dict(self.namespace),
"source_spans": [span.to_dict() for span in self.source_spans],
"provenance": [dict(item) for item in self.provenance],
"freshness": dict(self.freshness),
"confidence": self.confidence,
"policy": dict(self.policy),
"lifecycle": self.lifecycle.value,
"metadata": dict(self.metadata),
"created_at": self.created_at,
"updated_at": self.updated_at,
}
)
@dataclass(frozen=True)
class MemoryEdgeRecord:
graph_id: str
contract_graph_id: str
contract_edge_id: str
kind: str
source_node_id: str
target_node_id: str
source_contract_node_id: str
target_contract_node_id: str
provenance: tuple[dict[str, Any], ...] = ()
freshness: dict[str, Any] = field(default_factory=dict)
confidence: float | None = None
policy: dict[str, Any] = field(default_factory=dict)
lifecycle: LifecycleState = LifecycleState.ACTIVE
metadata: dict[str, Any] = field(default_factory=dict)
edge_id: str | None = None
created_at: str = field(default_factory=lambda: utc_now().isoformat())
def __post_init__(self) -> None:
if self.edge_id is None:
object.__setattr__(
self,
"edge_id",
runtime_id("memedge", self.contract_graph_id, self.contract_edge_id),
)
object.__setattr__(self, "provenance", tuple(dict(item) for item in self.provenance))
object.__setattr__(self, "lifecycle", LifecycleState(self.lifecycle))
@classmethod
def from_markitect_contract(
cls,
*,
graph_id: str,
contract_graph_id: str,
data: dict[str, Any],
node_id_by_contract_id: dict[str, str],
) -> "MemoryEdgeRecord":
kind = str(data.get("kind") or data.get("type") or "")
source_contract_id = str(data.get("source") or data.get("from") or "")
target_contract_id = str(data.get("target") or data.get("to") or "")
contract_edge_id = str(
data.get("id")
or f"edge:{stable_suffix(kind, source_contract_id, target_contract_id)}"
)
try:
source_node_id = node_id_by_contract_id[source_contract_id]
target_node_id = node_id_by_contract_id[target_contract_id]
except KeyError as exc:
missing = source_contract_id if source_contract_id not in node_id_by_contract_id else target_contract_id
raise ValueError(f"Memory edge `{contract_edge_id}` references unknown node `{missing}`.") from exc
return cls(
graph_id=graph_id,
contract_graph_id=contract_graph_id,
contract_edge_id=contract_edge_id,
kind=kind,
source_node_id=source_node_id,
target_node_id=target_node_id,
source_contract_node_id=source_contract_id,
target_contract_node_id=target_contract_id,
provenance=tuple(_mapping_list(data.get("provenance"))),
freshness=dict(data.get("freshness") or {}),
confidence=_optional_float(data.get("confidence")),
policy=dict(data.get("policy") or {}),
metadata=dict(data.get("metadata") or {}),
)
def to_dict(self) -> dict[str, Any]:
return compact_dict(
{
"edge_id": self.edge_id,
"graph_id": self.graph_id,
"contract_graph_id": self.contract_graph_id,
"contract_edge_id": self.contract_edge_id,
"kind": self.kind,
"source_node_id": self.source_node_id,
"target_node_id": self.target_node_id,
"source_contract_node_id": self.source_contract_node_id,
"target_contract_node_id": self.target_contract_node_id,
"provenance": [dict(item) for item in self.provenance],
"freshness": dict(self.freshness),
"confidence": self.confidence,
"policy": dict(self.policy),
"lifecycle": self.lifecycle.value,
"metadata": dict(self.metadata),
"created_at": self.created_at,
}
)
@dataclass(frozen=True)
class MemoryEventRecord:
graph_id: str
contract_graph_id: str
contract_event_id: str
kind: str
timestamp: str
namespace: dict[str, Any] = field(default_factory=dict)
actor_id: str | None = None
thread: str | None = None
task: str | None = None
node_updates: tuple[dict[str, Any], ...] = ()
edge_updates: tuple[dict[str, Any], ...] = ()
package_refs: tuple[str, ...] = ()
activation_refs: tuple[str, ...] = ()
branch: dict[str, Any] = field(default_factory=dict)
policy: dict[str, Any] = field(default_factory=dict)
metadata: dict[str, Any] = field(default_factory=dict)
event_id: str | None = None
appended_at: str = field(default_factory=lambda: utc_now().isoformat())
def __post_init__(self) -> None:
if self.event_id is None:
object.__setattr__(
self,
"event_id",
runtime_id("memevent", self.contract_graph_id, self.contract_event_id),
)
object.__setattr__(self, "node_updates", tuple(dict(item) for item in self.node_updates))
object.__setattr__(self, "edge_updates", tuple(dict(item) for item in self.edge_updates))
object.__setattr__(self, "package_refs", tuple(self.package_refs))
object.__setattr__(self, "activation_refs", tuple(self.activation_refs))
@classmethod
def from_markitect_contract(
cls,
*,
graph_id: str,
contract_graph_id: str,
data: dict[str, Any],
graph_namespace: dict[str, Any] | None = None,
) -> "MemoryEventRecord":
kind = str(data.get("kind") or data.get("type") or "recorded")
timestamp = str(data.get("timestamp") or data.get("at") or utc_now().isoformat())
contract_event_id = str(data.get("id") or f"event:{stable_suffix(kind, timestamp, data)}")
return cls(
graph_id=graph_id,
contract_graph_id=contract_graph_id,
contract_event_id=contract_event_id,
kind=kind,
timestamp=timestamp,
namespace=dict(data.get("namespace") or graph_namespace or {}),
actor_id=_optional_str(data.get("actor") or data.get("actor_id")),
thread=_optional_str(data.get("thread")),
task=_optional_str(data.get("task")),
node_updates=tuple(_mapping_list(data.get("node_updates") or data.get("nodes"))),
edge_updates=tuple(_mapping_list(data.get("edge_updates") or data.get("edges"))),
package_refs=tuple(_string_list(data.get("package_refs") or data.get("packages"))),
activation_refs=tuple(_string_list(data.get("activation_refs") or data.get("activations"))),
branch=dict(data.get("branch") or {}),
policy=dict(data.get("policy") or {}),
metadata=dict(data.get("metadata") or {}),
)
def to_dict(self) -> dict[str, Any]:
return compact_dict(
{
"event_id": self.event_id,
"graph_id": self.graph_id,
"contract_graph_id": self.contract_graph_id,
"contract_event_id": self.contract_event_id,
"kind": self.kind,
"timestamp": self.timestamp,
"namespace": dict(self.namespace),
"actor_id": self.actor_id,
"thread": self.thread,
"task": self.task,
"node_updates": [dict(item) for item in self.node_updates],
"edge_updates": [dict(item) for item in self.edge_updates],
"package_refs": list(self.package_refs),
"activation_refs": list(self.activation_refs),
"branch": dict(self.branch),
"policy": dict(self.policy),
"metadata": dict(self.metadata),
"appended_at": self.appended_at,
}
)
@dataclass(frozen=True)
class MemoryGraphImportResult:
graph_id: str
contract_graph_id: str
schema_version: str
title: str = ""
intent: str = ""
namespace: dict[str, Any] = field(default_factory=dict)
profile: MemoryProfileRecord | None = None
nodes: tuple[MemoryNodeRecord, ...] = ()
edges: tuple[MemoryEdgeRecord, ...] = ()
events: tuple[MemoryEventRecord, ...] = ()
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
object.__setattr__(self, "nodes", tuple(self.nodes))
object.__setattr__(self, "edges", tuple(self.edges))
object.__setattr__(self, "events", tuple(self.events))
@classmethod
def from_markitect_contract(
cls,
graph_contract: dict[str, Any],
*,
profile_contract: dict[str, Any] | None = None,
) -> "MemoryGraphImportResult":
schema_version = str(graph_contract.get("schema_version") or graph_contract.get("schema") or "")
if schema_version != MARKITECT_MEMORY_GRAPH_SCHEMA_VERSION:
raise ValueError(f"Unsupported memory graph schema `{schema_version}`.")
contract_graph_id = str(graph_contract.get("id") or "")
if not contract_graph_id:
raise ValueError("Memory graph contract must declare an id.")
graph_id = runtime_id("memgraph", schema_version, contract_graph_id)
namespace = dict(graph_contract.get("namespace") or {})
nodes = tuple(
MemoryNodeRecord.from_markitect_contract(
graph_id=graph_id,
contract_graph_id=contract_graph_id,
data=node,
graph_namespace=namespace,
)
for node in _mapping_values(graph_contract.get("nodes"))
)
node_id_by_contract_id = {node.contract_node_id: str(node.node_id) for node in nodes}
edges = tuple(
MemoryEdgeRecord.from_markitect_contract(
graph_id=graph_id,
contract_graph_id=contract_graph_id,
data=edge,
node_id_by_contract_id=node_id_by_contract_id,
)
for edge in _mapping_values(graph_contract.get("edges"))
)
events = tuple(
MemoryEventRecord.from_markitect_contract(
graph_id=graph_id,
contract_graph_id=contract_graph_id,
data=event,
graph_namespace=namespace,
)
for event in _mapping_values(graph_contract.get("events"))
)
profile = MemoryProfileRecord.from_markitect_contract(profile_contract) if profile_contract else None
return cls(
graph_id=graph_id,
contract_graph_id=contract_graph_id,
schema_version=schema_version,
title=str(graph_contract.get("title") or graph_contract.get("name") or ""),
intent=str(graph_contract.get("intent") or graph_contract.get("description") or ""),
namespace=namespace,
profile=profile,
nodes=nodes,
edges=edges,
events=events,
metadata=dict(graph_contract.get("metadata") or {}),
)
def to_dict(self) -> dict[str, Any]:
return compact_dict(
{
"graph_id": self.graph_id,
"contract_graph_id": self.contract_graph_id,
"schema_version": self.schema_version,
"title": self.title,
"intent": self.intent,
"namespace": dict(self.namespace),
"profile": self.profile.to_dict() if self.profile else None,
"nodes": [node.to_dict() for node in self.nodes],
"edges": [edge.to_dict() for edge in self.edges],
"events": [event.to_dict() for event in self.events],
"metadata": dict(self.metadata),
}
)
def runtime_id(prefix: str, *parts: Any) -> str:
return f"{prefix}_{stable_suffix(*parts)}"
def stable_suffix(*parts: Any) -> str:
digest = mapping_digest(parts)
return digest.removeprefix("sha256:")[:24]
def _mapping_values(value: Any) -> list[dict[str, Any]]:
if value is None:
return []
if isinstance(value, dict):
items: list[dict[str, Any]] = []
for key, raw in value.items():
if not isinstance(raw, dict):
continue
item = dict(raw)
item.setdefault("id", str(key))
items.append(item)
return items
return _mapping_list(value)
def _mapping_list(value: Any) -> list[dict[str, Any]]:
if value is None:
return []
if isinstance(value, dict):
return [dict(value)]
if isinstance(value, list):
return [dict(item) for item in value if isinstance(item, dict)]
if isinstance(value, tuple):
return [dict(item) for item in value if isinstance(item, dict)]
return []
def _string_list(value: Any) -> list[str]:
if value is None:
return []
if isinstance(value, (list, tuple)):
return [str(item) for item in value if item is not None]
return [str(value)]
def _optional_str(value: Any) -> str | None:
if value is None:
return None
text = str(value)
return text if text else None
def _optional_int(value: Any) -> int | None:
if value is None or value == "":
return None
return int(value)
def _optional_float(value: Any) -> float | None:
if value is None or value == "":
return None
return float(value)

View File

@@ -9,6 +9,7 @@ from .blob_storage import (
digest_storage_key,
)
from .ingestion import DirectorySourceConnector, FormatExtractor, SourceConnector
from .memory import MemoryGraphRepository
from .policy import AllowAllPolicyGateway, PolicyGateway
from .repositories import AssetRegistryRepository
@@ -23,6 +24,7 @@ __all__ = [
"digest_storage_key",
"DirectorySourceConnector",
"FormatExtractor",
"MemoryGraphRepository",
"PolicyGateway",
"SourceConnector",
]

View File

@@ -0,0 +1,47 @@
"""Ports for operational memory graph state."""
from __future__ import annotations
from typing import Protocol
from kontextual_engine.core import (
LifecycleState,
MemoryEdgeRecord,
MemoryEventRecord,
MemoryNodeRecord,
MemoryProfileRecord,
)
class MemoryGraphRepository(Protocol):
def save_memory_profile(self, profile: MemoryProfileRecord) -> MemoryProfileRecord: ...
def get_memory_profile(self, profile_id: str) -> MemoryProfileRecord: ...
def save_memory_node(self, node: MemoryNodeRecord) -> MemoryNodeRecord: ...
def get_memory_node(self, node_id: str) -> MemoryNodeRecord: ...
def list_memory_nodes(
self,
*,
graph_id: str | None = None,
kind: str | None = None,
lifecycle: LifecycleState | str | None = None,
) -> list[MemoryNodeRecord]: ...
def save_memory_edge(self, edge: MemoryEdgeRecord) -> MemoryEdgeRecord: ...
def get_memory_edge(self, edge_id: str) -> MemoryEdgeRecord: ...
def list_memory_edges(
self,
*,
graph_id: str | None = None,
source_node_id: str | None = None,
target_node_id: str | None = None,
) -> list[MemoryEdgeRecord]: ...
def append_memory_event(self, event: MemoryEventRecord) -> MemoryEventRecord: ...
def get_memory_event(self, event_id: str) -> MemoryEventRecord: ...
def list_memory_events(
self,
*,
graph_id: str | None = None,
kind: str | None = None,
) -> list[MemoryEventRecord]: ...

View File

@@ -7,6 +7,7 @@ from .asset_service import (
)
from .content_service import RepresentationContentResult, RepresentationContentStream, RepresentationContentService
from .ingestion_service import AssetIngestionResult, AssetIngestionService
from .memory_service import MemoryGraphImportSummary, MemoryRuntimeService
from .retrieval_service import (
AssetQueryItem,
AssetQueryRequest,
@@ -53,6 +54,8 @@ __all__ = [
"ContextEntityQueryRequest",
"ContextEntityQueryResult",
"LexicalIndexRefreshResult",
"MemoryGraphImportSummary",
"MemoryRuntimeService",
"RelationshipChangeResult",
"RepresentationContentResult",
"RepresentationContentStream",

View File

@@ -0,0 +1,100 @@
"""Application service for operational memory graph imports."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from kontextual_engine.core import (
AuditEvent,
AuditOutcome,
MemoryGraphImportResult,
OperationContext,
)
from kontextual_engine.errors import ValidationError
from kontextual_engine.ports import MemoryGraphRepository
@dataclass(frozen=True)
class MemoryGraphImportSummary:
graph_id: str
contract_graph_id: str
imported_nodes: int
imported_edges: int
appended_events: int
profile_id: str | None = None
audit_event: AuditEvent | None = None
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
data = {
"graph_id": self.graph_id,
"contract_graph_id": self.contract_graph_id,
"profile_id": self.profile_id,
"imported_nodes": self.imported_nodes,
"imported_edges": self.imported_edges,
"appended_events": self.appended_events,
"metadata": dict(self.metadata),
}
if self.audit_event:
data["audit_event"] = self.audit_event.to_dict()
return data
class MemoryRuntimeService:
def __init__(self, repository: MemoryGraphRepository) -> None:
self.repository = repository
def import_markitect_graph(
self,
graph_contract: dict[str, Any],
*,
profile_contract: dict[str, Any] | None = None,
context: OperationContext | None = None,
) -> MemoryGraphImportSummary:
try:
imported = MemoryGraphImportResult.from_markitect_contract(
graph_contract,
profile_contract=profile_contract,
)
except ValueError as exc:
raise ValidationError(str(exc), details={"operation": "memory.import_markitect_graph"}) from exc
if imported.profile:
self.repository.save_memory_profile(imported.profile)
for node in imported.nodes:
self.repository.save_memory_node(node)
for edge in imported.edges:
self.repository.save_memory_edge(edge)
for event in imported.events:
self.repository.append_memory_event(event)
audit_event = None
if context:
audit_event = AuditEvent.from_context(
"memory.import_markitect_graph",
f"memory-graph:{imported.graph_id}",
AuditOutcome.SUCCESS,
context,
details={
"contract_graph_id": imported.contract_graph_id,
"profile_id": imported.profile.profile_id if imported.profile else None,
"nodes": len(imported.nodes),
"edges": len(imported.edges),
"events": len(imported.events),
},
)
return MemoryGraphImportSummary(
graph_id=imported.graph_id,
contract_graph_id=imported.contract_graph_id,
profile_id=imported.profile.profile_id if imported.profile else None,
imported_nodes=len(imported.nodes),
imported_edges=len(imported.edges),
appended_events=len(imported.events),
audit_event=audit_event,
metadata={
"schema_version": imported.schema_version,
"title": imported.title,
"intent": imported.intent,
},
)