Files

53 lines
1.6 KiB
Python
Raw Permalink Normal View History

2026-07-17 23:20:34 +02:00
"""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]