260 lines
9.5 KiB
Python
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
|