"""Recipient autocomplete from known_recipients (SPEC §9.1, §4). Prefix match on email or name, ranked by ``times_used`` decayed by recency. The composer merges these with plugin capability providers client-side (M3). """ from __future__ import annotations from typing import Annotated from fastapi import APIRouter, Depends, Query from pydantic import BaseModel from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.db import get_session from app.identity.deps import CurrentUser from app.models import KnownRecipient router = APIRouter(prefix="/recipients", tags=["compose"]) DbSession = Annotated[AsyncSession, Depends(get_session)] class Suggestion(BaseModel): name: str email: str @router.get("/suggest", response_model=list[Suggestion]) async def suggest( user: CurrentUser, db: DbSession, q: str = Query("", max_length=200) ) -> list[Suggestion]: term = q.strip() if not term: return [] like = f"{term}%" # Recency-decayed score: times_used / (1 + age_in_days). Ranks frequent and # recent correspondents ahead of stale ones (SPEC §9.1). age_days = func.extract("epoch", func.now() - KnownRecipient.last_used_at) / 86400.0 score = KnownRecipient.times_used / (1.0 + age_days) stmt = ( select(KnownRecipient.name, KnownRecipient.email) .where( KnownRecipient.user_id == user.id, KnownRecipient.email.ilike(like) | KnownRecipient.name.ilike(like), ) .order_by(score.desc()) .limit(10) ) rows = (await db.execute(stmt)).all() return [Suggestion(name=row.name, email=row.email) for row in rows]