45 lines
1.6 KiB
Python
45 lines
1.6 KiB
Python
"""Sentence completion for the compose window (app/completion.py)."""
|
|||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import datetime as dt
|
||
|
|
from typing import Annotated
|
||
|
|
|
||
|
|
from fastapi import APIRouter, Depends
|
||
|
|
from pydantic import BaseModel, Field
|
||
|
|
from sqlalchemy import select
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
from app import completion
|
||
|
|
from app.db import get_session
|
||
|
|
from app.identity.deps import CurrentUser, require_csrf
|
||
|
|
from app.models import CompletionIndexState
|
||
|
|
from app.queue import enqueue_job
|
||
|
|
|
||
|
|
router = APIRouter(prefix="/completion", tags=["compose"], dependencies=[Depends(require_csrf)])
|
||
|
|
|
||
|
|
DbSession = Annotated[AsyncSession, Depends(get_session)]
|
||
|
|
|
||
|
|
|
||
|
|
class CompletionIn(BaseModel):
|
||
|
|
# The text before the cursor (its last line is all that matters). A POST,
|
||
|
|
# not a query string: what the user is typing stays out of access logs.
|
||
|
|
before: str = Field(max_length=2000)
|
||
|
|
|
||
|
|
|
||
|
|
class CompletionOut(BaseModel):
|
||
|
|
suggestion: str | None
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("", response_model=CompletionOut)
|
||
|
|
async def suggest(body: CompletionIn, user: CurrentUser, db: DbSession) -> CompletionOut:
|
||
|
|
before = body.before[-300:]
|
||
|
|
built = await db.scalar(
|
||
|
|
select(CompletionIndexState.built_at).where(CompletionIndexState.user_id == user.id)
|
||
|
|
)
|
||
|
|
if built is None or built < dt.datetime.now(dt.UTC) - completion.REBUILD_AFTER:
|
||
|
|
# In the background; until it is built, the starter phrases still answer.
|
||
|
|
await enqueue_job("build_index_job", str(user.id), _job_id=f"complete:{user.id}")
|
||
|
|
personal = await completion.personal_phrases(db, user.id, completion.keys_for(before))
|
||
|
|
return CompletionOut(suggestion=completion.complete(personal, before))
|