Source code for ralph.mcp.multimodal.resources
"""URI builder/parser and session-scoped manifest for ralph://media resources.
The manifest tracks every resource_reference artifact emitted during a session
so they can be listed via resources/list and retrieved via resources/read.
"""
from __future__ import annotations
import hashlib
import re
import uuid
from collections import OrderedDict
from dataclasses import dataclass, field
from ralph.mcp.multimodal._manifest_entry import ManifestEntry
from ralph.mcp.multimodal._media_entry_extras import MediaEntryExtras
from ralph.mcp.multimodal._media_source import MediaSource
_RALPH_MEDIA_PREFIX = "ralph://media/"
_URI_PATTERN = re.compile(
r"^ralph://media/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$"
)
MEDIA_URI_TEMPLATE = "ralph://media/{artifact_id}"
# wt-024 M2: default cap on retained manifest entries. Matches the
# shipped-in-production knob documented in the wt-024 plan. 256 is
# generous for any realistic session; pathological unbounded growth
# is the only thing this cap prevents.
_DEFAULT_MAX_ENTRIES = 256
[docs]
def build_media_uri(artifact_id: str) -> str:
"""Build a ralph://media/{artifact_id} URI."""
return f"{_RALPH_MEDIA_PREFIX}{artifact_id}"
[docs]
def parse_media_uri(uri: str) -> str | None:
"""Parse a ralph://media/{artifact_id} URI and return artifact_id, or None."""
m = _URI_PATTERN.match(uri)
if m is None:
return None
return m.group(1)
[docs]
def new_artifact_id() -> str:
"""Generate a new random artifact UUID."""
return str(uuid.uuid4())
[docs]
def build_media_identity(
*,
modality: str,
mime_type: str,
title: str,
source: MediaSource | None = None,
) -> str:
"""Build a stable identity for deduping repeated live artifacts."""
src = source or MediaSource()
if src.source_uri:
return f"source-uri:{modality}:{src.source_uri}"
if src.source_path:
return f"source-path:{modality}:{src.source_path}"
if src.raw_bytes is not None:
digest = hashlib.sha256(src.raw_bytes).hexdigest()
return f"payload:{modality}:{mime_type}:{title}:{digest}"
return f"artifact:{modality}:{mime_type}:{title}"
[docs]
@dataclass
class MediaManifest:
"""Session-scoped manifest of all multimodal resource references."""
# wt-024 M2: bounded retention. ``_entries`` is an ``OrderedDict``
# so we can evict the oldest entry (FIFO) when a NEW identity
# pushes the manifest past ``max_entries``. Re-adding an
# EXISTING identity dedups in place and PRESERVES its original
# insertion position so ``list_entries()`` (and the downstream
# ``resources/list`` response) keep stable ordering across
# duplicate adds. Typed as ``OrderedDict`` (not ``dict``) so
# mypy --strict can verify ``popitem(last=False)`` is a valid
# call.
_entries: OrderedDict[str, ManifestEntry] = field(
default_factory=OrderedDict,
)
_identity_index: dict[str, str] = field(default_factory=dict)
# Maximum number of distinct artifact_ids retained in ``_entries``.
# When a NEW identity pushes ``len(_entries) > max_entries`` we
# evict the oldest artifact and clear its mapping from
# ``_identity_index``. Default keeps existing small-fixture
# behaviour unchanged.
max_entries: int = _DEFAULT_MAX_ENTRIES
[docs]
def add(
self,
*,
title: str,
mime_type: str,
modality: str,
raw_bytes: bytes,
extras: MediaEntryExtras | None = None,
) -> ManifestEntry:
"""Add or replace an artifact and return its manifest entry.
Re-adding an EXISTING identity dedups in place and PRESERVES
the original insertion order so the ``resources/list`` output
order stays stable across duplicate adds (wt-024 analysis
feedback: reordering on re-add was an externally visible
behavior change that the memory cap did not require).
"""
xt = extras or MediaEntryExtras()
resolved_identity = xt.identity_key or build_media_identity(
modality=modality,
mime_type=mime_type,
title=title,
source=MediaSource(
source_path=xt.source_path,
source_uri=xt.source_uri,
raw_bytes=raw_bytes,
),
)
artifact_id = self._identity_index.get(
resolved_identity,
xt.artifact_id or new_artifact_id(),
)
uri = build_media_uri(artifact_id)
# wt-024 M2 follow-up (AC-06): when a durable replay source is
# wired at add-time (a byte_loader is supplied), the manifest
# must NOT retain the raw payload in memory. The byte_loader
# is the canonical source for rehydrating bytes later, so
# storing raw_bytes on top of it doubles peak memory per
# entry (up to 256 entries x multi-MB each) for no observable
# benefit. Raw bytes are only retained when the caller has
# no durable replay source — i.e. when neither byte_loader nor
# cache_path is supplied — so the in-memory-only contract for
# legacy callers is preserved verbatim.
retain_raw_bytes = xt.byte_loader is None and not xt.cache_path
stored_raw_bytes = raw_bytes if retain_raw_bytes else None
entry = ManifestEntry(
artifact_id=artifact_id,
uri=uri,
mime_type=mime_type,
title=title,
modality=modality,
identity_key=resolved_identity,
cache_path=xt.cache_path,
source_path=xt.source_path,
source_uri=xt.source_uri,
_raw_bytes=stored_raw_bytes,
_byte_loader=xt.byte_loader,
)
existing = self._entries.get(artifact_id)
self._entries[artifact_id] = entry
self._identity_index[resolved_identity] = artifact_id
if existing is None and len(self._entries) > self.max_entries:
self._evict_oldest()
return entry
def _evict_oldest(self) -> str | None:
"""Pop the oldest ``_entries`` key and drop its identity mappings.
Returns the evicted ``artifact_id`` so callers can log or
observe the eviction. No-op when ``_entries`` is empty.
"""
if not self._entries:
return None
evicted_id, _ = self._entries.popitem(last=False)
# Remove every ``_identity_index`` entry whose value is the
# evicted artifact_id so we never serve a stale dedup to a
# future ``add()`` of the same identity.
stale_identities = [
identity
for identity, mapped_id in self._identity_index.items()
if mapped_id == evicted_id
]
for identity in stale_identities:
del self._identity_index[identity]
return evicted_id
[docs]
def get(self, artifact_id: str) -> ManifestEntry | None:
"""Retrieve a manifest entry by artifact_id, or None if not found."""
return self._entries.get(artifact_id)
[docs]
def list_entries(self) -> list[ManifestEntry]:
"""Return all manifest entries in insertion order."""
return list(self._entries.values())
[docs]
def is_empty(self) -> bool:
"""Return True if no artifacts have been stored."""
return not self._entries
__all__ = [
"MEDIA_URI_TEMPLATE",
"ManifestEntry",
"MediaEntryExtras",
"MediaManifest",
"MediaSource",
"build_media_identity",
"build_media_uri",
"new_artifact_id",
"parse_media_uri",
]