orchestrata_ultimate.knowledge_graph.history

Reference › orchestrata_ultimate › knowledge_graph › history

HistoryManager records and queries a change log for knowledge graph entities (Spec 008, REQ-008-030 and REQ-008-034). record_change builds a ChangeEntry (entity id and type, a ChangeType, optional old and new values, who changed it, and the session id) and appends it to an in-memory list plus per-entity and per-session indexes. The constructor accepts an optional Redis client, but the recording path shown here writes only to those in-process structures, so the log lasts as long as the process. Queries are get_change_log (filter by entity, entity type or change type, with a limit of 100 by default), get_timeline (a time range), get_session_changes and get_entity_summary; total_entries and clear complete the surface. It is a separate, lighter record than the versions kept by temporal. Types come from types and types_enums. Only tests import it.

At a glance

Source src/orchestrata_ultimate/knowledge_graph/history.py
Lines of code 135
Classes 1
Functions 1
Imports (internal) orchestrata_ultimate.core.base.traces, orchestrata_ultimate.knowledge_graph.types
Imported by none
Tests that import it tests/generated_recovery/test_knowledge_graph_history.py, tests/knowledge_graph/test_history.py, tests/knowledge_graph/test_knowledge_performance.py, tests/knowledge_graph/test_knowledge_signatures.py, tests/knowledge_graph/test_knowledge_temporal.py

Class diagram

Class diagram orchestrata_ultimate.knowledge_graph.history

Dependency diagram

Dependency diagram orchestrata_ultimate.knowledge_graph.history

API

Classes

HistoryManager

Decorators: traces(spec='008', story='US-001', reqs=['REQ-008-030', 'REQ-008-034']) · Line 31

Records and queries change history.

Method Signature Summary
__init__ __init__(redis: redis.Redis | None = None) -> None
record_change record_change(entity_id: str, entity_type: str, change_type: ChangeType, old_value: dict[str, Any] | None = None, new_value: dict[str, Any] | None = None, changed_by: str = '', session_id: str = '') -> ChangeEntry Record a change to an entity.
get_change_log get_change_log(entity_id: str | None = None, entity_type: str | None = None, change_type: ChangeType | None = None, limit: int = 100) -> list[ChangeEntry] Get change log with optional filters.
get_timeline get_timeline(start: datetime | None = None, end: datetime | None = None, limit: int = 100) -> list[ChangeEntry] Get changes within a time range.
get_session_changes get_session_changes(session_id: str) -> list[ChangeEntry] Get all changes in a session.
get_entity_summary get_entity_summary(entity_id: str) -> dict[str, Any] Get a summary of changes for an entity.
total_entries property total_entries() -> int
clear clear() -> None Clear all history.

Functions

Function Signature Summary
_utc_now _utc_now() -> datetime

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_ultimate/knowledge_graph/history.py

# mypy: ignore-errors
"""Change history for the knowledge graph.

Records and queries changes to knowledge graph entities
with support for timeline queries and filtering.
"""

# Spec: 008 - Knowledge Graph

from __future__ import annotations

import logging
from collections import defaultdict
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any

from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.knowledge_graph.types import ChangeEntry, ChangeType

if TYPE_CHECKING:
    import redis

logger = logging.getLogger(__name__)


def _utc_now() -> datetime:
    return datetime.now(UTC)


@traces(spec="008", story="US-001", reqs=["REQ-008-030", "REQ-008-034"])
class HistoryManager:
    """Records and queries change history."""

    def __init__(self, redis: redis.Redis | None = None) -> None:
        self._redis = redis
        self._entries: list[ChangeEntry] = []
        self._by_entity: dict[str, list[ChangeEntry]] = defaultdict(list)
        self._by_session: dict[str, list[ChangeEntry]] = defaultdict(list)

    @traces(spec="008", story="US-001", reqs=["REQ-008-030", "REQ-008-034"])
    def record_change(
        self,
        entity_id: str,
        entity_type: str,
        change_type: ChangeType,
        old_value: dict[str, Any] | None = None,
        new_value: dict[str, Any] | None = None,
        changed_by: str = "",
        session_id: str = "",
    ) -> ChangeEntry:
        """Record a change to an entity."""
        entry = ChangeEntry(
            entity_id=entity_id,
            entity_type=entity_type,
            change_type=change_type,
            old_value=old_value,
            new_value=new_value,
            changed_by=changed_by,
            session_id=session_id,
        )
        self._entries.append(entry)
        self._by_entity[entity_id].append(entry)
        if session_id:
            self._by_session[session_id].append(entry)
        return entry

    @traces(spec="008", story="US-001", reqs=["REQ-008-030"])
    def get_change_log(
        self,
        entity_id: str | None = None,
        entity_type: str | None = None,
        change_type: ChangeType | None = None,
        limit: int = 100,
    ) -> list[ChangeEntry]:
        """Get change log with optional filters."""
        if entity_id:
            entries = self._by_entity.get(entity_id, [])
        else:
            entries = self._entries

        filtered = entries
        if entity_type:
            filtered = [e for e in filtered if e.entity_type == entity_type]
        if change_type:
            filtered = [e for e in filtered if e.change_type == change_type]

        return filtered[-limit:]

    @traces(spec="008", story="US-001", reqs=["REQ-008-028"])
    def get_timeline(
        self,
        start: datetime | None = None,
        end: datetime | None = None,
        limit: int = 100,
    ) -> list[ChangeEntry]:
        """Get changes within a time range."""
        entries = self._entries
        if start:
            entries = [e for e in entries if e.changed_at >= start]
        if end:
            entries = [e for e in entries if e.changed_at <= end]
        return entries[-limit:]

    @traces(spec="008", story="US-001", reqs=["REQ-008-043"])
    def get_session_changes(self, session_id: str) -> list[ChangeEntry]:
        """Get all changes in a session."""
        return list(self._by_session.get(session_id, []))

    def get_entity_summary(self, entity_id: str) -> dict[str, Any]:
        """Get a summary of changes for an entity."""
        entries = self._by_entity.get(entity_id, [])
        if not entries:
            return {"entity_id": entity_id, "total_changes": 0}

        type_counts: dict[str, int] = defaultdict(int)
        for entry in entries:
            type_counts[entry.change_type.value] += 1

        return {
            "entity_id": entity_id,
            "total_changes": len(entries),
            "first_change": entries[0].changed_at.isoformat(),
            "last_change": entries[-1].changed_at.isoformat(),
            "change_types": dict(type_counts),
        }

    @property
    def total_entries(self) -> int:
        return len(self._entries)

    def clear(self) -> None:
        """Clear all history."""
        self._entries.clear()
        self._by_entity.clear()
        self._by_session.clear()