Stores¶
varagity.stores.records
¶
Chunk-level data model — the canonical metadata record (spec §8.1).
Every chunk carries a complete, validated metadata record. Chunks live in
both stores (pgvector and Elasticsearch) and are joinable by the shared
identity (doc_id, original_index).
Identity derivation (plan decision #6): doc_id hashes the path
relative to DOCS_PATH — not the absolute path spec §8.1 sketched —
because absolute paths differ between host and container and across machines,
which would break idempotency and make golden eval sets non-portable. The
absolute path is still recorded in source.
ChunkRecord
¶
Bases: BaseModel
One chunk's full metadata record (every spec §8.1 field).
Attributes:
| Name | Type | Description |
|---|---|---|
doc_id |
str
|
Stable id per source document (see :func: |
chunk_id |
str
|
|
original_index |
int
|
Global monotonic chunk index across the corpus (the hybrid-fusion identity key, spec §8). |
chunk_index |
int
|
Chunk position within its document. |
source |
str
|
Absolute file path (host- or container-local; provenance only). |
file_name |
str
|
Basename of the source file. |
file_type |
str
|
|
page |
int | None
|
Page number (PDF; |
content |
str
|
Original chunk text. |
context |
str | None
|
LLM-generated situating blurb ( |
contextualized_content |
str
|
The text actually embedded and BM25-indexed;
identical to |
chunk_size |
int
|
Chunk size parameter used, in characters (provenance). |
chunk_overlap |
int
|
Chunk overlap parameter used, in characters. |
chunking_strategy |
str
|
Registry name of the chunker used. |
embedding_model |
str
|
Served embedding model name. |
n_tokens |
int
|
Approximate token count of |
content_hash |
str
|
The parent document's :func: |
created_at |
datetime
|
Ingestion timestamp (UTC) — when the chunk was made,
not a property of the source file (see |
file_created_at |
datetime | None
|
Filesystem birth time of the source file (UTC).
Best-effort: birth time is filesystem/platform-dependent, and a
copy or download resets it — |
file_modified_at |
datetime | None
|
Filesystem mtime of the source file (UTC) — when
the document's bytes last changed, independent of when they
were ingested. |
extraction |
str
|
How text was extracted: |
heading_path |
str | None
|
The chunk's markdown heading breadcrumb (e.g.
|
Source code in varagity/stores/records.py
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | |
create(*, doc_id, original_index, chunk_index, source, file_name, file_type, page, content, context, chunk_size, chunk_overlap, chunking_strategy, embedding_model, content_hash, file_created_at=None, file_modified_at=None, extraction='text', heading_path=None)
classmethod
¶
Build a record, deriving the dependent fields.
Derives chunk_id, n_tokens, created_at, and the
contextualized_content composition rule (spec §9.4): with a
context blurb it is context + "\n\n" + content; without one
(CONTEXTUALIZE off, plan decision #2) it is content
unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc_id
|
str
|
Stable id of the parent document. |
required |
original_index
|
int
|
Global monotonic chunk index across the corpus. |
required |
chunk_index
|
int
|
Chunk position within its document. |
required |
source
|
str
|
Absolute file path. |
required |
file_name
|
str
|
Basename of the source file. |
required |
file_type
|
str
|
|
required |
page
|
int | None
|
Page number (PDF; |
required |
content
|
str
|
Original chunk text. |
required |
context
|
str | None
|
LLM situating blurb, or |
required |
chunk_size
|
int
|
Chunk size parameter used (characters). |
required |
chunk_overlap
|
int
|
Chunk overlap parameter used (characters). |
required |
chunking_strategy
|
str
|
Registry name of the chunker used. |
required |
embedding_model
|
str
|
Served embedding model name. |
required |
content_hash
|
str
|
The parent document's content hash. |
required |
file_created_at
|
datetime | None
|
Filesystem birth time of the source file, or
|
None
|
file_modified_at
|
datetime | None
|
Filesystem mtime of the source file, or
|
None
|
extraction
|
str
|
Extraction provenance ( |
'text'
|
heading_path
|
str | None
|
Markdown heading breadcrumb, or |
None
|
Returns:
| Type | Description |
|---|---|
ChunkRecord
|
The fully-populated record. |
Source code in varagity/stores/records.py
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | |
DocumentInfo
¶
Bases: BaseModel
One ingested document, as listed by GET /api/documents (spec_v2 §4.2).
A documents-table row joined with its chunks' extraction mix — the
corpus-management view (file, type, chunk count, ingested-at, how much
of it came through OCR).
Attributes:
| Name | Type | Description |
|---|---|---|
doc_id |
str
|
The document's stable id. |
source |
str
|
Absolute file path recorded at ingest time. |
file_type |
str
|
File extension without the dot ( |
content_hash |
str
|
sha256 of the source file's bytes at ingest time —
the preview path re-hashes the on-disk file against it, so an
edited-but-not-reingested document degrades honestly
( |
n_chunks |
int
|
Chunks ingested ( |
ingested_at |
datetime
|
When the document (last) landed in the stores. |
extraction_mix |
dict[str, int]
|
Chunk count per extraction method ( |
Source code in varagity/stores/records.py
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 | |
GraphSourceSnapshot
¶
Bases: BaseModel
One cited transcript day of a graph-targeted turn (spec_graphrag §4.2).
The graph twin of :class:RetrievedChunk for persistence only: a graph
answer's evidence is document-grain (ADR-017's priced regret — the
engines cannot honestly attribute a single message), so what gets
snapshotted into message_sources is the transcript day the answer
cited, keyed by its doc_key.
A plain record rather than
:class:varagity.graph.records.TranscriptExcerpt on purpose: the store
layer must not import :mod:varagity.graph (the same rule that keeps
recent_turns returning primitive pairs instead of chat turns), and
the snapshot is a frozen shape — it has to keep explaining a
conversation after the graph it came from was rebuilt.
Attributes:
| Name | Type | Description |
|---|---|---|
doc_key |
str
|
The transcript document key, a soft reference exactly like
|
thread_name |
str
|
Human-facing thread label. |
span |
str
|
The document's day span ( |
excerpt |
str
|
The retrieved passage, capped for storage by the caller. |
message_guids |
list[str]
|
Per-message provenance for the day, or |
Source code in varagity/stores/records.py
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | |
RetrievalTrace
¶
Bases: BaseModel
Why one retrieved chunk ranked where it did (spec_v2 §9.2).
Query-time only — computed per answer by the retrievers, never persisted
on the immutable chunk rows. Ranks are 1-based (display-ready); an arm's
rank/score are None when that arm's ranked list never surfaced the
chunk. Single-arm retrievers report their arm's score/rank as the fused
values (there is nothing to fuse).
Attributes:
| Name | Type | Description |
|---|---|---|
semantic_rank |
int | None
|
Rank in the semantic (pgvector) arm. |
semantic_score |
float | None
|
Cosine similarity in the semantic arm. |
bm25_rank |
int | None
|
Rank in the BM25 (Elasticsearch) arm. |
bm25_score |
float | None
|
BM25 relevance in that arm. |
fused_score |
float
|
Weighted reciprocal-rank fusion score (spec §11.4). |
fused_rank |
int
|
Rank after fusion. |
rerank_score |
float | None
|
Cross-encoder relevance ( |
rerank_delta |
int | None
|
Positions moved by reranking, |
final_rank |
int
|
The rank actually returned to the caller. |
Source code in varagity/stores/records.py
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | |
RetrievedChunk
¶
Bases: BaseModel
A chunk returned by a store search, with its relevance score.
Field set mirrors the spec §11.2 SELECT list; metadata holds the
full :class:ChunkRecord dump persisted at ingest time.
Attributes:
| Name | Type | Description |
|---|---|---|
chunk_id |
str
|
The chunk's primary key. |
doc_id |
str
|
Parent document id. |
original_index |
int
|
Global chunk index (fusion identity). |
content |
str
|
Original chunk text. |
context |
str | None
|
LLM situating blurb ( |
metadata |
dict[str, Any]
|
Full persisted metadata record. |
score |
float
|
Similarity score — cosine similarity |
trace |
RetrievalTrace | None
|
Rank provenance attached by the retrievers (spec_v2 §9.2);
|
Source code in varagity/stores/records.py
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | |
content_hash(data)
¶
Hash raw file bytes for idempotency / dedup (spec §8.2).
Hashing the bytes (rather than parsed text) lets re-runs skip unchanged files before paying the parse cost.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
bytes
|
The file's raw bytes. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The sha256 hex digest. |
Source code in varagity/stores/records.py
23 24 25 26 27 28 29 30 31 32 33 34 35 | |
derive_doc_id(relative_path, file_hash)
¶
Derive the stable per-document id (plan decision #6).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
relative_path
|
str
|
POSIX-style path of the document relative to
|
required |
file_hash
|
str
|
The document's :func: |
required |
Returns:
| Type | Description |
|---|---|
str
|
|
Source code in varagity/stores/records.py
38 39 40 41 42 43 44 45 46 47 48 49 | |
varagity.stores.vector_store
¶
ContextualVectorDB — the pgvector-backed dense vector store.
The Anthropic cookbook's in-memory ContextualVectorDB responsibilities
(store vectors + metadata, cosine search), re-backed with
PostgreSQL/pgvector (spec §11.2) so the index is durable, concurrent, and
inspectable via SQL. The schema itself lives in schema.sql (applied by
the postgres container on first boot).
It also owns the RAG corpus's advisory lock (ADR-019): one fixed key in
the database's advisory-lock namespace that every ingest holds shared for
its whole run and a backup restore holds exclusive from before it empties
the tables until the BM25 index is rebuilt. The API's in-process corpus gate
cannot see a CLI ingest (main.py ingest / chat run in their own
process); this lock can, because it lives in the one place every writer
connects to. Ingests still overlap each other exactly as before, and a
session-level lock dies with its connection, so a crashed process never
strands the corpus.
ContextualVectorDB
¶
Bases: ClosingContextMixin
Dense vector store over PostgreSQL + pgvector.
Owns one connection (autocommit; per-document writes group into an
explicit transaction via :meth:store_document). Use as a context
manager or call :meth:close when done.
Source code in varagity/stores/vector_store.py
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 | |
__init__(conninfo=None)
¶
Connect and register the pgvector type adapters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conninfo
|
str | None
|
libpq connection string; defaults to the
|
None
|
Raises:
| Type | Description |
|---|---|
OperationalError
|
If the database is unreachable. |
Source code in varagity/stores/vector_store.py
456 457 458 459 460 461 462 463 464 465 466 467 | |
chunk_count()
¶
Count the stored chunks.
Backs the varagity_corpus_chunks gauge (spec_v3 §6.1a): the
store is the truthful source for "how big is my corpus", unlike the
per-process ingest counters that reset with the API.
Returns:
| Type | Description |
|---|---|
int
|
Number of |
Source code in varagity/stores/vector_store.py
521 522 523 524 525 526 527 528 529 530 531 532 | |
chunk_count_by_strategy()
¶
Count the stored chunks per chunking strategy.
Backs the varagity_corpus_chunks_by_strategy gauge (spec_v3
§6.1a). The strategy is read from each chunk's metadata JSONB
(it is not a column); chunks lacking the field count as
unknown.
Returns:
| Type | Description |
|---|---|
dict[str, int]
|
|
Source code in varagity/stores/vector_store.py
547 548 549 550 551 552 553 554 555 556 557 558 559 | |
close()
¶
Close the underlying connection (idempotent).
Source code in varagity/stores/vector_store.py
469 470 471 472 | |
corpus_lock(*, exclusive)
¶
Hold the RAG corpus's advisory lock for the with block.
Ingests hold it shared — they overlap each other exactly as they always have — and a backup restore holds it exclusive, so a restore and an ingest in any two processes exclude each other (ADR-019). The lock belongs to this store's database session: it is released when the block exits, and with the connection if the process dies first. Never blocks — a held lock is refused at once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
exclusive
|
bool
|
Take it exclusively (a restore) rather than shared (an ingest). |
required |
Yields:
| Type | Description |
|---|---|
None
|
Nothing; the corpus lock is held until the block exits. |
Raises:
| Type | Description |
|---|---|
CorpusLocked
|
If the lock is held in a mode that excludes this request. |
Source code in varagity/stores/vector_store.py
598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 | |
corpus_lock_free()
¶
Report whether a restore could take the corpus lock right now.
Takes the lock exclusively and gives it straight back — so False
means an ingest, in this process or another, holds it shared. The
restore route's pre-202 check; the restore itself still takes the
lock for real (and fails cleanly if an ingest won the race).
Returns:
| Type | Description |
|---|---|
bool
|
|
Source code in varagity/stores/vector_store.py
632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 | |
delete_document(doc_id)
¶
Delete a document row and, via FK cascade, all its chunks.
Backs ingest --reingest: pipeline-setting changes (e.g. toggling
CONTEXTUALIZE) don't change content hashes, so re-processing an
unchanged file requires deleting its previous ingest first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc_id
|
str
|
The document's stable id (deleting an unknown id is a no-op). |
required |
Returns:
| Type | Description |
|---|---|
int
|
The number of |
Source code in varagity/stores/vector_store.py
805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 | |
delete_documents(doc_ids)
¶
Delete several document rows and, via FK cascade, their chunks.
Backs the corpus table's multi-select delete (spec_v2 §4.2): one statement for the whole set, so the marker rows the delete route writes last all fall in the same round trip.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc_ids
|
Sequence[str]
|
The documents' stable ids (unknown ids are no-ops; an empty sequence skips the statement entirely). |
required |
Returns:
| Type | Description |
|---|---|
int
|
The number of |
Source code in varagity/stores/vector_store.py
821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 | |
document_chunks(doc_id)
¶
Fetch one document's chunks in reading order.
Backs the eval harness's fact-anchored golden resolution (spec_v2 §7.4): a chunker sweep must locate which strategy-true chunk holds a planted fact, so it scans the document's stored contents rather than trusting index-anchored refs authored against other boundaries.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc_id
|
str
|
The parent document id. |
required |
Returns:
| Type | Description |
|---|---|
list[RetrievedChunk]
|
The document's chunks ordered by |
list[RetrievedChunk]
|
document is unknown or has no extractable text). Chunks carry |
list[RetrievedChunk]
|
|
Source code in varagity/stores/vector_store.py
971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 | |
document_count()
¶
Count the ingested documents.
Backs the settings route's corpus-stale check (spec_v2 §4.7): an empty corpus has nothing to go stale, so a reingest-affecting override change only raises the flag when this is positive.
Returns:
| Type | Description |
|---|---|
int
|
Number of |
Source code in varagity/stores/vector_store.py
508 509 510 511 512 513 514 515 516 517 518 519 | |
document_count_by_type()
¶
Count the ingested documents per file type.
Backs the varagity_corpus_documents_by_type gauge (spec_v3
§6.1a) — the store-derived replacement for the Ingestion
dashboard's increase()-over-a-flat-counter panel.
Returns:
| Type | Description |
|---|---|
dict[str, int]
|
|
Source code in varagity/stores/vector_store.py
534 535 536 537 538 539 540 541 542 543 544 545 | |
document_exists(doc_id, content_hash)
¶
Check whether a document was already ingested (idempotency, spec §8.2).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc_id
|
str
|
The document's stable id. |
required |
content_hash
|
str
|
The document's content hash ( |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Source code in varagity/stores/vector_store.py
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 | |
document_n_chunks(doc_id)
¶
Fetch a document's recorded chunk count.
Lets the loader re-warn about known-empty documents (n_chunks = 0)
without re-parsing them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc_id
|
str
|
The document's stable id. |
required |
Returns:
| Type | Description |
|---|---|
int | None
|
The |
Source code in varagity/stores/vector_store.py
491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 | |
fetch_by_identity(keys)
¶
Fetch full chunk rows by their fusion identity (spec §11.4 hydrate).
Backs the bm25/hybrid retrievers: Elasticsearch only stores the
identity and text fields, so complete records (source, page, context,
full metadata) are hydrated from here. The returned chunks carry
score = 0.0 — relevance belongs to the caller, which attaches its
own (BM25 or fused) score.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keys
|
list[tuple[str, int]]
|
|
required |
Returns:
| Type | Description |
|---|---|
dict[tuple[str, int], RetrievedChunk]
|
A mapping of identity tuple → chunk for every key that exists; |
dict[tuple[str, int], RetrievedChunk]
|
unknown keys are simply absent (the caller decides whether that |
dict[tuple[str, int], RetrievedChunk]
|
is an inconsistency worth logging). |
Source code in varagity/stores/vector_store.py
990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 | |
get_document(doc_id)
¶
Fetch one ingested document by id.
Backs the preview routes (ADR-010): resolving doc_id →
source/content_hash happens once per locate/render request,
so it gets a single-row lookup instead of a full-corpus list.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc_id
|
str
|
The document's stable id. |
required |
Returns:
| Type | Description |
|---|---|
DocumentInfo | None
|
The document row with its extraction mix, or |
DocumentInfo | None
|
id is unknown. |
Source code in varagity/stores/vector_store.py
788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 | |
iter_corpus()
¶
Open one consistent snapshot of the whole corpus (a backup export).
Everything a RAG bundle holds — every documents row, then every
chunks row with its vector — is read inside one REPEATABLE
READ, READ ONLY transaction (ADR-019), so the export is a single
moment of the corpus even while an ingest or delete commits beside
it. The rows stream through server-side cursors, so memory stays
flat however large the corpus is. Readers never block writers in
Postgres: the open snapshot holds no lock an ingest would wait on.
Yields:
| Type | Description |
|---|---|
CorpusSnapshot
|
The snapshot, valid until the |
CorpusSnapshot
|
the transaction). |
Source code in varagity/stores/vector_store.py
576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 | |
iter_index_rows()
¶
Stream what the BM25 index mirrors of every chunk.
Backs the restore's BM25 rebuild (ADR-019): the five fields of each
stored chunk, through a server-side cursor so the corpus streams in
bounded memory. The transaction stays open until the iterator is
exhausted or closed — close it (contextlib.closing) when
abandoning it early.
Yields:
| Type | Description |
|---|---|
CorpusIndexRow
|
One row per chunk, |
Source code in varagity/stores/vector_store.py
767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 | |
list_documents()
¶
List every ingested document with its extraction mix.
Backs GET /api/documents (spec_v2 §4.2): the corpus-management
table of file, type, chunk count, ingested-at, and how many chunks
came through OCR versus the digital text layer.
Returns:
| Name | Type | Description |
|---|---|---|
One |
list[DocumentInfo]
|
class: |
list[DocumentInfo]
|
|
|
list[DocumentInfo]
|
included, with an empty mix). |
Source code in varagity/stores/vector_store.py
561 562 563 564 565 566 567 568 569 570 571 572 573 574 | |
next_original_index()
¶
Allocate the next global chunk index (called once per ingest run).
Returns:
| Type | Description |
|---|---|
int
|
|
int
|
empty; the loader then increments monotonically in-process. |
Source code in varagity/stores/vector_store.py
842 843 844 845 846 847 848 849 850 851 852 | |
replace_corpus(documents, chunks, *, on_indexing=None)
¶
Replace every documents and chunks row in one transaction.
The backup restore's write (ADR-019), in this order: empty both
tables, drop the HNSW index (its definition saved from
pg_indexes), binary-COPY the documents and then the chunks,
rebuild the index from that definition, ANALYZE, commit. The
commit is where the restore takes effect: any failure before it —
a row the bundle got wrong, a stream that turns out damaged, the
caller raising mid-iteration — rolls the whole thing back and the
previous corpus is untouched. TRUNCATE holds both tables until
then, so a concurrent reader waits for the restored rows rather
than seeing half of them.
The two streams are consumed strictly in order (documents first, as the foreign key demands), so a chunk stream may depend on what the document stream has already yielded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
documents
|
Iterable[CorpusDocumentRow]
|
Every document row, in any order. |
required |
chunks
|
Iterable[CorpusChunkRow]
|
Every chunk row with its vector; each |
required |
on_indexing
|
Callable[[], None] | None
|
Called once the rows are loaded, just before the vector index build — the slow tail of a large restore. |
None
|
Returns:
| Type | Description |
|---|---|
tuple[int, int]
|
|
Raises:
| Type | Description |
|---|---|
CorpusTablesBusy
|
If another session held the tables past the lock timeout (nothing was replaced). |
Source code in varagity/stores/vector_store.py
649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 | |
search(query_vector, k, verbose=None)
¶
Cosine top-k search (spec §11.2).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query_vector
|
list[float]
|
The query embedding (e5 query mode — see
:meth: |
required |
k
|
int
|
Number of chunks to return. |
required |
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
list[RetrievedChunk]
|
The top-k chunks by cosine similarity, best first, with |
list[RetrievedChunk]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/stores/vector_store.py
948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 | |
store_document(*, doc_id, source, file_type, content_hash, records, embeddings)
¶
Atomically upsert a document row and all its chunks.
One transaction per document: a partial failure rolls back the
documents row too, so a re-run's idempotency check re-attempts
the file instead of skipping a half-ingested one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc_id
|
str
|
The document's stable id. |
required |
source
|
str
|
Absolute file path (provenance). |
required |
file_type
|
str
|
|
required |
content_hash
|
str
|
The document's content hash. |
required |
records
|
list[ChunkRecord]
|
The chunk metadata records. |
required |
embeddings
|
list[list[float]]
|
One vector per record (same order). |
required |
Source code in varagity/stores/vector_store.py
914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 | |
upsert_chunks(records, embeddings)
¶
Insert or update one chunks row per record.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
records
|
list[ChunkRecord]
|
The chunk metadata records. |
required |
embeddings
|
list[list[float]]
|
One vector per record (same order). |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
UniqueViolation
|
If an insert collides on the
|
Source code in varagity/stores/vector_store.py
878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 | |
upsert_document(*, doc_id, source, file_type, content_hash, n_chunks)
¶
Insert or update one documents row.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc_id
|
str
|
The document's stable id. |
required |
source
|
str
|
Absolute file path (provenance). |
required |
file_type
|
str
|
|
required |
content_hash
|
str
|
The document's content hash. |
required |
n_chunks
|
int
|
Number of chunks ingested ( |
required |
Source code in varagity/stores/vector_store.py
854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 | |
CorpusChunkRow
dataclass
¶
One chunks row, every column, as a corpus snapshot reads it.
Attributes:
| Name | Type | Description |
|---|---|---|
chunk_id |
str
|
|
doc_id |
str
|
The owning document. |
original_index |
int
|
The corpus-global index (the cross-store join key). |
chunk_index |
int
|
Position within the document. |
content |
str
|
The original chunk text. |
context |
str | None
|
The contextual blurb ( |
contextualized_content |
str
|
The embedded and BM25-indexed text. |
metadata |
dict[str, Any]
|
The full |
created_at |
datetime
|
The chunk's creation timestamp. |
embedding |
Vector
|
The passage vector, exactly as stored (read in pgvector's binary format, so no decimal round trip touches a bit). |
Source code in varagity/stores/vector_store.py
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 | |
CorpusDocumentRow
dataclass
¶
One documents row, every column, as a corpus snapshot reads it.
Attributes:
| Name | Type | Description |
|---|---|---|
doc_id |
str
|
The document's stable id. |
source |
str
|
Absolute file path recorded at ingest time. |
file_type |
str
|
File extension without the dot. |
content_hash |
str
|
sha256 of the source file's bytes at ingest time. |
n_chunks |
int
|
Chunks ingested ( |
ingested_at |
datetime
|
When the document (last) landed in the stores. |
Source code in varagity/stores/vector_store.py
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 | |
CorpusIndexRow
dataclass
¶
What the BM25 index mirrors of one chunk — its five fields.
Attributes:
| Name | Type | Description |
|---|---|---|
chunk_id |
str
|
|
doc_id |
str
|
The owning document. |
original_index |
int
|
The corpus-global index (the fusion join key). |
content |
str
|
The original chunk text. |
contextualized_content |
str
|
The embedded and BM25-indexed text. |
Source code in varagity/stores/vector_store.py
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 | |
CorpusLocked
¶
Bases: RuntimeError
Raised when the corpus lock is held in a mode that excludes the request.
Attributes:
| Name | Type | Description |
|---|---|---|
exclusive |
What the refused request wanted — |
Source code in varagity/stores/vector_store.py
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | |
__init__(*, exclusive)
¶
Word the refusal for whoever was refused.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
exclusive
|
bool
|
Whether the refused request wanted the lock exclusively. |
required |
Source code in varagity/stores/vector_store.py
264 265 266 267 268 269 270 271 272 273 274 275 276 277 | |
CorpusSnapshot
¶
One consistent, read-only view of documents and chunks.
Handed out by :meth:ContextualVectorDB.iter_corpus for the life of its
with block: the counts and both row streams come from the same
REPEATABLE READ transaction, so an ingest or delete committing mid-export
can never tear a bundle — every row belongs to one moment, and the
counts agree with the streams.
Attributes:
| Name | Type | Description |
|---|---|---|
document_count |
|
|
chunk_count |
|
Source code in varagity/stores/vector_store.py
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 | |
__init__(conn, *, document_count, chunk_count)
¶
Bind the snapshot to its open transaction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conn
|
Connection[Any]
|
The connection holding the snapshot's transaction. |
required |
document_count
|
int
|
|
required |
chunk_count
|
int
|
|
required |
Source code in varagity/stores/vector_store.py
374 375 376 377 378 379 380 381 382 383 384 385 386 | |
chunks()
¶
Stream every chunks row with its vector, ingest order.
Yields:
| Type | Description |
|---|---|
CorpusChunkRow
|
One row per chunk, ordered by |
CorpusChunkRow
|
breaking ties) — the order a bundle's |
CorpusChunkRow
|
embedding matrix share. |
Source code in varagity/stores/vector_store.py
397 398 399 400 401 402 403 404 405 406 | |
documents()
¶
Stream every documents row, doc_id order.
Yields:
| Type | Description |
|---|---|
CorpusDocumentRow
|
One row per document. |
Source code in varagity/stores/vector_store.py
388 389 390 391 392 393 394 395 | |
CorpusTablesBusy
¶
Bases: RuntimeError
Raised when a restore can't get the corpus tables to itself in time.
Some session kept documents/chunks open past the restore's lock
timeout (an idle-in-transaction connection, a long dump). Nothing was
replaced: the restore's transaction rolled back.
Source code in varagity/stores/vector_store.py
280 281 282 283 284 285 286 | |
default_conninfo()
¶
Build the connection string from settings.
Shared by every Postgres-backed store (this one, the conversation
store) and the migration runner, so POSTGRES_* is interpreted in
exactly one place.
Returns:
| Type | Description |
|---|---|
str
|
A libpq conninfo string for the configured PostgreSQL instance. |
Source code in varagity/stores/vector_store.py
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 | |
varagity.stores.bm25_store
¶
ElasticsearchBM25 — the sparse (contextual BM25) store.
Ported near-verbatim from the Anthropic cookbook's ElasticsearchBM25
(spec §11.3): an index with the spec §8.3 mapping — content and
contextualized_content analyzed with the built-in english analyzer
under BM25 similarity, identity fields stored but not indexed — plus bulk
indexing and a multi_match search over both text fields. Because chunks
are contextualized before they reach this store,
the BM25 index is contextual from its first document.
Identity fields are mapped "index": false but keep doc values, so
term-level queries against them (delete_documents' delete_by_query)
still work — just via a slower doc-values scan, which is fine at dev scale.
Every field of an indexed document is a copy of a chunks column, so the
whole index can be rebuilt from the stored rows with no model call —
:meth:ElasticsearchBM25.rebuild, which a backup restore runs (ADR-019).
BM25Hit
¶
Bases: BaseModel
One BM25 search result (the spec §11.3 return fields).
Carries the fusion identity plus the indexed texts — not the full
metadata record, which lives in pgvector; retrievers hydrate complete
rows by (doc_id, original_index).
Attributes:
| Name | Type | Description |
|---|---|---|
doc_id |
str
|
Parent document id. |
original_index |
int
|
Global chunk index (fusion identity). |
content |
str
|
Original chunk text. |
contextualized_content |
str
|
The situated text that was BM25-indexed. |
score |
float
|
Elasticsearch BM25 relevance score (unbounded; higher is better). |
Source code in varagity/stores/bm25_store.py
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | |
ElasticsearchBM25
¶
Bases: ClosingContextMixin
Sparse keyword store over an Elasticsearch BM25 index.
Owns one client. Use as a context manager or call :meth:close when
done. The index is created lazily via :meth:create_index (the loader
calls it idempotently at ingest start).
Source code in varagity/stores/bm25_store.py
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 | |
__init__(*, url=None, index_name=None)
¶
Create the client (no I/O until the first operation).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str | None
|
Elasticsearch base URL; defaults to
|
None
|
index_name
|
str | None
|
BM25 index name; defaults to
|
None
|
Source code in varagity/stores/bm25_store.py
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | |
close()
¶
Close the underlying client (idempotent).
Source code in varagity/stores/bm25_store.py
179 180 181 | |
create_index()
¶
Create the BM25 index if it doesn't exist (idempotent).
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
existed. |
Raises:
| Type | Description |
|---|---|
ConnectionError
|
If Elasticsearch is still unreachable after retries. |
Source code in varagity/stores/bm25_store.py
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | |
delete_document(doc_id)
¶
Delete all of a document's chunks (--reingest consistency).
Keeps re-ingestion consistent across both stores: the vector store
deletes via FK cascade, this store via delete_by_query.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc_id
|
str
|
The document's stable id (deleting an unknown id is a no-op). |
required |
Returns:
| Type | Description |
|---|---|
int
|
The number of chunks deleted. |
Raises:
| Type | Description |
|---|---|
ConnectionError
|
If Elasticsearch is still unreachable after retries. |
Source code in varagity/stores/bm25_store.py
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 | |
delete_documents(doc_ids)
¶
Delete every chunk of several documents in one pass (bulk GC).
One terms delete_by_query rather than a query per document:
the whole set costs a single round trip and a single forced index
refresh, which is what makes the corpus table's multi-select delete
(spec_v2 §4.2) cheap. The retry lives here, so the single-document
wrapper inherits it exactly once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc_ids
|
Sequence[str]
|
The documents' stable ids (unknown ids are no-ops; an empty sequence skips the round trip entirely). |
required |
Returns:
| Type | Description |
|---|---|
int
|
The number of chunks deleted across all of them. |
Raises:
| Type | Description |
|---|---|
ConnectionError
|
If Elasticsearch is still unreachable after retries. |
Source code in varagity/stores/bm25_store.py
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 | |
index_chunks(records)
¶
Bulk-index chunks, then refresh so they are searchable.
Documents are addressed by chunk_id, so re-indexing the same
chunk overwrites instead of duplicating — the sparse-side counterpart
of the vector store's ON CONFLICT (chunk_id) DO UPDATE.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
records
|
list[ChunkRecord]
|
The chunk metadata records to index. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The number of successfully indexed documents. |
Raises:
| Type | Description |
|---|---|
BulkIndexError
|
If any document is rejected (permanent — not retried). |
ConnectionError
|
If Elasticsearch is still unreachable after retries. |
Source code in varagity/stores/bm25_store.py
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | |
rebuild(chunks, *, on_indexed=None)
¶
Recreate the index from scratch and stream every chunk into it.
A backup restore's second half (ADR-019): the index is deleted and
created anew with the spec mapping — so nothing of the previous
corpus survives, and no auto-created default mapping can either —
then the chunks stream in through helpers.streaming_bulk (bounded
memory however large the corpus) and a final refresh makes them
searchable. Index recreation is retried like every other call; a
failure mid-stream raises, and running the rebuild again repairs the
index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
chunks
|
Iterable[IndexableChunk]
|
Every chunk the index should hold. |
required |
on_indexed
|
Callable[[int], None] | None
|
Called with the running count after each indexed chunk (the restore's progress ticks). |
None
|
Returns:
| Type | Description |
|---|---|
int
|
The number of chunks indexed. |
Raises:
| Type | Description |
|---|---|
BulkIndexError
|
If any chunk is rejected. |
ConnectionError
|
If Elasticsearch is unreachable (after retries, for the index recreation). |
Source code in varagity/stores/bm25_store.py
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | |
search(query, k, verbose=None)
¶
BM25 top-k search via multi_match over both text fields.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
The user's query, unformatted (Elasticsearch's |
required |
k
|
int
|
Number of hits to return. |
required |
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
list[BM25Hit]
|
The top-k hits, best first. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
ConnectionError
|
If Elasticsearch is still unreachable after retries. |
Source code in varagity/stores/bm25_store.py
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 | |
IndexableChunk
¶
Bases: Protocol
What the index needs of one chunk: the five fields it mirrors.
Satisfied by :class:~varagity.stores.records.ChunkRecord (ingest) and
by the vector store's CorpusIndexRow (a restore's rebuild).
Source code in varagity/stores/bm25_store.py
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | |
varagity.stores.conversation_store
¶
Conversation persistence over PostgreSQL (spec_v2 §4.4, §9.1).
Single-user chat history lives in the same Postgres as the chunks (one
datastore to operate) but in independent tables: an assistant turn's
evidence is snapshotted into message_sources.trace — content,
context, source provenance, and the
:class:~varagity.stores.records.RetrievalTrace — with chunk_id kept
only as a soft reference, so a historical conversation still explains
itself after a reingest replaces the chunk rows. A graph-targeted turn
(spec_graphrag §4.2) rides the same table under a kind discriminator:
its cited transcript days are snapshotted with chunk_id holding the
transcript doc_key, and the same reasoning applies — a graph rebuild
re-derives keys, and the answer must still explain itself afterwards.
The tables are created by the migration runner
(:mod:varagity.stores.migrate), not schema.sql.
DEFAULT_TITLE = 'New conversation'
module-attribute
¶
Title given at creation; :meth:ConversationStore.auto_title replaces it.
ConversationDetail
¶
Bases: BaseModel
A full transcript: the conversation row plus its messages.
Attributes:
| Name | Type | Description |
|---|---|---|
conversation_id |
str
|
The app-generated id. |
title |
str
|
Current title. |
created_at |
datetime
|
Creation timestamp. |
updated_at |
datetime
|
Last-turn timestamp. |
messages |
list[MessageRecord]
|
All messages, oldest first, each with its sources. |
Source code in varagity/stores/conversation_store.py
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | |
ConversationGroup
¶
Bases: BaseModel
One sidebar conversation group (a user-created folder).
Attributes:
| Name | Type | Description |
|---|---|---|
group_id |
str
|
The app-generated id. |
name |
str
|
Display name (not unique — ids are the identity). |
created_at |
datetime
|
Creation timestamp. |
Source code in varagity/stores/conversation_store.py
113 114 115 116 117 118 119 120 121 122 123 124 | |
ConversationStore
¶
Bases: ClosingContextMixin
Conversation CRUD over PostgreSQL.
Owns one autocommit connection (per-turn writes group into explicit
transactions), mirroring
:class:~varagity.stores.vector_store.ContextualVectorDB. Use as a
context manager or call :meth:close when done.
Source code in varagity/stores/conversation_store.py
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 | |
__init__(conninfo=None)
¶
Connect to the configured PostgreSQL instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conninfo
|
str | None
|
libpq connection string; defaults to the
|
None
|
Raises:
| Type | Description |
|---|---|
OperationalError
|
If the database is unreachable. |
Source code in varagity/stores/conversation_store.py
271 272 273 274 275 276 277 278 279 280 281 | |
append_message(conversation_id, role, content, *, retrieval_method=None, latency_ms=None, reasoning=None, condensed_query=None, chat_engine=None, sources=(), target_corpus=None, graph_evidence=None, graph_sources=())
¶
Persist one turn, snapshotting an assistant turn's evidence.
One transaction covers the message, its message_sources rows,
and the conversation's updated_at bump — a partial failure
leaves no dangling turn.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conversation_id
|
str
|
The conversation the turn belongs to. |
required |
role
|
str
|
|
required |
content
|
str
|
The question or the generated answer. |
required |
retrieval_method
|
str | None
|
Retrieval method used (assistant turns) — the
registry name for a chunk turn, the engine query mode for a
graph one ( |
None
|
latency_ms
|
dict[str, Any] | None
|
Per-stage timings (assistant turns). |
None
|
reasoning
|
str | None
|
Captured |
None
|
condensed_query
|
str | None
|
The standalone search query that drove the
turn's retrieval (assistant turns; |
None
|
chat_engine
|
str | None
|
Registry name of the chat engine that produced the turn (assistant turns). |
None
|
sources
|
Sequence[RetrievedChunk]
|
The answer's retrieved chunks, best first; each is snapshotted via the spec_v2 §9.1 trace blob. Empty on a graph turn, which cites transcript days instead. |
()
|
target_corpus
|
str | None
|
Which corpus the turn asked for ( |
None
|
graph_evidence
|
dict[str, Any] | None
|
The graph retrieval's entities/relations and
mode; |
None
|
graph_sources
|
Sequence[GraphSourceSnapshot]
|
The cited transcript days, best first; each
becomes a |
()
|
Returns:
| Type | Description |
|---|---|
str
|
The new message's id. |
Raises:
| Type | Description |
|---|---|
ForeignKeyViolation
|
If |
Source code in varagity/stores/conversation_store.py
514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 | |
auto_title(conversation_id, question, *, llm=None)
¶
Title a still-default-titled conversation from its first question.
A cheap LLM one-liner (spec_v2 §4.2); on any generation failure the title falls back to the truncated question — titling must never break a chat turn. Conversations already titled (auto or manually) are left alone.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conversation_id
|
str
|
The conversation to title. |
required |
question
|
str
|
The first user question. |
required |
llm
|
LLMClient | None
|
Chat client; resolved via the model registry when omitted. |
None
|
Returns:
| Type | Description |
|---|---|
str | None
|
The new title, or |
str | None
|
no longer carries :data: |
Source code in varagity/stores/conversation_store.py
616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 | |
close()
¶
Close the underlying connection (idempotent).
Source code in varagity/stores/conversation_store.py
283 284 285 286 | |
conversation_exists(conversation_id)
¶
Check whether a conversation id is known.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conversation_id
|
str
|
The id to look up. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Source code in varagity/stores/conversation_store.py
313 314 315 316 317 318 319 320 321 322 323 324 325 | |
create_conversation(title=None)
¶
Insert a new conversation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
title
|
str | None
|
Initial title; defaults to :data: |
None
|
Returns:
| Type | Description |
|---|---|
ConversationSummary
|
The created conversation's summary ( |
Source code in varagity/stores/conversation_store.py
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | |
create_group(name)
¶
Insert a new conversation group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Display name (uniqueness is not enforced — ids are the identity). |
required |
Returns:
| Type | Description |
|---|---|
ConversationGroup
|
The created group. |
Source code in varagity/stores/conversation_store.py
444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 | |
delete_conversation(conversation_id)
¶
Delete a conversation; messages and sources cascade (spec_v2 §9.1).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conversation_id
|
str
|
The conversation to delete (unknown id is a no-op). |
required |
Returns:
| Type | Description |
|---|---|
int
|
The number of |
Source code in varagity/stores/conversation_store.py
419 420 421 422 423 424 425 426 427 428 429 430 431 432 | |
delete_group(group_id)
¶
Delete a group; its conversations detach (ON DELETE SET NULL).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_id
|
str
|
The group to delete (unknown id is a no-op). |
required |
Returns:
| Type | Description |
|---|---|
int
|
The number of |
Source code in varagity/stores/conversation_store.py
476 477 478 479 480 481 482 483 484 485 486 487 488 | |
get_conversation(conversation_id)
¶
Fetch a full transcript: messages plus snapshotted sources.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conversation_id
|
str
|
The conversation to fetch. |
required |
Returns:
| Type | Description |
|---|---|
ConversationDetail | None
|
The transcript, or |
Source code in varagity/stores/conversation_store.py
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 | |
group_exists(group_id)
¶
Check whether a group id is known.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_id
|
str
|
The id to look up. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Source code in varagity/stores/conversation_store.py
462 463 464 465 466 467 468 469 470 471 472 473 474 | |
list_conversations()
¶
List every conversation, most recently updated first.
Returns:
| Type | Description |
|---|---|
list[ConversationSummary]
|
Summaries with message counts (spec_v2 §4.2). |
Source code in varagity/stores/conversation_store.py
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 | |
list_groups()
¶
List every conversation group, name order (case-folded).
Returns:
| Type | Description |
|---|---|
list[ConversationGroup]
|
All groups, including empty ones — an empty folder is still a |
list[ConversationGroup]
|
drop target in the sidebar. |
Source code in varagity/stores/conversation_store.py
434 435 436 437 438 439 440 441 442 | |
recent_turns(conversation_id, limit)
¶
Fetch a conversation's newest turns, re-ordered oldest first.
The chat engine's history feed (spec_v3 §4.4) — deliberately not
:meth:get_conversation, which hydrates every message's snapshotted
sources and traces: evidence the condenser must not see, on the
pre-first-token hot path, growing with conversation length. Here the
bound is applied in SQL (newest limit rows) and only
role/content are read. Primitive pairs keep
varagity/stores/ from importing varagity/chat/ — callers map
them to their own turn type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conversation_id
|
str
|
The conversation to read (an unknown id yields the empty list — existence is the caller's check). |
required |
limit
|
int
|
Maximum turns returned; non-positive yields the empty list without touching the database. |
required |
Returns:
| Type | Description |
|---|---|
list[tuple[str, str]]
|
Up to |
Source code in varagity/stores/conversation_store.py
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 | |
set_conversation_group(conversation_id, group_id)
¶
File a conversation under a group, or ungroup it.
Deliberately no updated_at bump: moving a conversation must not
re-order the recency-sorted sidebar list.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conversation_id
|
str
|
The conversation to move (unknown id is a no-op). |
required |
group_id
|
str | None
|
Target group id, or |
required |
Returns:
| Type | Description |
|---|---|
int
|
The number of |
Raises:
| Type | Description |
|---|---|
ForeignKeyViolation
|
If |
Source code in varagity/stores/conversation_store.py
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 | |
ConversationSummary
¶
Bases: BaseModel
One conversation as listed in the sidebar (spec_v2 §4.2).
Attributes:
| Name | Type | Description |
|---|---|---|
conversation_id |
str
|
The app-generated id. |
title |
str
|
Current title (auto-generated or the creation default). |
created_at |
datetime
|
Creation timestamp. |
updated_at |
datetime
|
Last-turn timestamp (list ordering key). |
message_count |
int
|
Number of persisted messages (user + assistant). |
group_id |
str | None
|
Sidebar group the conversation is filed under ( |
Source code in varagity/stores/conversation_store.py
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | |
MessageRecord
¶
Bases: BaseModel
One persisted message, with the assistant turn's provenance.
Attributes:
| Name | Type | Description |
|---|---|---|
message_id |
str
|
The app-generated id. |
role |
str
|
|
content |
str
|
The question or the generated answer. |
created_at |
datetime
|
Persistence timestamp. |
retrieval_method |
str | None
|
Retrieval method that produced an assistant turn
( |
latency_ms |
dict[str, Any] | None
|
Per-stage timings of an assistant turn ( |
reasoning |
str | None
|
Captured |
condensed_query |
str | None
|
The standalone search query that drove an
assistant turn's retrieval (spec_v3 §8; |
chat_engine |
str | None
|
Registry name of the chat engine that produced an
assistant turn ( |
target_corpus |
str | None
|
Which corpus the turn asked for ( |
graph_evidence |
dict[str, Any] | None
|
The graph retrieval's entities/relations and the
engine query mode, or |
sources |
list[StoredSource]
|
The turn's snapshotted evidence, rank order. Chunk turns
snapshot retrieved chunks; graph turns snapshot the cited
transcript days under |
Source code in varagity/stores/conversation_store.py
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | |
StoredSource
¶
Bases: BaseModel
One snapshotted evidence row of an assistant turn (spec_v2 §9.1).
Attributes:
| Name | Type | Description |
|---|---|---|
rank |
int
|
Final rank in the answer's evidence (1-based). |
chunk_id |
str
|
Soft reference to the chunk that produced the snapshot —
or, on a graph turn, to the cited transcript day's |
trace |
dict[str, Any]
|
The persisted snapshot: |
Source code in varagity/stores/conversation_store.py
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | |
varagity.stores.app_settings_store
¶
AppSettingsStore — persisted runtime setting overrides (spec_v2 §4.7).
A thin psycopg wrapper over the app_settings table (migration 002):
one row per overridden :class:~varagity.config.Settings field, keyed by
the field name with the value in its env-string form — exactly what
pydantic-settings parses from the environment, so the override layer
(:mod:varagity.api.runtime_settings) replays rows as env vars verbatim.
Keys beginning with _ are reserved for app metadata and never surface
as overrides. Two are in use, both "the stores no longer match what would
be produced today" flags behind a persistent GUI affordance:
:data:CORPUS_STALE_KEY (an ingest-affecting setting changed since the
last reingest) and :data:GRAPH_STALE_KEY (a graph source file was
deleted, so the graph still holds messages whose source is gone —
stage-2 decision #16). Each is cleared only by a completed API-driven
rebuild of its own corpus.
CORPUS_STALE_KEY = '_corpus_stale'
module-attribute
¶
Reserved metadata key flagging the corpus stale (needs --reingest).
GRAPH_STALE_KEY = '_graph_stale'
module-attribute
¶
Reserved metadata key flagging the graph stale (needs a rebuild).
AppSettingsStore
¶
Bases: ClosingContextMixin
Persisted key/value overrides over PostgreSQL.
Owns one autocommit connection (every operation is a single statement).
Use as a context manager or call :meth:close when done.
Source code in varagity/stores/app_settings_store.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
__init__(conninfo=None)
¶
Connect to the settings database.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conninfo
|
str | None
|
libpq connection string; defaults to the
|
None
|
Raises:
| Type | Description |
|---|---|
OperationalError
|
If the database is unreachable. |
Source code in varagity/stores/app_settings_store.py
48 49 50 51 52 53 54 55 56 57 58 | |
close()
¶
Close the underlying connection (idempotent).
Source code in varagity/stores/app_settings_store.py
60 61 62 63 | |
delete_override(name)
¶
Remove one override row (reverting the setting to its env value).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The Settings field name (deleting an absent row is a no-op). |
required |
Source code in varagity/stores/app_settings_store.py
87 88 89 90 91 92 93 94 | |
is_corpus_stale()
¶
Read the corpus-stale metadata flag.
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
last reingest (set by |
bool
|
completed |
Source code in varagity/stores/app_settings_store.py
96 97 98 99 100 101 102 103 104 105 106 107 | |
is_graph_stale()
¶
Read the graph-stale metadata flag.
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
set by deleting a graph source file (the graph keeps that |
bool
|
file's messages until it is rebuilt), cleared only by a |
bool
|
completed |
Source code in varagity/stores/app_settings_store.py
119 120 121 122 123 124 125 126 127 128 129 130 131 | |
load_overrides()
¶
Read every persisted setting override.
Returns:
| Type | Description |
|---|---|
dict[str, str]
|
Setting name → env-string value, excluding reserved ( |
dict[str, str]
|
prefixed) metadata rows. |
Source code in varagity/stores/app_settings_store.py
65 66 67 68 69 70 71 72 73 74 75 | |
set_corpus_stale(stale)
¶
Write the corpus-stale metadata flag.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stale
|
bool
|
The new flag value (stored as |
required |
Source code in varagity/stores/app_settings_store.py
109 110 111 112 113 114 115 116 117 | |
set_graph_stale(stale)
¶
Write the graph-stale metadata flag.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stale
|
bool
|
The new flag value (stored as |
required |
Source code in varagity/stores/app_settings_store.py
133 134 135 136 137 138 139 140 141 | |
set_override(name, value)
¶
Insert or update one override row.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The Settings field name (e.g. |
required |
value
|
str
|
The value in env-string form (e.g. |
required |
Source code in varagity/stores/app_settings_store.py
77 78 79 80 81 82 83 84 85 | |
varagity.stores.migrate
¶
Idempotent ordered-SQL migration runner (spec_v2 §9.3).
schema.sql runs only on the postgres container's first boot
(docker-entrypoint-initdb.d), so schema added after a volume exists
never reaches it. This runner reconciles: ordered migrations/NNN_*.sql
files — each written IF NOT EXISTS-safe — are applied in a transaction
apiece and tracked by filename in a schema_migrations table. Running it
repeatedly is a no-op; running it against a fresh volume and a v1 volume
converges both to the same schema. It is invoked from the API's startup
lifespan (varagity.api.main).
Alembic is the noted heavier alternative if migrations ever get non-trivial (plan decision #8; recorded in the runbook and ADR-005).
MIGRATIONS_PATH = Path(__file__).parent / 'migrations'
module-attribute
¶
Directory holding the ordered NNN_*.sql migration files.
run_migrations(conn, migrations_path=None)
¶
Apply every unapplied migration file, in filename order.
Each unapplied file runs in its own transaction together with its
schema_migrations bookkeeping row, so a failing migration rolls
back atomically and a re-run re-attempts it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conn
|
Connection
|
An open (autocommit) connection to the target database. |
required |
migrations_path
|
Path | None
|
Directory of |
None
|
Returns:
| Type | Description |
|---|---|
list[str]
|
The names of the migrations applied by this call, in order |
list[str]
|
(empty when the database was already current). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the directory contains a |
Error
|
If a migration's SQL fails (the transaction for that file is rolled back). |
Source code in varagity/stores/migrate.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | |