feat: add recommendation service adapters
This commit is contained in:
parent
e970bdf542
commit
751391e6a2
9 changed files with 676 additions and 26 deletions
82
backend/app/ports/protocols.py
Normal file
82
backend/app/ports/protocols.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"""Structural ports implemented by external service adapters."""
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import Literal, Protocol
|
||||
|
||||
from app.domain.models import (
|
||||
ConversationTurn,
|
||||
CreatedPlaylist,
|
||||
Intent,
|
||||
PreviousRecommendation,
|
||||
RerankSelection,
|
||||
Track,
|
||||
)
|
||||
|
||||
TimeRange = Literal["short_term", "long_term"]
|
||||
|
||||
|
||||
class CatalogQuotaExhaustedError(Exception):
|
||||
"""A catalog quota stopped further resolution attempts."""
|
||||
|
||||
|
||||
class RecommenderOutputError(Exception):
|
||||
"""The recommender returned unusable structured output."""
|
||||
|
||||
|
||||
class MusicCatalog(Protocol):
|
||||
"""Read the restricted Spotify surface used by discovery."""
|
||||
|
||||
async def search_tracks(self, query: str, limit: int = 10) -> list[Track]:
|
||||
"""Search tracks by fielded or bare-text query."""
|
||||
...
|
||||
|
||||
async def fetch_top_artists(self, time_range: TimeRange, limit: int) -> list[str]:
|
||||
"""Fetch bounded top artist names for one time range."""
|
||||
...
|
||||
|
||||
async def fetch_top_tracks(self, time_range: TimeRange, limit: int) -> list[Track]:
|
||||
"""Fetch bounded top tracks for one time range."""
|
||||
...
|
||||
|
||||
async def fetch_saved_tracks(self, limit: int) -> list[Track]:
|
||||
"""Fetch a bounded sample of saved tracks."""
|
||||
...
|
||||
|
||||
|
||||
class Recommender(Protocol):
|
||||
"""Interpret discovery intent and stream a grounded reranking."""
|
||||
|
||||
async def create_intent(
|
||||
self,
|
||||
query: str,
|
||||
history: tuple[ConversationTurn, ...],
|
||||
previous_recommendations: tuple[PreviousRecommendation, ...],
|
||||
taste_summary: str,
|
||||
candidate_count: int,
|
||||
) -> Intent:
|
||||
"""Interpret a query and propose a bounded candidate set."""
|
||||
...
|
||||
|
||||
def stream_rerank(
|
||||
self,
|
||||
intent: Intent,
|
||||
grounded_tracks: tuple[Track, ...],
|
||||
taste_summary: str,
|
||||
history: tuple[ConversationTurn, ...],
|
||||
selection_count: int,
|
||||
correction: str | None = None,
|
||||
) -> AsyncIterator[RerankSelection]:
|
||||
"""Stream validated selections from the grounded pool."""
|
||||
...
|
||||
|
||||
|
||||
class PlaylistWriter(Protocol):
|
||||
"""Write a Spotify playlist without retrying ambiguous mutations."""
|
||||
|
||||
async def create_playlist(self, name: str, description: str) -> CreatedPlaylist:
|
||||
"""Create a private playlist."""
|
||||
...
|
||||
|
||||
async def add_tracks_to_playlist(self, playlist_id: str, track_uris: list[str]) -> None:
|
||||
"""Add ordered Spotify track URIs to a playlist."""
|
||||
...
|
||||
Loading…
Add table
Add a link
Reference in a new issue