Skip to content
Open
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ npx @modelcontextprotocol/inspector@latest http://127.0.0.1:8201/sse --cli --met
**Available tools:**
- `get_entities(task: str, entity_type: str = "guideline", include_public: bool = False)`: Get relevant entities for a specific task. Set `include_public=True` to merge in public entities from all other namespaces; those results are annotated with `[public: {owner_id}]`.
- `get_guidelines(task: str)`: Get relevant guidelines for a specific task (backward compatibility alias for `get_entities`).
- `get_relevant_guidelines(task: str, top_k: int | None, core_support: int | None)`: Retrieve the always-on guideline core plus a task-relevant dosage.
- `list_entities(...)`: Return structured, filtered, cursor-paginated entity inventory for user and administrative UIs.
- `get_entity(entity_id: str, user_id: str | None, record_access: bool = True)`: Return one structured entity, enforcing ownership when a caller ID is supplied.
- `patch_entity_metadata(entity_id: str, metadata_patch: str, user_id: str | None)`: Merge JSON metadata through the memory hook seam.
- `record_access(entity_ids: list[str], accessed_at: str | None)`: Explicitly stamp the retention engine's `last_accessed` signal.
- `validate_retention_policy(policy: str)`: Validate and normalize a JSON retention policy without scanning data.
- `run_retention(policy: str, dry_run: bool = True, as_of: str | None, scan_limit: int | None)`: Dry-run or apply retention and return a structured, entity-linked report.
- `get_compliance_status()`: Report backend health, retention availability, hook coverage, and configured plugin health.
- `save_trajectory(trajectory_data: str, task_id: str | None, owner_id: str | None)`: Save a conversation trajectory and generate new guidelines.
- `create_entity(content: str, entity_type: str, metadata: str | None, enable_conflict_resolution: bool, owner_id: str | None, visibility: str = "private")`: Create a single entity. Pass `visibility="public"` and `owner_id` to make it immediately discoverable by other namespaces.
- `publish_entity(entity_id: str, user_id: str | None)`: Make an entity publicly visible to all namespaces. Records the caller as owner and stamps `published_at`.
Expand Down
27 changes: 27 additions & 0 deletions altk_evolve/backend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,33 @@ def search_entities(
results = self._search_entities_impl(namespace_id, query, filters, limit)
return dispatch_memory_post_read(self, namespace_id, results, query=query, filters=filters)

def scan_entities(self, namespace_id: str, filters: dict | None = None, limit: int = 100) -> list[RecordedEntity]:
"""Read entities for administrative work without firing read hooks."""
return self._search_entities_impl(namespace_id, query=None, filters=filters, limit=limit)

def set_entity_created_at(self, namespace_id: str, entity_id: str, created_at: datetime.datetime) -> RecordedEntity:
"""Set an imported entity's persisted creation time without a public-read hook.

This is intentionally an administrative import operation. It uses the
backend's normal patch/persist path without firing public-read hooks.
"""
entities = self.scan_entities(namespace_id, filters={"id": entity_id}, limit=1)
if not entities:
raise EvolveException(f"Entity '{entity_id}' not found in namespace '{namespace_id}'")
entity = entities[0]
self._patch_entity(
namespace_id,
entity_id,
entity.type,
serialize_content(entity.content),
int(created_at.timestamp()),
dict(entity.metadata or {}),
)
updated = self.scan_entities(namespace_id, filters={"id": entity_id}, limit=1)
if not updated:
raise EvolveException(f"Entity '{entity_id}' disappeared after timestamp update")
return updated[0]

@abstractmethod
def _search_entities_impl(
self, namespace_id: str, query: str | None = None, filters: dict | None = None, limit: int = 10
Expand Down
24 changes: 22 additions & 2 deletions altk_evolve/frontend/client/evolve_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,21 @@ def get_all_entities(self, namespace_id: str, filters: dict | None = None, limit
"""Get all entities from a namespace."""
return self.search_entities(namespace_id, query=None, filters=filters, limit=limit)

def scan_entities(self, namespace_id: str, filters: dict | None = None, limit: int = 100) -> list[RecordedEntity]:
"""Read entities for an administrative scan without recording access.

Retention and inventory scans are not user recalls and must not refresh
``metadata.last_accessed`` through ``memory_post_read``. This method
intentionally uses the backend's administrative scan seam, so callers must
not use it for ordinary memory retrieval where read transforms and
access auditing are expected.
"""
return self.backend.scan_entities(namespace_id, filters=filters, limit=limit)

def set_entity_created_at(self, namespace_id: str, entity_id: str, created_at: datetime.datetime) -> RecordedEntity:
"""Administratively stamp an imported entity and persist the result."""
return self.backend.set_entity_created_at(namespace_id, entity_id, created_at)

def delete_entity_by_id(self, namespace_id: str, entity_id: str) -> None:
"""Delete a specific entity by its ID."""
self.backend.delete_entity_by_id(namespace_id, entity_id)
Expand All @@ -127,7 +142,7 @@ def patch_entity_metadata(self, namespace_id: str, entity_id: str, metadata_upda
"""Merge metadata_updates into an entity without touching content or ID."""
return self.backend.update_entity_metadata(namespace_id, entity_id, metadata_updates)

def record_access(self, namespace_id: str, entity_ids: list[str], when: datetime.datetime | None = None) -> None:
def record_access(self, namespace_id: str, entity_ids: list[str], when: datetime.datetime | None = None) -> list[str]:
"""Stamp ``metadata.last_accessed`` (ISO-8601 UTC) on the given entities.

This is the *explicit* half of the access-stamping story and the signal
Expand All @@ -145,17 +160,22 @@ def record_access(self, namespace_id: str, entity_ids: list[str], when: datetime

Failures on individual ids are logged and skipped rather than raised —
recording an access must never break the flow it rode in on.

Returns the IDs that were stamped successfully.
"""
# Imported here (not at module scope) to keep the client import cheap;
# build_access_stamps is a pure function with no cpex dependency.
from altk_evolve.hooks.plugins.access_stamp import build_access_stamps

moment = when if when is not None else datetime.datetime.now(datetime.UTC)
updated_ids: list[str] = []
for entity_id, patch in build_access_stamps([{"id": eid} for eid in entity_ids], now=lambda: moment):
try:
self.patch_entity_metadata(namespace_id, entity_id, patch)
updated_ids.append(entity_id)
except Exception:
logger.warning("record_access: failed to stamp entity %s", entity_id, exc_info=True)
return updated_ids

def get_public_entities(
self,
Expand Down Expand Up @@ -254,7 +274,7 @@ def consolidate_guidelines(self, namespace_id: str, threshold: float | None = No
# llm_pre_call; the write-back still fires memory_pre_write and the
# deletes still fire memory_pre_delete.
limit = 10000
entities = self.backend._search_entities_impl(namespace_id, query=None, filters={"type": "guideline"}, limit=limit)
entities = self.backend.scan_entities(namespace_id, filters={"type": "guideline"}, limit=limit)
if len(entities) >= limit:
logger.warning(
"Fetched %d entities (hit limit=%d); consolidation may be incomplete. Consider increasing the limit.",
Expand Down
Loading
Loading