| """Knowledge Feed endpoint: a shuffled, reel-style stream of "moments" built from |
| the owner's saved cards (see services/feed.py). Owner-scoped via X-Owner-Id. |
| |
| Cheap by design: every moment except `connection` comes from data already stored |
| on the card; connections reuse the cached serendipity links and top up a small, |
| bounded number per load.""" |
|
|
| from __future__ import annotations |
|
|
| import logging |
| from typing import Annotated, Optional |
|
|
| from fastapi import APIRouter, Query |
| from pydantic import BaseModel |
| from sqlalchemy import select |
|
|
| from app.auth import OwnerDep |
| from app.models.card import CardState |
| from app.services import feed as feed_service |
| from app.store import db |
|
|
| log = logging.getLogger("api.feed") |
| router = APIRouter(prefix="/feed", tags=["feed"]) |
|
|
|
|
| class FeedCardRef(BaseModel): |
| card_id: str |
| title: str |
| content_type: str |
| thumbnail: Optional[str] = None |
|
|
|
|
| class FeedItem(BaseModel): |
| id: str |
| kind: str |
| card: FeedCardRef |
| text: str = "" |
| |
| question: str = "" |
| options: list[str] = [] |
| answer_index: int = 0 |
| explanation: str = "" |
| |
| card_b: Optional[FeedCardRef] = None |
|
|
|
|
| class FeedResponse(BaseModel): |
| items: list[FeedItem] |
|
|
|
|
| @router.get("", response_model=FeedResponse) |
| async def get_feed( |
| owner_id: OwnerDep, |
| limit: int = Query(40, ge=1, le=100), |
| ) -> FeedResponse: |
| """Assemble the owner's knowledge feed from their READY cards.""" |
| async with db.session() as session: |
| stmt = select(db.CardRow).where(db.CardRow.state == CardState.READY.value) |
| stmt = stmt.where(db.CardRow.owner_id == owner_id) |
| rows = list((await session.execute(stmt)).scalars().all()) |
| items = await feed_service.build_feed( |
| session, owner_id=owner_id, cards=rows, limit=limit |
| ) |
| return FeedResponse(items=[FeedItem(**it) for it in items]) |
|
|