feat: expose recommendation streaming api
This commit is contained in:
parent
e8d20158e3
commit
41aa93c3c7
7 changed files with 473 additions and 12 deletions
|
|
@ -1,16 +1,24 @@
|
|||
"""Application factory and wiring. No logic lives here."""
|
||||
"""Application factory and dependency wiring."""
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import httpx2
|
||||
from anthropic import AsyncAnthropic
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app.adapters.spotify.session import PendingLogins, SessionStore
|
||||
from app.adapters.anthropic.llm import AnthropicRecommender
|
||||
from app.adapters.spotify.auth import TokenSet, refresh_access_token
|
||||
from app.adapters.spotify.client import SpotifyClient
|
||||
from app.adapters.spotify.session import PendingLogins, SessionStore, SpotifySession
|
||||
from app.api.recommendations import router as recommendations_router
|
||||
from app.api.routes import router
|
||||
from app.config import AppMode, Settings, settings
|
||||
from app.observability.logging import configure_logging
|
||||
from app.observability.timing import RequestTimingMiddleware
|
||||
from app.pipeline.orchestrator import RecommendationPipeline
|
||||
|
||||
FRONTEND_DIST = Path(__file__).parent / "static"
|
||||
|
||||
|
|
@ -21,11 +29,11 @@ def create_app(
|
|||
) -> FastAPI:
|
||||
"""Build the FastAPI app: API routes plus the built SPA on one port."""
|
||||
active_settings = application_settings or settings
|
||||
configure_logging(active_settings.app_mode)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(application: FastAPI) -> AsyncIterator[None]:
|
||||
if active_settings.app_mode is AppMode.LIVE and not active_settings.spotify_client_id:
|
||||
raise RuntimeError("SPOTIFY_CLIENT_ID is required in live mode")
|
||||
_validate_live_settings(active_settings)
|
||||
async with httpx2.AsyncClient(
|
||||
timeout=active_settings.spotify_timeout_seconds,
|
||||
transport=http_transport,
|
||||
|
|
@ -34,7 +42,33 @@ def create_app(
|
|||
application.state.session_store = SessionStore()
|
||||
application.state.pending_logins = PendingLogins()
|
||||
application.state.settings = active_settings
|
||||
yield
|
||||
application.state.seed_session_id = None
|
||||
anthropic_client = AsyncAnthropic(
|
||||
api_key=active_settings.anthropic_api_key or "unused-demo-key"
|
||||
)
|
||||
application.state.anthropic = anthropic_client
|
||||
application.state.recommendation_pipeline = RecommendationPipeline(
|
||||
AnthropicRecommender(anthropic_client, active_settings),
|
||||
active_settings,
|
||||
)
|
||||
|
||||
def spotify_client_factory(session: SpotifySession) -> SpotifyClient:
|
||||
return SpotifyClient(http, session, active_settings)
|
||||
|
||||
application.state.spotify_client_factory = spotify_client_factory
|
||||
if (
|
||||
active_settings.app_mode is AppMode.LIVE
|
||||
and active_settings.spotify_seed_refresh_token
|
||||
):
|
||||
application.state.seed_session_id = await _install_seed_session(
|
||||
http,
|
||||
application.state.session_store,
|
||||
active_settings,
|
||||
)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
await anthropic_client.close()
|
||||
|
||||
app = FastAPI(
|
||||
title="discovery-by-llm",
|
||||
|
|
@ -42,12 +76,14 @@ def create_app(
|
|||
redoc_url=None,
|
||||
lifespan=lifespan,
|
||||
)
|
||||
app.add_middleware(RequestTimingMiddleware)
|
||||
|
||||
@app.get("/api/health")
|
||||
def health() -> dict[str, str]:
|
||||
return {"status": "ok", "mode": active_settings.app_mode}
|
||||
|
||||
app.include_router(router)
|
||||
app.include_router(recommendations_router)
|
||||
if FRONTEND_DIST.is_dir():
|
||||
app.mount("/", StaticFiles(directory=FRONTEND_DIST, html=True), name="spa")
|
||||
|
||||
|
|
@ -55,3 +91,41 @@ def create_app(
|
|||
|
||||
|
||||
app = create_app()
|
||||
|
||||
|
||||
def _validate_live_settings(application_settings: Settings) -> None:
|
||||
if application_settings.app_mode is not AppMode.LIVE:
|
||||
return
|
||||
if not application_settings.spotify_client_id:
|
||||
raise RuntimeError("SPOTIFY_CLIENT_ID is required in live mode")
|
||||
if not application_settings.anthropic_api_key:
|
||||
raise RuntimeError("ANTHROPIC_API_KEY is required in live mode")
|
||||
|
||||
|
||||
async def _install_seed_session(
|
||||
http: httpx2.AsyncClient,
|
||||
session_store: SessionStore,
|
||||
application_settings: Settings,
|
||||
) -> str:
|
||||
tokens = await refresh_access_token(
|
||||
http,
|
||||
client_id=application_settings.spotify_client_id,
|
||||
tokens=TokenSet(
|
||||
access_token="seed-bootstrap",
|
||||
refresh_token=application_settings.spotify_seed_refresh_token,
|
||||
expires_at=0.0,
|
||||
),
|
||||
)
|
||||
bootstrap_session = SpotifySession(tokens=tokens, account_id="", display_name="")
|
||||
current_user = await SpotifyClient(
|
||||
http,
|
||||
bootstrap_session,
|
||||
application_settings,
|
||||
).fetch_current_user()
|
||||
return session_store.create(
|
||||
SpotifySession(
|
||||
tokens=bootstrap_session.tokens,
|
||||
account_id=current_user.account_id,
|
||||
display_name=current_user.display_name,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue