Add AI-suggested editable document title
AI now returns a short descriptive title per document (e.g. "ACME Corp
Invoice April 2026"). Title is stored in a new documents.title column
(migration 0002), shown in the row header instead of the raw filename,
and editable inline via PATCH /documents/{id}/title. Filename is shown
as a subtitle when a title exists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
"""add document title column
|
||||
|
||||
Revision ID: 0002
|
||||
Revises: 0001
|
||||
Create Date: 2026-04-14
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0002"
|
||||
down_revision: Union[str, None] = "0001"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("documents", sa.Column("title", sa.String(500), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("documents", "title")
|
||||
@@ -16,6 +16,7 @@ class Document(Base):
|
||||
file_path: Mapped[str] = mapped_column(String, nullable=False)
|
||||
file_size: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
status: Mapped[str] = mapped_column(String, nullable=False, default="pending")
|
||||
title: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
document_type: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
raw_text: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
extracted_data: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON string
|
||||
|
||||
@@ -16,7 +16,7 @@ from app.deps import get_user_id
|
||||
from app.models.category import DocumentCategory
|
||||
from app.models.category_assignment import CategoryAssignment
|
||||
from app.models.document import Document
|
||||
from app.schemas.document import DocumentOut, DocumentStatusOut, DocumentTypeUpdate, TagsUpdate
|
||||
from app.schemas.document import DocumentOut, DocumentStatusOut, DocumentTypeUpdate, TagsUpdate, TitleUpdate
|
||||
from app.services.ai_client import AIServiceError, classify_document
|
||||
from app.services.config_reader import load_doc_config
|
||||
from app.services.storage import delete_file, get_upload_path, save_upload
|
||||
@@ -95,6 +95,7 @@ async def process_document(doc_id: str) -> None:
|
||||
|
||||
doc.raw_text = text[:500_000] # cap stored text at 500k chars
|
||||
doc.extracted_data = json.dumps(result)
|
||||
doc.title = result.get("title") or None
|
||||
doc.document_type = result.get("document_type", "unknown")
|
||||
doc.tags = json.dumps(result.get("tags", []))
|
||||
doc.status = "done"
|
||||
@@ -227,6 +228,20 @@ async def update_document_tags(
|
||||
return _doc_with_categories(doc)
|
||||
|
||||
|
||||
@router.patch("/{doc_id}/title", response_model=DocumentOut)
|
||||
async def update_document_title(
|
||||
doc_id: str,
|
||||
body: TitleUpdate,
|
||||
user_id: str = Depends(get_user_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> DocumentOut:
|
||||
doc = await _get_user_doc(doc_id, user_id, db)
|
||||
doc.title = body.title.strip() or None
|
||||
await db.commit()
|
||||
doc = await _get_user_doc(doc_id, user_id, db)
|
||||
return _doc_with_categories(doc)
|
||||
|
||||
|
||||
@router.delete("/{doc_id}", status_code=204)
|
||||
async def delete_document(
|
||||
doc_id: str,
|
||||
|
||||
@@ -13,6 +13,7 @@ class DocumentOut(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
filename: str
|
||||
title: str | None
|
||||
file_size: int
|
||||
status: str
|
||||
document_type: str | None
|
||||
@@ -41,3 +42,7 @@ class DocumentTypeUpdate(BaseModel):
|
||||
|
||||
class TagsUpdate(BaseModel):
|
||||
tags: list[str]
|
||||
|
||||
|
||||
class TitleUpdate(BaseModel):
|
||||
title: str
|
||||
|
||||
@@ -5,6 +5,7 @@ SYSTEM_PROMPT = (
|
||||
)
|
||||
|
||||
USER_PROMPT_TEMPLATE = """Analyze the following document text and return a JSON object with exactly these keys:
|
||||
title (a short, descriptive human-readable title for this document, e.g. "ACME Corp Invoice April 2026", "Office Supplies Receipt", "Q1 Flower Delivery Order"),
|
||||
document_type (one of: invoice, bill, receipt, order, expense, revenue, unknown),
|
||||
total_amount (string or null),
|
||||
currency (string or null),
|
||||
|
||||
Reference in New Issue
Block a user