"""Two small suggestions the editor asks for: a title, and what to extend. Both are one-shot calls rather than streams, and both are advisory: the author keeps the generic title or starts a new document if the suggestion does not fit. """ import uuid from typing import Annotated from fastapi import Depends from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.api.authoring.routing import authoring_router from app.api.documents import readable_document, require_editor from app.auth.deps import get_current_user from app.authoring.context import summarize_conversation from app.authoring.prompts import render_title_prompt from app.db import get_db from app.errors import ApiError from app.llm.client import NO_THINKING, chat_json from app.llm.errors import LLMError from app.models import Conversation, User from app.rag.similarity import CAPTURE_CONTEXT_MAX_DISTANCE, similar_documents router = authoring_router() class TitleSuggestion(BaseModel): title: str @router.post("/{document_id}/suggest-title") async def suggest_title( document_id: uuid.UUID, user: Annotated[User, Depends(get_current_user)], db: Annotated[AsyncSession, Depends(get_db)], ) -> TitleSuggestion: """Suggest a concise title from the document's content (review step for a new document). Owner-scoped; content in, title out, nothing logged.""" document = await readable_document(db, document_id, user) require_editor(document, user) if not document.content_md.strip(): return TitleSuggestion(title=document.title) try: return await chat_json( render_title_prompt(document.content_md), TitleSuggestion, extra_body=NO_THINKING, ) except LLMError as exc: raise ApiError(503, "The model endpoint did not answer.", exc.code) from None class SuggestSimilarRequest(BaseModel): conversation_id: uuid.UUID class SimilarDocumentOut(BaseModel): document_id: uuid.UUID title: str @router.post("/suggest-similar") async def suggest_similar( body: SuggestSimilarRequest, user: Annotated[User, Depends(get_current_user)], db: Annotated[AsyncSession, Depends(get_db)], ) -> list[SimilarDocumentOut]: """Existing documents that match the conversation a capture is starting from — found over an LLM TOPIC SUMMARY of the chat (not the raw last message), permission-filtered, help pages excluded. Empty list when the conversation is unknown, empty, or nothing is close enough.""" conversation = ( await db.execute( select(Conversation) .where( Conversation.id == body.conversation_id, Conversation.user_id == user.id, ) .options(selectinload(Conversation.messages)) ) ).scalar_one_or_none() if conversation is None: return [] topic = await summarize_conversation(conversation) if not topic: return [] matches = await similar_documents( db, topic, user=user, max_distance=CAPTURE_CONTEXT_MAX_DISTANCE, exclude_builtin=True, ) return [ SimilarDocumentOut(document_id=match.document_id, title=match.title) for match in matches ]