Talent-Lens / serving /app /models.py
riezqidr's picture
fix(ci): resolve ruff lint errors, mypy type issues
70a8ed7
Raw
History Blame Contribute Delete
46.9 kB
"""ORM models for Phase 0-3.
Every tenant-scoped table carries `tenant_id` as the first filter column and is
indexed on it. Content-addressed uniqueness on `(tenant_id, sha256)` is what
makes the same resume arriving through three channels resolve to one document.
"""
from __future__ import annotations
import uuid
from datetime import datetime
from decimal import Decimal
from pgvector.sqlalchemy import HALFVEC, Vector
from sqlalchemy import (
ARRAY,
BigInteger,
Boolean,
Date,
DateTime,
Float,
ForeignKey,
Index,
Integer,
Numeric,
String,
Text,
UniqueConstraint,
func,
)
from sqlalchemy.dialects.postgresql import JSONB, TSVECTOR
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db import Base
class TimestampMixin:
"""Adds server-side creation and update timestamps."""
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,
)
class ResumeDocument(TimestampMixin, Base):
"""An uploaded resume file, deduplicated by content hash within a tenant."""
__tablename__ = "resume_documents"
__table_args__ = (
UniqueConstraint("tenant_id", "sha256", name="uq_resume_documents_tenant_sha256"),
Index("ix_resume_documents_tenant_created", "tenant_id", "created_at"),
)
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
uploaded_by: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), nullable=False)
storage_key: Mapped[str] = mapped_column(String(512), nullable=False)
filename_sanitized: Mapped[str] = mapped_column(String(255), nullable=False)
media_type: Mapped[str] = mapped_column(String(128), nullable=False)
size_bytes: Mapped[int] = mapped_column(Integer, nullable=False)
sha256: Mapped[str] = mapped_column(String(64), nullable=False)
page_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
parse_status: Mapped[str] = mapped_column(String(32), nullable=False, default="ok")
parse_error: Mapped[str | None] = mapped_column(Text, nullable=True)
parser_version: Mapped[str] = mapped_column(String(64), nullable=False)
needs_ocr: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
versions: Mapped[list[ResumeVersion]] = relationship(
back_populates="document", cascade="all, delete-orphan", lazy="noload"
)
class ResumeVersion(TimestampMixin, Base):
"""Extracted text for one parse of one document.
Kept separate from the document so a re-parse under a newer parser version
is a new row rather than a destructive update — prior evidence offsets stay
attributable to the parse that produced them.
"""
__tablename__ = "resume_versions"
__table_args__ = (
UniqueConstraint(
"document_id", "version", name="uq_resume_versions_document_version"
),
)
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
document_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("resume_documents.id", ondelete="CASCADE"),
nullable=False,
)
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
extracted_text: Mapped[str] = mapped_column(Text, nullable=False)
text_sha256: Mapped[str] = mapped_column(String(64), nullable=False)
page_offsets: Mapped[list[dict[str, object]]] = mapped_column(
JSONB, nullable=False, default=list
)
parser_version: Mapped[str] = mapped_column(String(64), nullable=False)
# Sanitization provenance. Stored per version because a re-parse under a
# newer sanitizer must not overwrite what an earlier decision was based on.
sanitization_report: Mapped[dict[str, object]] = mapped_column(
JSONB, nullable=False, default=dict
)
injection_risk_score: Mapped[float] = mapped_column(
Float, nullable=False, default=0.0
)
quarantined: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
document: Mapped[ResumeDocument] = relationship(back_populates="versions")
class ResumeChunk(TimestampMixin, Base):
"""A chunk of resume text with its embedding vector.
Parent-child chunking: small children (~180 words) for embedding precision,
larger parents (~700 words) fed to the LLM for context. Retrieve on children,
expand to parents.
``embedding_version`` on the row is what makes an embedding-model upgrade a
background backfill instead of a flag day.
"""
__tablename__ = "resume_chunks"
# Every index the migration creates is declared here, including the two
# PostgreSQL-specific ones. Leaving them out of the metadata does not make
# them optional — it makes `alembic revision --autogenerate` emit a
# `drop_index` for each on the next run, quietly removing the indexes both
# halves of hybrid search depend on.
__table_args__ = (
Index("ix_resume_chunks_tenant_document", "tenant_id", "document_id"),
Index("ix_resume_chunks_tenant_id", "tenant_id"),
Index("ix_resume_chunks_content_tsv", "content_tsv", postgresql_using="gin"),
Index(
"ix_resume_chunks_embedding_hnsw",
"embedding",
postgresql_using="hnsw",
postgresql_with={"m": 16, "ef_construction": 64},
postgresql_ops={"embedding": "vector_cosine_ops"},
),
)
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), nullable=False)
resume_version_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("resume_versions.id", ondelete="CASCADE"),
nullable=False,
)
document_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("resume_documents.id", ondelete="CASCADE"),
nullable=False,
)
chunk_index: Mapped[int] = mapped_column(Integer, nullable=False)
parent_chunk_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True), nullable=True
)
section: Mapped[str] = mapped_column(
String(64), nullable=False, default="other"
)
content: Mapped[str] = mapped_column(Text, nullable=False)
# content_tsv is a PostgreSQL tsvector GENERATED column managed by the
# database. The column exists for GIN indexing and ts_rank_cd queries.
# We declare it here so SQLAlchemy knows about it but never write to it
# from Python.
content_tsv: Mapped[str | None] = mapped_column(
TSVECTOR, nullable=True, server_default=None
)
page_from: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
page_to: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
start_char: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
end_char: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
token_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
# pgvector embedding — stored as vector(1024). The HNSW index is created
# in the Alembic migration rather than here because SQLAlchemy's Index()
# does not support the pgvector-specific USING/WITH clauses.
embedding: Mapped[list[float] | None] = mapped_column(
Vector(1024), nullable=True
)
embedding_model: Mapped[str] = mapped_column(String(128), nullable=False, default="")
embedding_version: Mapped[str] = mapped_column(String(64), nullable=False, default="")
is_parent: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
class Job(TimestampMixin, Base):
"""A job description, either pasted as text or extracted from an upload."""
__tablename__ = "jobs"
__table_args__ = (Index("ix_jobs_tenant_created", "tenant_id", "created_at"),)
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
created_by: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), nullable=False)
title: Mapped[str] = mapped_column(String(255), nullable=False)
department: Mapped[str | None] = mapped_column(String(255), nullable=True)
location: Mapped[str | None] = mapped_column(String(255), nullable=True)
employment_type: Mapped[str | None] = mapped_column(String(64), nullable=True)
seniority: Mapped[str | None] = mapped_column(String(64), nullable=True)
description_raw: Mapped[str] = mapped_column(Text, nullable=False)
source: Mapped[str] = mapped_column(String(32), nullable=False, default="manual")
status: Mapped[str] = mapped_column(String(32), nullable=False, default="draft")
blind_mode: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
class RubricVersion(TimestampMixin, Base):
"""One immutable-once-approved snapshot of the criteria for a job.
A rubric is the artifact a human signs off on, and every score is attributed
to the version it was computed against. Editing an approved rubric mints
version N+1 and leaves prior scores attributable to the criteria that
produced them, so `unique(job_id, version)` is what keeps "which criteria
was this candidate judged by" answerable.
``content_hash`` fingerprints the full requirement set and is part of every
verdict cache key. It stays NULL while the rubric is a draft — a draft has
no frozen criteria to fingerprint.
"""
__tablename__ = "rubric_versions"
__table_args__ = (
UniqueConstraint("job_id", "version", name="uq_rubric_versions_job_version"),
Index("ix_rubric_versions_tenant_job", "tenant_id", "job_id"),
)
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
job_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("jobs.id", ondelete="CASCADE"),
nullable=False,
)
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="draft")
content_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
# A candidate failing a must-have cannot score above this, no matter how
# strong the rest of the match is. Stored per version so tightening the cap
# does not silently re-rank candidates scored under the old one.
must_have_fail_cap: Mapped[int] = mapped_column(Integer, nullable=False, default=40)
aggregation_formula_version: Mapped[str] = mapped_column(
String(32), nullable=False, default="v1"
)
source: Mapped[str] = mapped_column(String(32), nullable=False, default="manual")
approved_by: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True), nullable=True
)
approved_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
requirements: Mapped[list[Requirement]] = relationship(
back_populates="rubric_version",
cascade="all, delete-orphan",
lazy="noload",
)
class Requirement(TimestampMixin, Base):
"""One weighted criterion within a rubric version.
``weight`` is ``numeric(5,4)`` rather than a float because normalized
weights must sum to exactly 1.0. Binary floats cannot represent tenths
exactly, so a float column would make that invariant unverifiable.
``skill_id`` is nullable: the ESCO skill taxonomy it would point at is not
built yet, and requiring it would make every requirement uninsertable.
"""
__tablename__ = "requirements"
__table_args__ = (
Index("ix_requirements_tenant_version", "tenant_id", "rubric_version_id"),
)
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
rubric_version_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("rubric_versions.id", ondelete="CASCADE"),
nullable=False,
)
ordinal: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
text: Mapped[str] = mapped_column(Text, nullable=False)
category: Mapped[str] = mapped_column(String(32), nullable=False, default="skill")
is_must_have: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
weight: Mapped[Decimal] = mapped_column(Numeric(5, 4), nullable=False)
min_years: Mapped[Decimal | None] = mapped_column(Numeric(4, 1), nullable=True)
min_seniority: Mapped[str | None] = mapped_column(String(32), nullable=True)
# Reserved for taxonomy linking; the ESCO tables do not exist yet.
skill_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True), nullable=True
)
# Half precision because requirement counts are small and the extra recall
# from full precision is not worth doubling the index footprint. The HNSW
# index is created in the Alembic migration, not here.
embedding: Mapped[list[float] | None] = mapped_column(HALFVEC(1024), nullable=True)
rubric_version: Mapped[RubricVersion] = relationship(back_populates="requirements")
class ScreeningRun(TimestampMixin, Base):
"""One execution of the scoring pipeline over a job's candidate pool.
``rubric_version_id`` is RESTRICT rather than CASCADE: a run is the record
of which criteria produced a ranking, so the rubric it cites must not be
deletable out from under it. The job itself CASCADEs — deleting a job is a
deliberate purge of everything scoped to it.
``triggered_by`` is a plain uuid, not a foreign key: the ``users`` table
described in ARCHITECTURE.md §6.4 does not exist in this repo yet, and
declaring the constraint would make the migration unrunnable.
"""
__tablename__ = "screening_runs"
__table_args__ = (Index("ix_screening_runs_tenant_job", "tenant_id", "job_id"),)
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
job_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("jobs.id", ondelete="CASCADE"),
nullable=False,
)
rubric_version_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("rubric_versions.id", ondelete="RESTRICT"),
nullable=False,
)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="queued")
mode: Mapped[str] = mapped_column(String(32), nullable=False, default="interactive")
candidate_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
# Per-stage survivor counts for the retrieval funnel, so a shortlist can be
# explained as "N of M candidates reached the judge" without re-running it.
funnel_stage_counts: Mapped[dict[str, object] | None] = mapped_column(
JSONB, nullable=True
)
started_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
completed_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
workflow_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
triggered_by: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True), nullable=True
)
cost_usd: Mapped[Decimal] = mapped_column(
Numeric(10, 4), nullable=False, default=Decimal("0")
)
total_input_tokens: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0)
total_output_tokens: Mapped[int] = mapped_column(
BigInteger, nullable=False, default=0
)
cache_read_tokens: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0)
cache_write_tokens: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0)
scores: Mapped[list[CandidateScore]] = relationship(
back_populates="run",
cascade="all, delete-orphan",
lazy="noload",
)
class CandidateScore(TimestampMixin, Base):
"""One candidate's aggregated result within a run.
``raw_weighted`` keeps four decimal places while ``overall_score`` keeps
two: the weighted sum is the input to capping and rounding, and storing it
pre-rounding is what makes a score replayable. ``cap_applied`` is NULL when
no must-have cap fired, so "was this candidate capped" is answerable without
recomputing the rubric.
``candidate_id`` and ``profile_id`` are plain uuids rather than foreign
keys: the ``candidates`` and ``candidate_profiles`` tables described in
ARCHITECTURE.md §6.3 are not built in this repo, and declaring the
constraints would make the migration unrunnable.
"""
__tablename__ = "candidate_scores"
__table_args__ = (
UniqueConstraint(
"run_id", "candidate_id", name="uq_candidate_scores_run_candidate"
),
Index("ix_candidate_scores_run_rank", "run_id", "rank"),
Index("ix_candidate_scores_tenant_run", "tenant_id", "run_id"),
)
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
run_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("screening_runs.id", ondelete="CASCADE"),
nullable=False,
)
candidate_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False
)
profile_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True), nullable=True
)
overall_score: Mapped[Decimal] = mapped_column(Numeric(5, 2), nullable=False)
raw_weighted: Mapped[Decimal] = mapped_column(Numeric(5, 4), nullable=False)
cap_applied: Mapped[int | None] = mapped_column(Integer, nullable=True)
# Assigned in a second pass once every candidate in the run is scored, so a
# partially-completed run has scores without ranks rather than wrong ranks.
rank: Mapped[int | None] = mapped_column(Integer, nullable=True)
retrieval_score: Mapped[float | None] = mapped_column(Float, nullable=True)
rerank_score: Mapped[float | None] = mapped_column(Float, nullable=True)
recommendation: Mapped[str | None] = mapped_column(String(32), nullable=True)
recommendation_confidence: Mapped[float | None] = mapped_column(Float, nullable=True)
summary: Mapped[str | None] = mapped_column(Text, nullable=True)
# Provenance for replay: which aggregation formula produced this number.
aggregation_formula_version: Mapped[str] = mapped_column(
String(32), nullable=False, default="v1"
)
run: Mapped[ScreeningRun] = relationship(back_populates="scores")
verdicts: Mapped[list[RequirementVerdict]] = relationship(
back_populates="score",
cascade="all, delete-orphan",
lazy="noload",
)
class RequirementVerdict(TimestampMixin, Base):
"""The judge's call on one requirement for one candidate.
``weight_at_scoring`` and ``contribution`` are stored rather than derived:
the rubric can mint a new version with different weights, and a verdict must
stay explainable against the weights that actually produced it.
``retrieved_chunk_ids`` is the exposure record — exactly what the judge saw.
Without it a "missing" verdict is indistinguishable from a retrieval failure.
``requirement_id`` is RESTRICT: a verdict cites a criterion, so the
criterion must outlive it. Override columns are all nullable and set
together when a human corrects the judge.
"""
__tablename__ = "requirement_verdicts"
__table_args__ = (
UniqueConstraint(
"score_id", "requirement_id", name="uq_requirement_verdicts_score_req"
),
Index("ix_requirement_verdicts_tenant_score", "tenant_id", "score_id"),
Index("ix_requirement_verdicts_cache_key", "result_cache_key"),
)
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
score_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("candidate_scores.id", ondelete="CASCADE"),
nullable=False,
)
requirement_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("requirements.id", ondelete="RESTRICT"),
nullable=False,
)
verdict: Mapped[str] = mapped_column(String(16), nullable=False)
confidence: Mapped[float] = mapped_column(Float, nullable=False)
weight_at_scoring: Mapped[Decimal] = mapped_column(Numeric(5, 4), nullable=False)
contribution: Mapped[Decimal] = mapped_column(Numeric(6, 4), nullable=False)
reasoning: Mapped[str | None] = mapped_column(Text, nullable=True)
judge_model: Mapped[str | None] = mapped_column(String(128), nullable=True)
judge_prompt_version: Mapped[str | None] = mapped_column(String(32), nullable=True)
judge_effort: Mapped[str | None] = mapped_column(String(32), nullable=True)
cache_hit: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
input_tokens: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
output_tokens: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
latency_ms: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
retrieved_chunk_ids: Mapped[list[uuid.UUID] | None] = mapped_column(
ARRAY(PG_UUID(as_uuid=True)), nullable=True
)
result_cache_key: Mapped[str | None] = mapped_column(String(64), nullable=True)
overridden_by: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True), nullable=True
)
override_verdict: Mapped[str | None] = mapped_column(String(16), nullable=True)
override_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
overridden_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
score: Mapped[CandidateScore] = relationship(back_populates="verdicts")
evidence_spans: Mapped[list[EvidenceSpanRecord]] = relationship(
back_populates="verdict",
cascade="all, delete-orphan",
lazy="noload",
)
class EvidenceSpanRecord(TimestampMixin, Base):
"""A verbatim quote from a resume backing one verdict.
Named ``EvidenceSpanRecord`` rather than ``EvidenceSpan`` because
``app.services.search`` already exposes an ``EvidenceSpan`` dataclass for
in-flight search results; this is the persisted form.
``verbatim_verified`` is set by the automated check that re-slices the
resume text at ``[start_char, end_char)`` and compares it to
``quoted_text`` — the anti-hallucination gate. A row with it False is a
quote the judge produced that the source does not contain.
``chunk_id`` is SET NULL on delete: re-indexing a resume replaces its
chunks, and the citation survives that because ``resume_version_id`` plus
the character offsets locate the quote independently of any chunk row.
"""
__tablename__ = "evidence_spans"
__table_args__ = (
Index("ix_evidence_spans_tenant_verdict", "tenant_id", "verdict_id"),
)
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
# Nullable so a span can be recorded while its verdict is still being built.
verdict_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("requirement_verdicts.id", ondelete="CASCADE"),
nullable=True,
)
resume_version_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("resume_versions.id", ondelete="RESTRICT"),
nullable=False,
)
chunk_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("resume_chunks.id", ondelete="SET NULL"),
nullable=True,
)
page: Mapped[int | None] = mapped_column(Integer, nullable=True)
start_char: Mapped[int] = mapped_column(Integer, nullable=False)
end_char: Mapped[int] = mapped_column(Integer, nullable=False)
quoted_text: Mapped[str] = mapped_column(Text, nullable=False)
verbatim_verified: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=False
)
relevance: Mapped[float | None] = mapped_column(Float, nullable=True)
verdict: Mapped[RequirementVerdict | None] = relationship(
back_populates="evidence_spans"
)
# ---------------------------------------------------------------------------
# Revised-stack tables (ARCHITECTURE-AGENTS.md §10)
# ---------------------------------------------------------------------------
class RunTask(TimestampMixin, Base):
"""One unit of work in the screening pipeline queue.
Replaces a Celery/Arq broker. Claimed via ``SELECT ... FOR UPDATE
SKIP LOCKED``. A rate-limit reschedule sets ``not_before`` and resets
``status='pending'`` without consuming a retry attempt — distinct
from a genuine failure (status='failed', attempt incremented).
"""
__tablename__ = "run_tasks"
__table_args__ = (
Index("ix_run_tasks_run_status", "run_id", "status", "not_before"),
)
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
run_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("screening_runs.id", ondelete="CASCADE"),
nullable=False,
)
stage: Mapped[str] = mapped_column(String(64), nullable=False)
agent_name: Mapped[str] = mapped_column(
String(64), nullable=False, default=""
)
candidate_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True), nullable=True
)
requirement_group: Mapped[dict[str, object] | None] = mapped_column(
JSONB, nullable=True
)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="pending")
claimed_by: Mapped[str | None] = mapped_column(String(128), nullable=True)
claimed_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
not_before: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
attempt: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
payload: Mapped[dict[str, object] | None] = mapped_column(JSONB, nullable=True)
result: Mapped[dict[str, object] | None] = mapped_column(JSONB, nullable=True)
error: Mapped[str | None] = mapped_column(Text, nullable=True)
class AgentResultCache(TimestampMixin, Base):
"""Durable agent result cache — the reproducibility mechanism.
A cache hit skips an LLM call entirely, so a re-run with an unchanged
rubric costs ~zero tokens and produces an identical score.
"""
__tablename__ = "agent_result_cache"
__table_args__ = (
Index("ix_agent_result_cache_tenant_agent", "tenant_id", "agent_name"),
)
cache_key: Mapped[str] = mapped_column(String(64), primary_key=True)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False
)
agent_name: Mapped[str] = mapped_column(String(64), nullable=False)
agent_version: Mapped[str] = mapped_column(String(32), nullable=False)
output: Mapped[dict[str, object]] = mapped_column(JSONB, nullable=False)
expires_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
class RateLimitBucket(Base):
"""Per-provider token-bucket state.
UNLOGGED — losing it on crash is an accepted degradation (bucket
resets, worst case: burst of provider 429s absorbed by the rate-limit
classifier).
"""
__tablename__ = "rate_limit_buckets"
__table_args__ = (
Index(
"ix_rate_limit_buckets_lookup",
"provider",
"model",
"api_key_hash",
"window",
"window_start",
unique=True,
),
{"prefixes": ["UNLOGGED"]},
)
provider: Mapped[str] = mapped_column(String(64), primary_key=True)
model: Mapped[str] = mapped_column(String(128), primary_key=True)
api_key_hash: Mapped[str] = mapped_column(String(64), primary_key=True)
window: Mapped[str] = mapped_column(
String(16), primary_key=True
) # 'rpm' | 'tpm' | 'rpd'
window_start: Mapped[datetime] = mapped_column(
DateTime(timezone=True), primary_key=True
)
used: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0)
cap: Mapped[int] = mapped_column(BigInteger, nullable=False)
class RunCheckpoint(TimestampMixin, Base):
"""Heartbeat for resume-after-restart. One row per run."""
__tablename__ = "run_checkpoints"
run_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("screening_runs.id", ondelete="CASCADE"),
primary_key=True,
)
last_stage: Mapped[str] = mapped_column(String(64), nullable=False, default="")
heartbeat_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=func.now
)
resumed_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
class AtsComplianceReport(TimestampMixin, Base):
"""ATS keyword/format compliance sidecar — never blended into overall_score."""
__tablename__ = "ats_compliance_reports"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
resume_version_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False
)
rubric_version_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True), nullable=True
)
keyword_coverage: Mapped[float] = mapped_column(Float, nullable=False)
matched_keywords: Mapped[list[str] | None] = mapped_column(
ARRAY(String), nullable=True
)
missing_keywords: Mapped[list[str] | None] = mapped_column(
ARRAY(String), nullable=True
)
format_flags: Mapped[dict[str, object] | None] = mapped_column(
JSONB, nullable=True
)
compliance_score: Mapped[float] = mapped_column(Float, nullable=False)
is_ats_safe: Mapped[bool] = mapped_column(Boolean, nullable=False)
computed_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=func.now
)
class FraudFlag(TimestampMixin, Base):
"""Deception signals — distinct from verdicts, never scored."""
__tablename__ = "fraud_flags"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
score_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("candidate_scores.id", ondelete="CASCADE"),
nullable=False,
)
signal_type: Mapped[str] = mapped_column(String(64), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False)
confidence: Mapped[float] = mapped_column(Float, nullable=False)
related_requirement_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True), nullable=True
)
judge_model: Mapped[str | None] = mapped_column(String(128), nullable=True)
prompt_version: Mapped[str | None] = mapped_column(String(32), nullable=True)
class BiasFlag(TimestampMixin, Base):
"""Language-bias findings in generated text — distinct from verdicts."""
__tablename__ = "bias_flags"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
score_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("candidate_scores.id", ondelete="CASCADE"),
nullable=False,
)
source: Mapped[str] = mapped_column(String(32), nullable=False)
text_excerpt: Mapped[str] = mapped_column(Text, nullable=False)
bias_category: Mapped[str] = mapped_column(String(64), nullable=False)
severity: Mapped[str] = mapped_column(String(16), nullable=False)
rationale: Mapped[str] = mapped_column(Text, nullable=False)
judge_model: Mapped[str | None] = mapped_column(String(128), nullable=True)
prompt_version: Mapped[str | None] = mapped_column(String(32), nullable=True)
# ---------------------------------------------------------------------------
# Phase 0 foundation tables — tenants, users, candidates, audit
# ---------------------------------------------------------------------------
class Tenant(TimestampMixin, Base):
"""One customer account. Not tenant-scoped — this IS the tenant."""
__tablename__ = "tenants"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
slug: Mapped[str] = mapped_column(String(64), nullable=False, unique=True)
plan: Mapped[str] = mapped_column(String(32), nullable=False, default="free")
retention_days: Mapped[int] = mapped_column(Integer, nullable=False, default=365)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
class User(TimestampMixin, Base):
"""A human user. Cross-tenant — membership is via user_roles."""
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
display_name: Mapped[str] = mapped_column(String(255), nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
class UserRole(TimestampMixin, Base):
"""Per-tenant role assignment. Composite PK: (user_id, tenant_id, role)."""
__tablename__ = "user_roles"
__table_args__ = (
Index("ix_user_roles_tenant", "tenant_id"),
)
user_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
primary_key=True,
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("tenants.id", ondelete="CASCADE"),
primary_key=True,
)
role: Mapped[str] = mapped_column(String(32), nullable=False, primary_key=True)
class ApiKey(TimestampMixin, Base):
"""Programmatic access key per tenant."""
__tablename__ = "api_keys"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("tenants.id", ondelete="CASCADE"),
nullable=False, index=True,
)
key_hash: Mapped[str] = mapped_column(String(64), nullable=False, unique=True)
key_prefix: Mapped[str] = mapped_column(String(12), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
last_used_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
class AuditEvent(TimestampMixin, Base):
"""Tamper-evident audit log. Hash-chained."""
__tablename__ = "audit_events"
__table_args__ = (
Index("ix_audit_events_tenant_time", "tenant_id", "occurred_at"),
Index("ix_audit_events_resource", "resource_type", "resource_id"),
)
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
user_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True), nullable=True
)
action: Mapped[str] = mapped_column(String(64), nullable=False)
resource_type: Mapped[str] = mapped_column(String(64), nullable=False)
resource_id: Mapped[str] = mapped_column(String(64), nullable=False)
details: Mapped[dict[str, object] | None] = mapped_column(JSONB, nullable=True)
ip_address: Mapped[str | None] = mapped_column(String(45), nullable=True)
request_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
occurred_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=func.now
)
prev_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
chain_hash: Mapped[str] = mapped_column(String(64), nullable=False)
class Candidate(TimestampMixin, Base):
"""A job applicant within a tenant."""
__tablename__ = "candidates"
__table_args__ = (
UniqueConstraint("tenant_id", "email", name="uq_candidates_tenant_email"),
)
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
external_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
name: Mapped[str] = mapped_column(String(255), nullable=False)
email: Mapped[str | None] = mapped_column(String(255), nullable=True)
phone: Mapped[str | None] = mapped_column(String(64), nullable=True)
consent_purpose: Mapped[str] = mapped_column(
String(64), nullable=False, default="candidate_screening"
)
consent_granted_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
class CandidateProfile(TimestampMixin, Base):
"""Structured extraction from a resume version."""
__tablename__ = "candidate_profiles"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
candidate_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("candidates.id", ondelete="CASCADE"),
nullable=False,
)
resume_version_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("resume_versions.id", ondelete="SET NULL"),
nullable=True,
)
extraction_status: Mapped[str] = mapped_column(
String(32), nullable=False, default="pending"
)
total_experience_months: Mapped[int | None] = mapped_column(Integer, nullable=True)
skills: Mapped[dict[str, object] | None] = mapped_column(JSONB, nullable=True)
education: Mapped[dict[str, object] | None] = mapped_column(JSONB, nullable=True)
languages: Mapped[dict[str, object] | None] = mapped_column(JSONB, nullable=True)
latest_role: Mapped[str | None] = mapped_column(String(255), nullable=True)
extraction_model: Mapped[str | None] = mapped_column(String(128), nullable=True)
extraction_prompt_version: Mapped[str | None] = mapped_column(
String(32), nullable=True
)
class SkillGap(TimestampMixin, Base):
"""One identified skill gap for a candidate."""
__tablename__ = "skill_gaps"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
score_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("candidate_scores.id", ondelete="CASCADE"),
nullable=False,
)
requirement_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False
)
severity: Mapped[str] = mapped_column(String(16), nullable=False)
gap_type: Mapped[str] = mapped_column(String(32), nullable=False)
suggested_probe: Mapped[str | None] = mapped_column(Text, nullable=True)
weight: Mapped[float] = mapped_column(Float, nullable=False)
class InterviewKit(TimestampMixin, Base):
"""One set of interview questions for a scored candidate."""
__tablename__ = "interview_kits"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
score_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("candidate_scores.id", ondelete="CASCADE"),
nullable=False,
)
class InterviewQuestion(TimestampMixin, Base):
"""One question in an interview kit."""
__tablename__ = "interview_questions"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
ordinal: Mapped[int] = mapped_column(Integer, nullable=False)
question: Mapped[str] = mapped_column(Text, nullable=False)
category: Mapped[str] = mapped_column(String(32), nullable=False)
targets_requirement_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True), nullable=True
)
difficulty: Mapped[str] = mapped_column(String(16), nullable=False)
rationale: Mapped[str | None] = mapped_column(Text, nullable=True)
expected_signal: Mapped[str | None] = mapped_column(Text, nullable=True)
follow_ups: Mapped[dict[str, object] | None] = mapped_column(
JSONB, nullable=True
)
kit_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("interview_kits.id", ondelete="CASCADE"),
nullable=False,
)
class Decision(TimestampMixin, Base):
"""A human recruiter's advance/reject decision with mandatory reason."""
__tablename__ = "decisions"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
score_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("candidate_scores.id", ondelete="RESTRICT"),
nullable=False,
)
decided_by: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("users.id", ondelete="RESTRICT"),
nullable=False,
)
decision: Mapped[str] = mapped_column(String(16), nullable=False)
reason: Mapped[str] = mapped_column(Text, nullable=False)
agreed_with_ai: Mapped[bool] = mapped_column(Boolean, nullable=False)
decided_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=func.now
)
class FairnessSnapshot(TimestampMixin, Base):
"""Nightly fairness metrics for one tenant."""
__tablename__ = "fairness_snapshots"
__table_args__ = (
UniqueConstraint(
"tenant_id", "computed_for_date",
name="uq_fairness_snapshots_tenant_date",
),
)
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
computed_for_date: Mapped[datetime.date] = mapped_column(
Date, nullable=False
)
metrics: Mapped[dict[str, object]] = mapped_column(JSONB, nullable=False)
computed_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=func.now
)
class ChatSession(TimestampMixin, Base):
"""One recruiter chat session (multi-turn RAG)."""
__tablename__ = "chat_sessions"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), nullable=False, index=True
)
job_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("jobs.id", ondelete="CASCADE"),
nullable=True,
)
candidate_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True), nullable=True
)
created_by: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("users.id", ondelete="RESTRICT"),
nullable=False,
)
last_active_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
class ChatMessage(TimestampMixin, Base):
"""One message in a chat session."""
__tablename__ = "chat_messages"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
session_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("chat_sessions.id", ondelete="CASCADE"),
nullable=False, index=True,
)
role: Mapped[str] = mapped_column(String(16), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
citations: Mapped[dict[str, object] | None] = mapped_column(
JSONB, nullable=True
)
tool_calls: Mapped[dict[str, object] | None] = mapped_column(
JSONB, nullable=True
)