Files
pablan/backend/app/api/authoring/suggest.py
T
ProfessorNovaandClaude Opus 5 97dbff309c Pablan, as it stands
Self-hosted knowledge management for SMEs: a split-screen Markdown editor
whose sections an LLM refines while you write, and RAG question answering
over the documents that result. FastAPI + Postgres/pgvector on the back,
SvelteKit on the front, everything OpenAI-compatible and self-hostable.

Squashed into a single commit; the development history stays local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
2026-09-04 08:36:17 +02:00

102 lines
3.3 KiB
Python

"""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
]