feat: add key-free demo mode replaying recorded fixtures

This commit is contained in:
Justin Visser 2026-08-11 09:14:22 +02:00
parent 0664cc2d27
commit 401a0ddea7
21 changed files with 1040 additions and 31 deletions

View file

@ -0,0 +1,128 @@
"""Decode recorded Spotify and Anthropic responses for demo replay."""
import base64
import json
from dataclasses import dataclass
from functools import cache
from pathlib import Path
from app.adapters.demo.scenario import FIXTURE_ROOT
@dataclass(frozen=True)
class RecordedResponse:
"""One decoded response retained from a recorded HTTP interaction."""
method: str
url: str
status: int
response_body: bytes
response_chunks: tuple[bytes, ...]
def json_body(self) -> object:
"""Parse the decoded response body as JSON."""
return json.loads(self.response_body)
@dataclass(frozen=True)
class DemoCassette:
"""The recorded service responses needed by one demo scenario."""
key: str
spotify_responses: tuple[RecordedResponse, ...]
intent_response_bodies: tuple[bytes, ...]
rerank_response_chunks: tuple[bytes, ...]
@property
def intent_response_body(self) -> bytes:
"""Return the final intent response recorded for this scenario."""
return self.intent_response_bodies[-1]
@property
def spotify_search_responses(self) -> tuple[RecordedResponse, ...]:
"""Return recorded Spotify search responses in capture order."""
return tuple(response for response in self.spotify_responses if "/search?" in response.url)
@property
def spotify_taste_responses(self) -> tuple[RecordedResponse, ...]:
"""Return recorded Spotify taste responses in capture order."""
return tuple(
response
for response in self.spotify_responses
if any(
endpoint in response.url
for endpoint in ("/me/top/artists", "/me/top/tracks", "/me/tracks?")
)
)
@cache
def load_cassette(scenario_key: str) -> DemoCassette:
"""Load and decode both service cassettes for one scenario."""
scenario_root = FIXTURE_ROOT / scenario_key
spotify = _load_responses(scenario_root / "spotify.json")
anthropic = _load_responses(scenario_root / "anthropic.json")
intent_responses = tuple(
response for response in anthropic if _is_anthropic_message(response.response_body)
)
rerank_responses = tuple(
response for response in anthropic if not _is_anthropic_message(response.response_body)
)
if not spotify or not intent_responses or not rerank_responses:
raise ValueError(f"Scenario cassette is incomplete: {scenario_key}")
return DemoCassette(
key=scenario_key,
spotify_responses=spotify,
intent_response_bodies=tuple(response.response_body for response in intent_responses),
rerank_response_chunks=rerank_responses[-1].response_chunks,
)
def _load_responses(path: Path) -> tuple[RecordedResponse, ...]:
payload = json.loads(path.read_text(encoding="ascii"))
if not isinstance(payload, list):
raise ValueError(f"Cassette must contain a response list: {path}")
return tuple(_decode_response(entry) for entry in payload)
def _decode_response(payload: object) -> RecordedResponse:
if not isinstance(payload, dict):
raise ValueError("Cassette response must be a mapping")
try:
chunks_value = payload["response_chunks_base64"]
if not isinstance(chunks_value, list):
raise TypeError
chunks = tuple(_decode_base64(value) for value in chunks_value)
return RecordedResponse(
method=_string(payload["method"]),
url=_string(payload["url"]),
status=_integer(payload["status"]),
response_body=_decode_base64(payload["response_body_base64"]),
response_chunks=chunks,
)
except (KeyError, TypeError, ValueError) as error:
raise ValueError("Cassette response is invalid") from error
def _decode_base64(value: object) -> bytes:
return base64.b64decode(_string(value), validate=True)
def _string(value: object) -> str:
if not isinstance(value, str):
raise TypeError
return value
def _integer(value: object) -> int:
if not isinstance(value, int):
raise TypeError
return value
def _is_anthropic_message(body: bytes) -> bool:
try:
payload = json.loads(body)
except (json.JSONDecodeError, UnicodeDecodeError):
return False
return isinstance(payload, dict) and payload.get("type") == "message"