feat: define the discovery api wire contract
This commit is contained in:
parent
6769833f7e
commit
4bc48663c8
2 changed files with 182 additions and 0 deletions
103
backend/app/api/schemas.py
Normal file
103
backend/app/api/schemas.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""Wire schemas for the discovery API: requests and streamed events."""
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
NDJSON_CONTENT_TYPE = "application/x-ndjson"
|
||||
|
||||
|
||||
class HistoryTurn(BaseModel):
|
||||
"""One prior chat turn; conversation state lives client-side."""
|
||||
|
||||
role: Literal["user", "assistant"]
|
||||
content: str = Field(min_length=1, max_length=2000)
|
||||
|
||||
|
||||
class PriorRecommendation(BaseModel):
|
||||
"""A track from an earlier response that follow-up turns can refer to."""
|
||||
|
||||
rank: int = Field(ge=1, le=50)
|
||||
track_id: str
|
||||
title: str
|
||||
artists: list[str] = Field(max_length=10)
|
||||
|
||||
|
||||
class RecommendationRequest(BaseModel):
|
||||
"""A discovery query with bounded client-side conversation state."""
|
||||
|
||||
schema_version: Literal[1] = 1
|
||||
query: str = Field(min_length=1, max_length=1000)
|
||||
history: list[HistoryTurn] = Field(default_factory=list, max_length=12)
|
||||
prior_recommendations: list[PriorRecommendation] = Field(default_factory=list, max_length=50)
|
||||
|
||||
|
||||
class TrackCard(BaseModel):
|
||||
"""The wire shape of one recommended track."""
|
||||
|
||||
id: str
|
||||
uri: str
|
||||
title: str
|
||||
artists: list[str]
|
||||
album_name: str
|
||||
album_art_url: str | None
|
||||
external_url: str | None
|
||||
|
||||
|
||||
class MetadataEvent(BaseModel):
|
||||
"""First stream event: how the query was understood."""
|
||||
|
||||
type: Literal["metadata"] = "metadata"
|
||||
request_id: str
|
||||
intent_summary: str
|
||||
candidate_count: int
|
||||
|
||||
|
||||
class TrackEvent(BaseModel):
|
||||
"""One recommended track, streamed as soon as it validates."""
|
||||
|
||||
type: Literal["track"] = "track"
|
||||
rank: int
|
||||
track: TrackCard
|
||||
justification: str
|
||||
|
||||
|
||||
class WarningEvent(BaseModel):
|
||||
"""A non-fatal degradation, surfaced honestly instead of hidden."""
|
||||
|
||||
type: Literal["warning"] = "warning"
|
||||
code: str
|
||||
message: str
|
||||
|
||||
|
||||
class ErrorEvent(BaseModel):
|
||||
"""A terminal failure; no further events follow it."""
|
||||
|
||||
type: Literal["error"] = "error"
|
||||
code: str
|
||||
message: str
|
||||
|
||||
|
||||
class DoneEvent(BaseModel):
|
||||
"""Final stream event with response-level counters."""
|
||||
|
||||
type: Literal["done"] = "done"
|
||||
track_count: int
|
||||
total_ms: int
|
||||
|
||||
|
||||
StreamEvent = MetadataEvent | TrackEvent | WarningEvent | ErrorEvent | DoneEvent
|
||||
|
||||
|
||||
class PlaylistCreateRequest(BaseModel):
|
||||
"""A request to save recommended tracks as a real Spotify playlist."""
|
||||
|
||||
schema_version: Literal[1] = 1
|
||||
name: str = Field(min_length=1, max_length=100)
|
||||
track_uris: list[str] = Field(min_length=1, max_length=50)
|
||||
|
||||
|
||||
class PlaylistCreateResponse(BaseModel):
|
||||
"""The created playlist's public location."""
|
||||
|
||||
url: str
|
||||
79
frontend/src/lib/types.ts
Normal file
79
frontend/src/lib/types.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
// Wire types mirrored against backend/app/api/schemas.py.
|
||||
// Field names match the JSON payloads exactly, so they stay snake_case.
|
||||
|
||||
export interface HistoryTurn {
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface PriorRecommendation {
|
||||
rank: number
|
||||
track_id: string
|
||||
title: string
|
||||
artists: string[]
|
||||
}
|
||||
|
||||
export interface RecommendationRequest {
|
||||
schema_version: 1
|
||||
query: string
|
||||
history: HistoryTurn[]
|
||||
prior_recommendations: PriorRecommendation[]
|
||||
}
|
||||
|
||||
export interface TrackCard {
|
||||
id: string
|
||||
uri: string
|
||||
title: string
|
||||
artists: string[]
|
||||
album_name: string
|
||||
album_art_url: string | null
|
||||
external_url: string | null
|
||||
}
|
||||
|
||||
export interface MetadataEvent {
|
||||
type: 'metadata'
|
||||
request_id: string
|
||||
intent_summary: string
|
||||
candidate_count: number
|
||||
}
|
||||
|
||||
export interface TrackEvent {
|
||||
type: 'track'
|
||||
rank: number
|
||||
track: TrackCard
|
||||
justification: string
|
||||
}
|
||||
|
||||
export interface WarningEvent {
|
||||
type: 'warning'
|
||||
code: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ErrorEvent {
|
||||
type: 'error'
|
||||
code: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface DoneEvent {
|
||||
type: 'done'
|
||||
track_count: number
|
||||
total_ms: number
|
||||
}
|
||||
|
||||
export type StreamEvent = MetadataEvent | TrackEvent | WarningEvent | ErrorEvent | DoneEvent
|
||||
|
||||
export interface PlaylistCreateRequest {
|
||||
schema_version: 1
|
||||
name: string
|
||||
track_uris: string[]
|
||||
}
|
||||
|
||||
export interface PlaylistCreateResponse {
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface CurrentUser {
|
||||
display_name: string
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue