Ukweli / app /models /database.py
Benard John
feat: implement full-stack architecture with database models, authentication services, and web dashboard components
37b5223
Raw
History Blame Contribute Delete
10.5 kB
"""
Ukweli — SQLAlchemy ORM Models
Defines the relational schema for PostgreSQL per Architecture Section 3.3.4.
"""
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import (
Boolean,
DateTime,
Enum,
Float,
ForeignKey,
Index,
Integer,
String,
Text,
func,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
"""Declarative base for all ORM models."""
pass
class Document(Base):
"""
Catalog of all ingested documents.
Maps to the Metadata Registry described in Section 3.3.4.
"""
__tablename__ = "documents"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
title: Mapped[str] = mapped_column(String(1024), nullable=False)
source_url: Mapped[str | None] = mapped_column(String(2048), nullable=True)
pdf_url: Mapped[str | None] = mapped_column(String(2048), nullable=True)
fiscal_year: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True)
auditee: Mapped[str | None] = mapped_column(String(512), nullable=True, index=True)
report_type: Mapped[str] = mapped_column(
Enum(
"financial_audit",
"performance_audit",
"special_audit",
"public_debt_audit",
"county_audit",
"summary_report",
"strategic_document",
"annual_corporate_report",
name="report_type_enum",
),
nullable=False,
default="financial_audit",
)
page_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
published_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
checksum_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True, unique=True)
status: Mapped[str] = mapped_column(
Enum(
"pending",
"downloading",
"parsing",
"parsed",
"embedding",
"ready",
"failed",
name="document_status_enum",
),
nullable=False,
default="pending",
index=True,
)
raw_storage_path: Mapped[str | None] = mapped_column(String(1024), nullable=True)
language: Mapped[str] = mapped_column(String(5), nullable=False, default="en")
file_size_bytes: Mapped[int | None] = mapped_column(Integer, nullable=True)
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
)
# Relationships
chunks: Mapped[list[Chunk]] = relationship("Chunk", back_populates="document", cascade="all, delete-orphan")
ingestion_logs: Mapped[list[IngestionLog]] = relationship(
"IngestionLog", back_populates="document", cascade="all, delete-orphan"
)
__table_args__ = (
Index("ix_documents_auditee_fy", "auditee", "fiscal_year"),
)
class Chunk(Base):
"""
Individual content chunks derived from parsed documents.
Schema matches Section 3.2.4 Chunk Metadata Schema.
"""
__tablename__ = "chunks"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
document_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("documents.id", ondelete="CASCADE"), nullable=False, index=True
)
chunk_type: Mapped[str] = mapped_column(
Enum(
"narrative",
"table",
"finding",
"recommendation",
"legal_reference",
name="chunk_type_enum",
),
nullable=False,
)
content: Mapped[str] = mapped_column(Text, nullable=False)
page_range_start: Mapped[int | None] = mapped_column(Integer, nullable=True)
page_range_end: Mapped[int | None] = mapped_column(Integer, nullable=True)
section_heading: Mapped[str | None] = mapped_column(String(512), nullable=True)
entities: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
language: Mapped[str] = mapped_column(String(5), nullable=False, default="en")
audit_period: Mapped[str | None] = mapped_column(String(20), nullable=True)
auditee: Mapped[str | None] = mapped_column(String(512), nullable=True)
finding_category: Mapped[str | None] = mapped_column(String(64), nullable=True)
embedding_vector_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
token_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
chunk_index: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
# Relationships
document: Mapped[Document] = relationship("Document", back_populates="chunks")
__table_args__ = (
Index("ix_chunks_document_index", "document_id", "chunk_index"),
Index("ix_chunks_auditee_period", "auditee", "audit_period"),
# Index(
# "ix_chunks_content_fts",
# func.to_tsvector("english", "content"),
# postgresql_using="gin",
# ),
)
class IngestionLog(Base):
"""
Audit trail for every document processing step.
Provides transparency per Section 3.3.4.
"""
__tablename__ = "ingestion_logs"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
document_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("documents.id", ondelete="CASCADE"), nullable=False, index=True
)
stage: Mapped[str] = mapped_column(
String(64), nullable=False
) # e.g. "download", "parse", "chunk", "embed"
status: Mapped[str] = mapped_column(String(32), nullable=False) # "started", "completed", "failed"
started_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
worker_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
metadata_extra: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
# Relationships
document: Mapped[Document] = relationship("Document", back_populates="ingestion_logs")
class QueryLog(Base):
"""
Immutable audit trail of every RAG query.
Required by Section 7.2 — transparency logging (WORM-style).
"""
__tablename__ = "query_logs"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
query_text: Mapped[str] = mapped_column(Text, nullable=False)
language: Mapped[str] = mapped_column(String(5), nullable=False, default="en")
mode: Mapped[str] = mapped_column(String(20), nullable=False, default="concise")
user_tier: Mapped[str] = mapped_column(String(20), nullable=False, default="public")
user_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id"), nullable=True, index=True
)
api_key_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("api_keys.id"), nullable=True
)
fingerprint: Mapped[str | None] = mapped_column(String(16), nullable=True, index=True)
filters: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
answer: Mapped[str | None] = mapped_column(Text, nullable=True)
citations: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
chunks_considered: Mapped[int | None] = mapped_column(Integer, nullable=True)
latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
confidence: Mapped[str | None] = mapped_column(String(32), nullable=True)
llm_model_used: Mapped[str | None] = mapped_column(String(128), nullable=True)
llm_tokens_used: Mapped[int | None] = mapped_column(Integer, nullable=True)
conversation_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
# Relationships
feedbacks: Mapped[list[Feedback]] = relationship("Feedback", back_populates="query_log")
class Feedback(Base):
"""
User feedback and corrections on RAG responses.
Maps to Section 4.2 POST /feedback.
"""
__tablename__ = "feedbacks"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
query_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("query_logs.id", ondelete="CASCADE"), nullable=False, index=True
)
rating: Mapped[int] = mapped_column(Integer, nullable=False) # -1, 0, 1
correction: Mapped[str | None] = mapped_column(Text, nullable=True)
suggested_citation: Mapped[str | None] = mapped_column(Text, nullable=True)
review_status: Mapped[str] = mapped_column(
String(20), nullable=False, default="pending"
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
# Relationships
query_log: Mapped[QueryLog] = relationship("QueryLog", back_populates="feedbacks")
class PageHash(Base):
"""
Stores content hashes of crawled pages for change detection.
Used by the change_detection service.
"""
__tablename__ = "page_hashes"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
url_path: Mapped[str] = mapped_column(String(2048), nullable=False, unique=True, index=True)
content_hash: Mapped[str] = mapped_column(String(64), nullable=False)
last_checked_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
changed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)