Files
memory-gateway/memory_gateway/backend_normalization.py

260 lines
9.5 KiB
Python

"""Offline response normalization helpers for future v2 backend adapters."""
from __future__ import annotations
from typing import Any
from .backend_contracts import (
BackendCommitResult,
BackendOperation,
BackendProducedRef,
BackendResultStatus,
BackendRetrieveItem,
BackendRetrieveResult,
BackendWriteResult,
)
from .backend_ref_mapping import map_backend_ref_type
from .schemas_v2 import BackendType
SAFE_METADATA_KEYS = {
"backend_request_id",
"request_id",
"trace_id",
"latency_ms",
"schema_version",
"source_channel",
"reason",
"original_ref_type",
"confidence",
"score",
"version",
"created_at",
}
BLOCKED_METADATA_KEYS = {"content", "raw_request", "messages", "conversation", "transcript"}
def safe_backend_metadata(metadata: dict[str, Any] | None) -> dict[str, Any]:
if not metadata:
return {}
safe: dict[str, Any] = {}
for key, value in metadata.items():
if key in BLOCKED_METADATA_KEYS or key not in SAFE_METADATA_KEYS:
continue
if isinstance(value, (str, int, float, bool)) or value is None:
safe[key] = value
return safe
def normalize_openviking_commit_response(raw: dict[str, Any]) -> BackendCommitResult:
status = _result_status(raw)
refs = [_produced_ref(BackendType.OPENVIKING, item) for item in _extract_ref_items(raw)]
return BackendCommitResult(
backend_type=BackendType.OPENVIKING,
operation=BackendOperation.COMMIT_SESSION,
status=status,
native_id=raw.get("native_id") or raw.get("session_id"),
native_uri=raw.get("native_uri") or raw.get("uri"),
retryable=_retryable_from_raw(BackendType.OPENVIKING, raw),
error_code=raw.get("error_code"),
error_message=raw.get("error") or raw.get("error_message"),
latency_ms=raw.get("latency_ms"),
refs=refs,
metadata=safe_backend_metadata(raw.get("metadata") or raw),
)
def normalize_evermemos_commit_response(raw: dict[str, Any]) -> BackendCommitResult:
status = _result_status(raw)
refs = [_produced_ref(BackendType.EVERMEMOS, item) for item in _extract_ref_items(raw)]
return BackendCommitResult(
backend_type=BackendType.EVERMEMOS,
operation=BackendOperation.COMMIT_SESSION,
status=status,
native_id=raw.get("native_id") or raw.get("session_id"),
native_uri=raw.get("native_uri") or raw.get("uri"),
retryable=_retryable_from_raw(BackendType.EVERMEMOS, raw),
error_code=raw.get("error_code"),
error_message=raw.get("error") or raw.get("error_message"),
latency_ms=raw.get("latency_ms"),
refs=refs,
metadata=safe_backend_metadata(raw.get("metadata") or raw),
)
def normalize_openviking_ingest_response(raw: dict[str, Any]) -> BackendWriteResult:
return _write_result(BackendType.OPENVIKING, raw)
def normalize_evermemos_ingest_response(raw: dict[str, Any]) -> BackendWriteResult:
return _write_result(BackendType.EVERMEMOS, raw)
def normalize_openviking_retrieve_response(raw: dict[str, Any]) -> BackendRetrieveResult:
return _retrieve_result(BackendType.OPENVIKING, raw)
def normalize_evermemos_retrieve_response(raw: dict[str, Any]) -> BackendRetrieveResult:
return _retrieve_result(BackendType.EVERMEMOS, raw)
def map_backend_error_to_retryable(
backend_type: BackendType,
status_code: int | None = None,
error_code: str | None = None,
error_message: str | None = None,
) -> bool:
"""Map backend errors into retryable/non-retryable categories.
Unknown errors default to retryable because adapter contracts are still
unstable and transient backend/API rollout failures are more likely during
integration.
"""
if status_code in {429, 500, 502, 503, 504}:
return True
if status_code in {400, 401, 403, 404, 422}:
return False
text = f"{error_code or ''} {error_message or ''}".lower()
if "timeout" in text or "network_error" in text or "connection" in text:
return True
if "validation" in text or "unauthorized" in text or "forbidden" in text or "not_found" in text:
return False
return True
def _write_result(backend_type: BackendType, raw: dict[str, Any]) -> BackendWriteResult:
data = raw.get("result") or raw.get("data") or {}
if not isinstance(data, dict):
data = {}
native_id = (
raw.get("native_id")
or raw.get("id")
or raw.get("memory_id")
or raw.get("session_id")
or data.get("native_id")
or data.get("id")
or data.get("memory_id")
or data.get("session_id")
)
native_uri = (
raw.get("native_uri")
or raw.get("uri")
or raw.get("url")
or data.get("native_uri")
or data.get("uri")
or data.get("url")
)
if not native_uri and backend_type == BackendType.OPENVIKING and native_id:
native_uri = f"viking://sessions/{native_id}"
return BackendWriteResult(
backend_type=backend_type,
operation=BackendOperation.INGEST_TURN,
status=_result_status(raw),
native_id=native_id,
native_uri=native_uri,
retryable=_retryable_from_raw(backend_type, raw),
error_code=raw.get("error_code"),
error_message=raw.get("error") or raw.get("error_message"),
latency_ms=raw.get("latency_ms"),
metadata=safe_backend_metadata(raw.get("metadata") or raw),
)
def _retrieve_result(backend_type: BackendType, raw: dict[str, Any]) -> BackendRetrieveResult:
if not isinstance(raw, dict) or not raw:
return BackendRetrieveResult(
backend_type=backend_type,
operation=BackendOperation.RETRIEVE_CONTEXT,
status=BackendResultStatus.SKIPPED,
metadata={"reason": "malformed_or_empty_response"},
)
return BackendRetrieveResult(
backend_type=backend_type,
operation=BackendOperation.RETRIEVE_CONTEXT,
status=_result_status(raw),
native_id=raw.get("native_id") or raw.get("session_id"),
native_uri=raw.get("native_uri") or raw.get("uri"),
retryable=_retryable_from_raw(backend_type, raw),
error_code=raw.get("error_code"),
error_message=raw.get("error") or raw.get("error_message"),
latency_ms=raw.get("latency_ms"),
items=[_retrieve_item(backend_type, item) for item in _extract_retrieve_items(raw)],
metadata=safe_backend_metadata(raw.get("metadata") or raw),
)
def _retrieve_item(backend_type: BackendType, item: dict[str, Any]) -> BackendRetrieveItem:
metadata = safe_backend_metadata(item.get("metadata") if isinstance(item.get("metadata"), dict) else item)
return BackendRetrieveItem(
text=item.get("text") or item.get("summary") or item.get("abstract"),
source_backend=backend_type,
ref_id=item.get("ref_id") or item.get("id") or item.get("memory_id") or item.get("profile_id") or item.get("session_id") or item.get("uri"),
score=float(item.get("score") or 0.0),
memory_type=item.get("memory_type") or item.get("ref_type") or item.get("type") or item.get("kind"),
metadata=metadata,
)
def _produced_ref(backend_type: BackendType, item: dict[str, Any]) -> BackendProducedRef:
ref_type, mapping_metadata = map_backend_ref_type(backend_type, item.get("ref_type") or item.get("type") or item.get("kind"))
metadata = {
**safe_backend_metadata(item.get("metadata") if isinstance(item.get("metadata"), dict) else item),
**mapping_metadata,
}
return BackendProducedRef(
ref_type=ref_type,
native_id=item.get("native_id") or item.get("id") or item.get("memory_id") or item.get("profile_id") or item.get("session_id"),
native_uri=item.get("native_uri") or item.get("uri") or item.get("url"),
metadata=metadata,
)
def _extract_ref_items(raw: dict[str, Any]) -> list[dict[str, Any]]:
data = raw.get("result") or raw.get("data") or raw
candidates = (
data.get("refs")
or data.get("produced_refs")
or data.get("created_refs")
or data.get("memories")
or data.get("items")
or []
)
return [item for item in candidates if isinstance(item, dict)]
def _extract_retrieve_items(raw: dict[str, Any]) -> list[dict[str, Any]]:
data = raw.get("result") or raw.get("data") or raw
if not isinstance(data, dict):
return []
candidates = (
data.get("items")
or data.get("results")
or data.get("memories")
or data.get("resources")
or data.get("contexts")
or []
)
return [item for item in candidates if isinstance(item, dict)]
def _result_status(raw: dict[str, Any]) -> BackendResultStatus:
status = str(raw.get("status") or "success").lower()
if status in {"ok", "created", "accepted"}:
return BackendResultStatus.SUCCESS
try:
return BackendResultStatus(status)
except ValueError:
return BackendResultStatus.SUCCESS if not raw.get("error") and not raw.get("error_message") else BackendResultStatus.FAILED
def _retryable_from_raw(backend_type: BackendType, raw: dict[str, Any]) -> bool:
if "retryable" in raw:
return bool(raw["retryable"])
if raw.get("error") or raw.get("error_message") or raw.get("error_code") or raw.get("status_code"):
return map_backend_error_to_retryable(
backend_type,
status_code=raw.get("status_code"),
error_code=raw.get("error_code"),
error_message=raw.get("error") or raw.get("error_message"),
)
return False