Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from infinity_context_core.features.memory_facts.public import (
FEATURE_ID,
FactEligibilityPolicy,
FactRelationSnapshot,
FactSupersessionRelation,
FactTemporalDecision,
FactTemporalDecisionType,
Expand All @@ -22,6 +23,10 @@
MemoryFactUnitOfWorkFactoryPort,
)

from infinity_context_adapters.features.memory_facts.in_memory_relation_store import (
InMemoryFactRelationRepository,
)

_FactKey = tuple[str, str, str | None, str]
_IdempotencyKey = tuple[str, str, str | None, FactTemporalDecisionType, str]
_OperationReceiptKey = tuple[str, str, str | None, str, str]
Expand All @@ -36,6 +41,7 @@ def __init__(self, facts: Iterable[MemoryFactSnapshot] = ()) -> None:
self._decision_idempotency: dict[_IdempotencyKey, str] = {}
self._operation_receipts: dict[_OperationReceiptKey, MemoryFactOperationReceipt] = {}
self._supersessions: list[FactSupersessionRelation] = []
self._relations: dict[str, FactRelationSnapshot] = {}
self._revision = 0
for fact in facts:
self._put(fact, allow_existing=False)
Expand All @@ -51,6 +57,7 @@ def snapshot(
dict[_IdempotencyKey, str],
dict[_OperationReceiptKey, MemoryFactOperationReceipt],
list[FactSupersessionRelation],
dict[str, FactRelationSnapshot],
int,
]:
return (
Expand All @@ -61,6 +68,7 @@ def snapshot(
dict(self._decision_idempotency),
dict(self._operation_receipts),
list(self._supersessions),
dict(self._relations),
self._revision,
)

Expand All @@ -73,6 +81,7 @@ def replace(
decision_idempotency: dict[_IdempotencyKey, str],
operation_receipts: dict[_OperationReceiptKey, MemoryFactOperationReceipt],
supersessions: list[FactSupersessionRelation],
relations: dict[str, FactRelationSnapshot],
expected_revision: int,
) -> None:
if self._revision != expected_revision:
Expand All @@ -84,6 +93,7 @@ def replace(
self._decision_idempotency = dict(decision_idempotency)
self._operation_receipts = dict(operation_receipts)
self._supersessions = list(supersessions)
self._relations = dict(relations)
self._revision += 1

def facts(self) -> tuple[MemoryFactSnapshot, ...]:
Expand Down Expand Up @@ -507,6 +517,7 @@ def __init__(self, state: _InMemoryMemoryFactState | None = None) -> None:
self._working_decision_idempotency,
self._working_operation_receipts,
self._working_supersessions,
self._working_relations,
self._base_revision,
) = self._state.snapshot()
self.facts = InMemoryMemoryFactRepository.transactional(
Expand All @@ -523,6 +534,9 @@ def __init__(self, state: _InMemoryMemoryFactState | None = None) -> None:
self._working_operation_receipts
)
self.supersessions = InMemoryFactSupersessionRepository(self._working_supersessions)
self.relations = InMemoryFactRelationRepository(
self._working_relations, self._working_facts, self._working_supersessions
)
self._committed = False

async def __aenter__(self) -> InMemoryMemoryFactUnitOfWork:
Expand Down Expand Up @@ -554,6 +568,7 @@ async def commit(self) -> None:
self._working_decision_idempotency,
self._working_operation_receipts,
self._working_supersessions,
self._working_relations,
self._base_revision,
)
self._committed = True
Expand All @@ -567,6 +582,7 @@ async def rollback(self) -> None:
self._working_decision_idempotency,
self._working_operation_receipts,
self._working_supersessions,
self._working_relations,
self._base_revision,
) = self._state.snapshot()
self.facts = InMemoryMemoryFactRepository.transactional(
Expand All @@ -582,6 +598,9 @@ async def rollback(self) -> None:
self._working_operation_receipts
)
self.supersessions = InMemoryFactSupersessionRepository(self._working_supersessions)
self.relations = InMemoryFactRelationRepository(
self._working_relations, self._working_facts, self._working_supersessions
)
self._committed = False


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""Minimal transaction-local relation adapter for focused feature tests."""

from __future__ import annotations

from collections.abc import Mapping

from infinity_context_core.features.memory_facts.public import (
FactRelationConflict,
FactRelationSnapshot,
FactRelationStatus,
FactRelationType,
FactSupersessionRelation,
MemoryFactScope,
MemoryFactSnapshot,
)


class InMemoryFactRelationRepository:
def __init__(
self,
relations: dict[str, FactRelationSnapshot],
facts: Mapping[tuple[str, str, str | None, str], MemoryFactSnapshot],
supersessions: list[FactSupersessionRelation],
) -> None:
self._relations = relations
self._facts = facts
self._supersessions = supersessions

def _all(self) -> tuple[FactRelationSnapshot, ...]:
return tuple(self._relations.values()) + tuple(
FactRelationSnapshot(
relation_id=item.relation_id,
space_id=item.scope.space_id,
memory_scope_id=item.scope.memory_scope_id,
thread_id=item.scope.thread_id,
source_fact_id=item.successor_fact_id,
target_fact_id=item.predecessor_fact_id,
relation_type=FactRelationType.SUPERSEDES,
reason=f"temporal_decision:{item.decision_id}",
status=FactRelationStatus.ACTIVE,
observed_at=item.created_at,
valid_from=item.effective_at,
valid_to=None,
created_at=item.created_at,
updated_at=item.created_at,
)
for item in self._supersessions
)

async def get(self, relation_id: str, *, scope: MemoryFactScope) -> FactRelationSnapshot | None:
return next(
(
item
for item in self._all()
if item.relation_id == relation_id and _in_scope(item, scope)
),
None,
)

async def find_active(
self, *, source_fact_id: str, target_fact_id: str, relation_type: FactRelationType
) -> FactRelationSnapshot | None:
return next(
(
item
for item in self._all()
if item.source_fact_id == source_fact_id
and item.target_fact_id == target_fact_id
and item.relation_type == relation_type
and item.status == FactRelationStatus.ACTIVE
),
None,
)

async def create(self, relation: FactRelationSnapshot) -> FactRelationSnapshot:
if any(item.relation_id == relation.relation_id for item in self._all()):
raise FactRelationConflict("Fact relation already exists")
if (
await self.find_active(
source_fact_id=relation.source_fact_id,
target_fact_id=relation.target_fact_id,
relation_type=relation.relation_type,
)
is not None
):
raise FactRelationConflict("Active fact relation already exists")
self._relations[relation.relation_id] = relation
return relation

async def save(self, relation: FactRelationSnapshot) -> FactRelationSnapshot:
current = await self.get(
relation.relation_id, scope=MemoryFactScope(relation.space_id, relation.memory_scope_id)
)
if current is None:
raise LookupError("Fact relation not found")
saved = current.delete(now=relation.updated_at)
self._relations[relation.relation_id] = saved
return saved

async def get_related_fact(
self, fact_id: str, *, scope: MemoryFactScope
) -> MemoryFactSnapshot | None:
return next(
(
fact
for fact in self._facts.values()
if fact.identity.fact_id == fact_id
and fact.identity.scope.space_id == scope.space_id
and fact.identity.scope.memory_scope_id == scope.memory_scope_id
),
None,
)

async def list_for_fact(
self,
*,
fact_id: str,
scope: MemoryFactScope,
status: str | None,
limit: int,
enforce_code_scope: bool = False,
repository_id: str | None = None,
code_scope_id: str | None = None,
) -> tuple[FactRelationSnapshot, ...]:
candidates = []
for relation in self._all():
if not _in_scope(relation, scope):
continue
if fact_id not in (relation.source_fact_id, relation.target_fact_id):
continue
if status is not None and relation.status != status:
continue
if enforce_code_scope:
other_id = (
relation.target_fact_id
if relation.source_fact_id == fact_id
else relation.source_fact_id
)
other = await self.get_related_fact(other_id, scope=scope)
if other is None or other.visibility.classification == "restricted":
continue
if other.code_scope is not None and not other.code_scope.is_visible_in(
repository_id=repository_id, code_scope_id=code_scope_id
):
continue
candidates.append(relation)
return tuple(
sorted(candidates, key=lambda item: (item.updated_at, item.relation_id), reverse=True)[
:limit
]
)


def _in_scope(relation: FactRelationSnapshot, scope: MemoryFactScope) -> bool:
return (relation.space_id, relation.memory_scope_id) == (scope.space_id, scope.memory_scope_id)
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
memory_fact_source_ref_row_to_domain,
memory_fact_source_ref_to_row,
)
from infinity_context_adapters.features.memory_facts.postgres_relation_store import (
PostgresFactRelationRepository,
)
from infinity_context_adapters.features.memory_facts.postgres_temporal_decision_store import (
PostgresFactSupersessionRepository,
PostgresFactTemporalDecisionRepository,
Expand Down Expand Up @@ -501,6 +504,7 @@ class PostgresMemoryFactTransaction:

def __init__(self, session: AsyncSession, *, now: datetime) -> None:
self._session = session
self.relations = PostgresFactRelationRepository(session)
self.facts = PostgresMemoryFactStore(session)
self.temporal_decisions = PostgresFactTemporalDecisionRepository(session)
self.supersessions = PostgresFactSupersessionRepository(session)
Expand Down Expand Up @@ -555,6 +559,7 @@ async def __aenter__(self) -> PostgresMemoryFactUnitOfWork:
self._session = self._session_factory()
now = self._clock.now()
self._transaction = PostgresMemoryFactTransaction(self._session, now=now)
self.relations = self._transaction.relations
self.facts = self._transaction.facts
self.temporal_decisions = self._transaction.temporal_decisions
self.supersessions = self._transaction.supersessions
Expand Down
Loading
Loading