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
This commit is contained in:
co-authored by
Claude Opus 5
parent
68d3a43191
commit
784b76baf7
@@ -0,0 +1,101 @@
|
||||
"""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
|
||||
]
|
||||
Reference in New Issue
Block a user