biblicus 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- biblicus/__init__.py +28 -0
- biblicus/__main__.py +8 -0
- biblicus/backends/__init__.py +44 -0
- biblicus/backends/base.py +65 -0
- biblicus/backends/scan.py +292 -0
- biblicus/backends/sqlite_full_text_search.py +427 -0
- biblicus/cli.py +468 -0
- biblicus/constants.py +10 -0
- biblicus/corpus.py +952 -0
- biblicus/evaluation.py +261 -0
- biblicus/frontmatter.py +92 -0
- biblicus/models.py +307 -0
- biblicus/retrieval.py +137 -0
- biblicus/sources.py +132 -0
- biblicus/time.py +18 -0
- biblicus/uris.py +64 -0
- biblicus-0.1.1.dist-info/METADATA +174 -0
- biblicus-0.1.1.dist-info/RECORD +22 -0
- biblicus-0.1.1.dist-info/WHEEL +5 -0
- biblicus-0.1.1.dist-info/entry_points.txt +2 -0
- biblicus-0.1.1.dist-info/licenses/LICENSE +21 -0
- biblicus-0.1.1.dist-info/top_level.txt +1 -0
biblicus/evaluation.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Evaluation utilities for Biblicus retrieval runs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
13
|
+
|
|
14
|
+
from .constants import DATASET_SCHEMA_VERSION
|
|
15
|
+
from .backends import get_backend
|
|
16
|
+
from .corpus import Corpus
|
|
17
|
+
from .models import QueryBudget, RetrievalResult, RetrievalRun
|
|
18
|
+
from .time import utc_now_iso
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EvaluationQuery(BaseModel):
|
|
22
|
+
"""
|
|
23
|
+
Query record for retrieval evaluation.
|
|
24
|
+
|
|
25
|
+
:ivar query_id: Unique identifier for the query.
|
|
26
|
+
:vartype query_id: str
|
|
27
|
+
:ivar query_text: Natural language query to execute.
|
|
28
|
+
:vartype query_text: str
|
|
29
|
+
:ivar expected_item_id: Optional expected item identifier.
|
|
30
|
+
:vartype expected_item_id: str or None
|
|
31
|
+
:ivar expected_source_uri: Optional expected source uniform resource identifier.
|
|
32
|
+
:vartype expected_source_uri: str or None
|
|
33
|
+
:ivar kind: Query kind (gold or synthetic).
|
|
34
|
+
:vartype kind: str
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(extra="forbid")
|
|
38
|
+
|
|
39
|
+
query_id: str
|
|
40
|
+
query_text: str
|
|
41
|
+
expected_item_id: Optional[str] = None
|
|
42
|
+
expected_source_uri: Optional[str] = None
|
|
43
|
+
kind: str = Field(default="gold")
|
|
44
|
+
|
|
45
|
+
@model_validator(mode="after")
|
|
46
|
+
def _require_expectation(self) -> "EvaluationQuery":
|
|
47
|
+
if not self.expected_item_id and not self.expected_source_uri:
|
|
48
|
+
raise ValueError("Evaluation queries must include expected_item_id or expected_source_uri")
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class EvaluationDataset(BaseModel):
|
|
53
|
+
"""
|
|
54
|
+
Dataset for retrieval evaluation.
|
|
55
|
+
|
|
56
|
+
:ivar schema_version: Dataset schema version.
|
|
57
|
+
:vartype schema_version: int
|
|
58
|
+
:ivar name: Dataset name.
|
|
59
|
+
:vartype name: str
|
|
60
|
+
:ivar description: Optional description.
|
|
61
|
+
:vartype description: str or None
|
|
62
|
+
:ivar queries: List of evaluation queries.
|
|
63
|
+
:vartype queries: list[EvaluationQuery]
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
model_config = ConfigDict(extra="forbid")
|
|
67
|
+
|
|
68
|
+
schema_version: int = Field(ge=1)
|
|
69
|
+
name: str
|
|
70
|
+
description: Optional[str] = None
|
|
71
|
+
queries: List[EvaluationQuery] = Field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
@model_validator(mode="after")
|
|
74
|
+
def _enforce_schema_version(self) -> "EvaluationDataset":
|
|
75
|
+
if self.schema_version != DATASET_SCHEMA_VERSION:
|
|
76
|
+
raise ValueError(f"Unsupported dataset schema version: {self.schema_version}")
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class EvaluationResult(BaseModel):
|
|
81
|
+
"""
|
|
82
|
+
Result bundle for a retrieval evaluation.
|
|
83
|
+
|
|
84
|
+
:ivar dataset: Dataset metadata.
|
|
85
|
+
:vartype dataset: dict[str, object]
|
|
86
|
+
:ivar backend_id: Backend identifier.
|
|
87
|
+
:vartype backend_id: str
|
|
88
|
+
:ivar run_id: Retrieval run identifier.
|
|
89
|
+
:vartype run_id: str
|
|
90
|
+
:ivar evaluated_at: International Organization for Standardization 8601 evaluation timestamp.
|
|
91
|
+
:vartype evaluated_at: str
|
|
92
|
+
:ivar metrics: Quality metrics for retrieval.
|
|
93
|
+
:vartype metrics: dict[str, float]
|
|
94
|
+
:ivar system: System metrics for retrieval.
|
|
95
|
+
:vartype system: dict[str, float]
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
model_config = ConfigDict(extra="forbid")
|
|
99
|
+
|
|
100
|
+
dataset: Dict[str, object]
|
|
101
|
+
backend_id: str
|
|
102
|
+
run_id: str
|
|
103
|
+
evaluated_at: str
|
|
104
|
+
metrics: Dict[str, float]
|
|
105
|
+
system: Dict[str, float]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def load_dataset(path: Path) -> EvaluationDataset:
|
|
109
|
+
"""
|
|
110
|
+
Load an evaluation dataset from JavaScript Object Notation.
|
|
111
|
+
|
|
112
|
+
:param path: Path to the dataset JavaScript Object Notation file.
|
|
113
|
+
:type path: Path
|
|
114
|
+
:return: Parsed evaluation dataset.
|
|
115
|
+
:rtype: EvaluationDataset
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
119
|
+
return EvaluationDataset.model_validate(data)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def evaluate_run(
|
|
123
|
+
*,
|
|
124
|
+
corpus: Corpus,
|
|
125
|
+
run: RetrievalRun,
|
|
126
|
+
dataset: EvaluationDataset,
|
|
127
|
+
budget: QueryBudget,
|
|
128
|
+
) -> EvaluationResult:
|
|
129
|
+
"""
|
|
130
|
+
Evaluate a retrieval run against a dataset.
|
|
131
|
+
|
|
132
|
+
:param corpus: Corpus associated with the run.
|
|
133
|
+
:type corpus: Corpus
|
|
134
|
+
:param run: Retrieval run manifest.
|
|
135
|
+
:type run: RetrievalRun
|
|
136
|
+
:param dataset: Evaluation dataset.
|
|
137
|
+
:type dataset: EvaluationDataset
|
|
138
|
+
:param budget: Evidence selection budget.
|
|
139
|
+
:type budget: QueryBudget
|
|
140
|
+
:return: Evaluation result bundle.
|
|
141
|
+
:rtype: EvaluationResult
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
backend = get_backend(run.recipe.backend_id)
|
|
145
|
+
latency_seconds: List[float] = []
|
|
146
|
+
hit_count = 0
|
|
147
|
+
reciprocal_ranks: List[float] = []
|
|
148
|
+
|
|
149
|
+
for query in dataset.queries:
|
|
150
|
+
timer_start = time.perf_counter()
|
|
151
|
+
result = backend.query(corpus, run=run, query_text=query.query_text, budget=budget)
|
|
152
|
+
elapsed_seconds = time.perf_counter() - timer_start
|
|
153
|
+
latency_seconds.append(elapsed_seconds)
|
|
154
|
+
expected_rank = _expected_rank(result, query)
|
|
155
|
+
if expected_rank is not None:
|
|
156
|
+
hit_count += 1
|
|
157
|
+
reciprocal_ranks.append(1.0 / expected_rank)
|
|
158
|
+
else:
|
|
159
|
+
reciprocal_ranks.append(0.0)
|
|
160
|
+
|
|
161
|
+
total_queries = max(len(dataset.queries), 1)
|
|
162
|
+
max_total_items = float(budget.max_total_items)
|
|
163
|
+
hit_rate = hit_count / total_queries
|
|
164
|
+
precision_at_max_total_items = hit_count / (total_queries * max_total_items)
|
|
165
|
+
mean_reciprocal_rank = sum(reciprocal_ranks) / total_queries
|
|
166
|
+
|
|
167
|
+
metrics = {
|
|
168
|
+
"hit_rate": hit_rate,
|
|
169
|
+
"precision_at_max_total_items": precision_at_max_total_items,
|
|
170
|
+
"mean_reciprocal_rank": mean_reciprocal_rank,
|
|
171
|
+
}
|
|
172
|
+
system = {
|
|
173
|
+
"average_latency_milliseconds": _average_latency_milliseconds(latency_seconds),
|
|
174
|
+
"percentile_95_latency_milliseconds": _percentile_95_latency_milliseconds(latency_seconds),
|
|
175
|
+
"index_bytes": float(_run_artifact_bytes(corpus, run)),
|
|
176
|
+
}
|
|
177
|
+
dataset_meta = {
|
|
178
|
+
"name": dataset.name,
|
|
179
|
+
"description": dataset.description,
|
|
180
|
+
"queries": len(dataset.queries),
|
|
181
|
+
}
|
|
182
|
+
return EvaluationResult(
|
|
183
|
+
dataset=dataset_meta,
|
|
184
|
+
backend_id=run.recipe.backend_id,
|
|
185
|
+
run_id=run.run_id,
|
|
186
|
+
evaluated_at=utc_now_iso(),
|
|
187
|
+
metrics=metrics,
|
|
188
|
+
system=system,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _expected_rank(result: RetrievalResult, query: EvaluationQuery) -> Optional[int]:
|
|
193
|
+
"""
|
|
194
|
+
Locate the first evidence rank that matches the expected item or source.
|
|
195
|
+
|
|
196
|
+
:param result: Retrieval result for a query.
|
|
197
|
+
:type result: RetrievalResult
|
|
198
|
+
:param query: Evaluation query definition.
|
|
199
|
+
:type query: EvaluationQuery
|
|
200
|
+
:return: Rank of the first matching evidence item, or None.
|
|
201
|
+
:rtype: int or None
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
for evidence in result.evidence:
|
|
205
|
+
if query.expected_item_id and evidence.item_id == query.expected_item_id:
|
|
206
|
+
return evidence.rank
|
|
207
|
+
if query.expected_source_uri and evidence.source_uri == query.expected_source_uri:
|
|
208
|
+
return evidence.rank
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _average_latency_milliseconds(latencies: List[float]) -> float:
|
|
213
|
+
"""
|
|
214
|
+
Compute average latency in milliseconds.
|
|
215
|
+
|
|
216
|
+
:param latencies: Latency samples in seconds.
|
|
217
|
+
:type latencies: list[float]
|
|
218
|
+
:return: Average latency in milliseconds.
|
|
219
|
+
:rtype: float
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
if not latencies:
|
|
223
|
+
return 0.0
|
|
224
|
+
return sum(latencies) / len(latencies) * 1000.0
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _percentile_95_latency_milliseconds(latencies: List[float]) -> float:
|
|
228
|
+
"""
|
|
229
|
+
Compute the percentile 95 latency in milliseconds.
|
|
230
|
+
|
|
231
|
+
:param latencies: Latency samples in seconds.
|
|
232
|
+
:type latencies: list[float]
|
|
233
|
+
:return: Percentile 95 latency in milliseconds.
|
|
234
|
+
:rtype: float
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
if not latencies:
|
|
238
|
+
return 0.0
|
|
239
|
+
sorted_latencies = sorted(latencies)
|
|
240
|
+
percentile_index = int(round(0.95 * (len(sorted_latencies) - 1)))
|
|
241
|
+
return sorted_latencies[percentile_index] * 1000.0
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _run_artifact_bytes(corpus: Corpus, run: RetrievalRun) -> int:
|
|
245
|
+
"""
|
|
246
|
+
Sum artifact sizes for a retrieval run.
|
|
247
|
+
|
|
248
|
+
:param corpus: Corpus that owns the artifacts.
|
|
249
|
+
:type corpus: Corpus
|
|
250
|
+
:param run: Retrieval run manifest.
|
|
251
|
+
:type run: RetrievalRun
|
|
252
|
+
:return: Total artifact bytes.
|
|
253
|
+
:rtype: int
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
total_bytes = 0
|
|
257
|
+
for artifact_relpath in run.artifact_paths:
|
|
258
|
+
artifact_path = corpus.root / artifact_relpath
|
|
259
|
+
if artifact_path.exists():
|
|
260
|
+
total_bytes += artifact_path.stat().st_size
|
|
261
|
+
return total_bytes
|
biblicus/frontmatter.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Markdown front matter helpers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, Dict, Tuple
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class FrontMatterDocument:
|
|
15
|
+
"""
|
|
16
|
+
Parsed front matter and markdown body.
|
|
17
|
+
|
|
18
|
+
:ivar metadata: Front matter metadata mapping.
|
|
19
|
+
:vartype metadata: dict[str, Any]
|
|
20
|
+
:ivar body: Markdown body text.
|
|
21
|
+
:vartype body: str
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
metadata: Dict[str, Any]
|
|
25
|
+
body: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_front_matter(text: str) -> FrontMatterDocument:
|
|
29
|
+
"""
|
|
30
|
+
Parse Yet Another Markup Language front matter from a Markdown document.
|
|
31
|
+
|
|
32
|
+
:param text: Markdown content with optional front matter.
|
|
33
|
+
:type text: str
|
|
34
|
+
:return: Parsed front matter and body.
|
|
35
|
+
:rtype: FrontMatterDocument
|
|
36
|
+
:raises ValueError: If front matter is present but not a mapping.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
if not text.startswith("---\n"):
|
|
40
|
+
return FrontMatterDocument(metadata={}, body=text)
|
|
41
|
+
|
|
42
|
+
front_matter_end = text.find("\n---\n", 4)
|
|
43
|
+
if front_matter_end == -1:
|
|
44
|
+
return FrontMatterDocument(metadata={}, body=text)
|
|
45
|
+
|
|
46
|
+
raw_yaml = text[4:front_matter_end]
|
|
47
|
+
body = text[front_matter_end + len("\n---\n") :]
|
|
48
|
+
|
|
49
|
+
metadata = yaml.safe_load(raw_yaml) or {}
|
|
50
|
+
if not isinstance(metadata, dict):
|
|
51
|
+
raise ValueError("Yet Another Markup Language front matter must be a mapping object")
|
|
52
|
+
|
|
53
|
+
return FrontMatterDocument(metadata=dict(metadata), body=body)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def render_front_matter(metadata: Dict[str, Any], body: str) -> str:
|
|
57
|
+
"""
|
|
58
|
+
Render Yet Another Markup Language front matter with a Markdown body.
|
|
59
|
+
|
|
60
|
+
:param metadata: Front matter metadata mapping.
|
|
61
|
+
:type metadata: dict[str, Any]
|
|
62
|
+
:param body: Markdown body text.
|
|
63
|
+
:type body: str
|
|
64
|
+
:return: Markdown with Yet Another Markup Language front matter.
|
|
65
|
+
:rtype: str
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
if not metadata:
|
|
69
|
+
return body
|
|
70
|
+
|
|
71
|
+
yaml_text = yaml.safe_dump(
|
|
72
|
+
metadata,
|
|
73
|
+
sort_keys=False,
|
|
74
|
+
allow_unicode=True,
|
|
75
|
+
default_flow_style=False,
|
|
76
|
+
).strip()
|
|
77
|
+
|
|
78
|
+
return f"---\n{yaml_text}\n---\n{body}"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def split_markdown_front_matter(path_text: str) -> Tuple[Dict[str, Any], str]:
|
|
82
|
+
"""
|
|
83
|
+
Split Markdown into front matter metadata and body.
|
|
84
|
+
|
|
85
|
+
:param path_text: Markdown content.
|
|
86
|
+
:type path_text: str
|
|
87
|
+
:return: Metadata mapping and body text.
|
|
88
|
+
:rtype: tuple[dict[str, Any], str]
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
parsed_document = parse_front_matter(path_text)
|
|
92
|
+
return parsed_document.metadata, parsed_document.body
|
biblicus/models.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic models for Biblicus domain concepts.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
10
|
+
|
|
11
|
+
from .constants import SCHEMA_VERSION
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CorpusConfig(BaseModel):
|
|
15
|
+
"""
|
|
16
|
+
Canonical on-disk config for a local Biblicus corpus.
|
|
17
|
+
|
|
18
|
+
:ivar schema_version: Version of the corpus config schema.
|
|
19
|
+
:vartype schema_version: int
|
|
20
|
+
:ivar created_at: International Organization for Standardization 8601 timestamp for corpus creation.
|
|
21
|
+
:vartype created_at: str
|
|
22
|
+
:ivar corpus_uri: Canonical uniform resource identifier for the corpus root.
|
|
23
|
+
:vartype corpus_uri: str
|
|
24
|
+
:ivar raw_dir: Relative path to the raw items folder.
|
|
25
|
+
:vartype raw_dir: str
|
|
26
|
+
:ivar notes: Optional free-form notes for operators.
|
|
27
|
+
:vartype notes: dict[str, Any] or None
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
model_config = ConfigDict(extra="forbid")
|
|
31
|
+
|
|
32
|
+
schema_version: int = Field(ge=1)
|
|
33
|
+
created_at: str
|
|
34
|
+
corpus_uri: str
|
|
35
|
+
raw_dir: str = "raw"
|
|
36
|
+
notes: Optional[Dict[str, Any]] = None
|
|
37
|
+
|
|
38
|
+
@model_validator(mode="after")
|
|
39
|
+
def _enforce_schema_version(self) -> "CorpusConfig":
|
|
40
|
+
if self.schema_version != SCHEMA_VERSION:
|
|
41
|
+
raise ValueError(f"Unsupported corpus config schema version: {self.schema_version}")
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class IngestResult(BaseModel):
|
|
46
|
+
"""
|
|
47
|
+
Minimal summary for an ingestion event.
|
|
48
|
+
|
|
49
|
+
:ivar item_id: Universally unique identifier assigned to the ingested item.
|
|
50
|
+
:vartype item_id: str
|
|
51
|
+
:ivar relpath: Relative path to the raw item file.
|
|
52
|
+
:vartype relpath: str
|
|
53
|
+
:ivar sha256: Secure Hash Algorithm 256 digest of the stored bytes.
|
|
54
|
+
:vartype sha256: str
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
model_config = ConfigDict(extra="forbid")
|
|
58
|
+
|
|
59
|
+
item_id: str
|
|
60
|
+
relpath: str
|
|
61
|
+
sha256: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CatalogItem(BaseModel):
|
|
65
|
+
"""
|
|
66
|
+
Catalog entry derived from a raw corpus item.
|
|
67
|
+
|
|
68
|
+
:ivar id: Universally unique identifier of the item.
|
|
69
|
+
:vartype id: str
|
|
70
|
+
:ivar relpath: Relative path to the raw item file.
|
|
71
|
+
:vartype relpath: str
|
|
72
|
+
:ivar sha256: Secure Hash Algorithm 256 digest of the stored bytes.
|
|
73
|
+
:vartype sha256: str
|
|
74
|
+
:ivar bytes: Size of the raw item in bytes.
|
|
75
|
+
:vartype bytes: int
|
|
76
|
+
:ivar media_type: Internet Assigned Numbers Authority media type for the item.
|
|
77
|
+
:vartype media_type: str
|
|
78
|
+
:ivar title: Optional human title extracted from metadata.
|
|
79
|
+
:vartype title: str or None
|
|
80
|
+
:ivar tags: Tags extracted or supplied for the item.
|
|
81
|
+
:vartype tags: list[str]
|
|
82
|
+
:ivar metadata: Merged front matter or sidecar metadata.
|
|
83
|
+
:vartype metadata: dict[str, Any]
|
|
84
|
+
:ivar created_at: International Organization for Standardization 8601 timestamp when the item was first indexed.
|
|
85
|
+
:vartype created_at: str
|
|
86
|
+
:ivar source_uri: Optional source uniform resource identifier used at ingestion time.
|
|
87
|
+
:vartype source_uri: str or None
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
model_config = ConfigDict(extra="forbid")
|
|
91
|
+
|
|
92
|
+
id: str
|
|
93
|
+
relpath: str
|
|
94
|
+
sha256: str
|
|
95
|
+
bytes: int = Field(ge=0)
|
|
96
|
+
media_type: str
|
|
97
|
+
title: Optional[str] = None
|
|
98
|
+
tags: List[str] = Field(default_factory=list)
|
|
99
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
100
|
+
created_at: str
|
|
101
|
+
source_uri: Optional[str] = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class CorpusCatalog(BaseModel):
|
|
105
|
+
"""
|
|
106
|
+
Snapshot of the derived corpus catalog.
|
|
107
|
+
|
|
108
|
+
:ivar schema_version: Version of the catalog schema.
|
|
109
|
+
:vartype schema_version: int
|
|
110
|
+
:ivar generated_at: International Organization for Standardization 8601 timestamp of catalog generation.
|
|
111
|
+
:vartype generated_at: str
|
|
112
|
+
:ivar corpus_uri: Canonical uniform resource identifier for the corpus root.
|
|
113
|
+
:vartype corpus_uri: str
|
|
114
|
+
:ivar raw_dir: Relative path to the raw items folder.
|
|
115
|
+
:vartype raw_dir: str
|
|
116
|
+
:ivar latest_run_id: Latest retrieval run identifier, if any.
|
|
117
|
+
:vartype latest_run_id: str or None
|
|
118
|
+
:ivar items: Mapping of item IDs to catalog entries.
|
|
119
|
+
:vartype items: dict[str, CatalogItem]
|
|
120
|
+
:ivar order: Display order of item IDs (most recent first).
|
|
121
|
+
:vartype order: list[str]
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
model_config = ConfigDict(extra="forbid")
|
|
125
|
+
|
|
126
|
+
schema_version: int = Field(ge=1)
|
|
127
|
+
generated_at: str
|
|
128
|
+
corpus_uri: str
|
|
129
|
+
raw_dir: str = "raw"
|
|
130
|
+
latest_run_id: Optional[str] = None
|
|
131
|
+
items: Dict[str, CatalogItem] = Field(default_factory=dict)
|
|
132
|
+
order: List[str] = Field(default_factory=list)
|
|
133
|
+
|
|
134
|
+
@model_validator(mode="after")
|
|
135
|
+
def _enforce_schema_version(self) -> "CorpusCatalog":
|
|
136
|
+
if self.schema_version != SCHEMA_VERSION:
|
|
137
|
+
raise ValueError(f"Unsupported catalog schema version: {self.schema_version}")
|
|
138
|
+
return self
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class QueryBudget(BaseModel):
|
|
142
|
+
"""
|
|
143
|
+
Evidence selection budget for retrieval.
|
|
144
|
+
|
|
145
|
+
:ivar max_total_items: Maximum number of evidence items to return.
|
|
146
|
+
:vartype max_total_items: int
|
|
147
|
+
:ivar max_total_characters: Optional maximum total characters across evidence text.
|
|
148
|
+
:vartype max_total_characters: int or None
|
|
149
|
+
:ivar max_items_per_source: Optional cap per source uniform resource identifier.
|
|
150
|
+
:vartype max_items_per_source: int or None
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
model_config = ConfigDict(extra="forbid")
|
|
154
|
+
|
|
155
|
+
max_total_items: int = Field(ge=1)
|
|
156
|
+
max_total_characters: Optional[int] = Field(default=None, ge=1)
|
|
157
|
+
max_items_per_source: Optional[int] = Field(default=None, ge=1)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class Evidence(BaseModel):
|
|
161
|
+
"""
|
|
162
|
+
Structured retrieval evidence returned from a backend.
|
|
163
|
+
|
|
164
|
+
:ivar item_id: Item identifier that produced the evidence.
|
|
165
|
+
:vartype item_id: str
|
|
166
|
+
:ivar source_uri: Source uniform resource identifier from ingestion metadata.
|
|
167
|
+
:vartype source_uri: str or None
|
|
168
|
+
:ivar media_type: Media type for the evidence item.
|
|
169
|
+
:vartype media_type: str
|
|
170
|
+
:ivar score: Retrieval score (higher is better).
|
|
171
|
+
:vartype score: float
|
|
172
|
+
:ivar rank: Rank within the final evidence list (1-based).
|
|
173
|
+
:vartype rank: int
|
|
174
|
+
:ivar text: Optional text payload for the evidence.
|
|
175
|
+
:vartype text: str or None
|
|
176
|
+
:ivar content_ref: Optional reference for non-text content.
|
|
177
|
+
:vartype content_ref: str or None
|
|
178
|
+
:ivar span_start: Optional start offset in the source text.
|
|
179
|
+
:vartype span_start: int or None
|
|
180
|
+
:ivar span_end: Optional end offset in the source text.
|
|
181
|
+
:vartype span_end: int or None
|
|
182
|
+
:ivar stage: Retrieval stage label (for example, scan, full-text search, rerank).
|
|
183
|
+
:vartype stage: str
|
|
184
|
+
:ivar recipe_id: Recipe identifier used to create the run.
|
|
185
|
+
:vartype recipe_id: str
|
|
186
|
+
:ivar run_id: Retrieval run identifier.
|
|
187
|
+
:vartype run_id: str
|
|
188
|
+
:ivar hash: Optional content hash for provenance.
|
|
189
|
+
:vartype hash: str or None
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
model_config = ConfigDict(extra="forbid")
|
|
193
|
+
|
|
194
|
+
item_id: str
|
|
195
|
+
source_uri: Optional[str] = None
|
|
196
|
+
media_type: str
|
|
197
|
+
score: float
|
|
198
|
+
rank: int = Field(ge=1)
|
|
199
|
+
text: Optional[str] = None
|
|
200
|
+
content_ref: Optional[str] = None
|
|
201
|
+
span_start: Optional[int] = None
|
|
202
|
+
span_end: Optional[int] = None
|
|
203
|
+
stage: str
|
|
204
|
+
recipe_id: str
|
|
205
|
+
run_id: str
|
|
206
|
+
hash: Optional[str] = None
|
|
207
|
+
|
|
208
|
+
@model_validator(mode="after")
|
|
209
|
+
def _require_text_or_reference(self) -> "Evidence":
|
|
210
|
+
has_text = isinstance(self.text, str) and self.text.strip()
|
|
211
|
+
has_ref = isinstance(self.content_ref, str) and self.content_ref.strip()
|
|
212
|
+
if not has_text and not has_ref:
|
|
213
|
+
raise ValueError("Evidence must include either text or content_ref")
|
|
214
|
+
return self
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class RecipeManifest(BaseModel):
|
|
218
|
+
"""
|
|
219
|
+
Reproducible configuration for a retrieval backend.
|
|
220
|
+
|
|
221
|
+
:ivar recipe_id: Deterministic recipe identifier.
|
|
222
|
+
:vartype recipe_id: str
|
|
223
|
+
:ivar backend_id: Backend identifier for the recipe.
|
|
224
|
+
:vartype backend_id: str
|
|
225
|
+
:ivar name: Human-readable name for the recipe.
|
|
226
|
+
:vartype name: str
|
|
227
|
+
:ivar created_at: International Organization for Standardization 8601 timestamp for recipe creation.
|
|
228
|
+
:vartype created_at: str
|
|
229
|
+
:ivar config: Backend-specific configuration values.
|
|
230
|
+
:vartype config: dict[str, Any]
|
|
231
|
+
:ivar description: Optional human description.
|
|
232
|
+
:vartype description: str or None
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
model_config = ConfigDict(extra="forbid")
|
|
236
|
+
|
|
237
|
+
recipe_id: str
|
|
238
|
+
backend_id: str
|
|
239
|
+
name: str
|
|
240
|
+
created_at: str
|
|
241
|
+
config: Dict[str, Any] = Field(default_factory=dict)
|
|
242
|
+
description: Optional[str] = None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class RetrievalRun(BaseModel):
|
|
246
|
+
"""
|
|
247
|
+
Immutable record of a retrieval materialization or on-demand run.
|
|
248
|
+
|
|
249
|
+
:ivar run_id: Unique run identifier.
|
|
250
|
+
:vartype run_id: str
|
|
251
|
+
:ivar recipe: Recipe manifest for this run.
|
|
252
|
+
:vartype recipe: RecipeManifest
|
|
253
|
+
:ivar corpus_uri: Canonical uniform resource identifier for the corpus root.
|
|
254
|
+
:vartype corpus_uri: str
|
|
255
|
+
:ivar catalog_generated_at: Catalog timestamp used for the run.
|
|
256
|
+
:vartype catalog_generated_at: str
|
|
257
|
+
:ivar created_at: International Organization for Standardization 8601 timestamp for run creation.
|
|
258
|
+
:vartype created_at: str
|
|
259
|
+
:ivar artifact_paths: Relative paths to materialized artifacts.
|
|
260
|
+
:vartype artifact_paths: list[str]
|
|
261
|
+
:ivar stats: Backend-specific run statistics.
|
|
262
|
+
:vartype stats: dict[str, Any]
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
model_config = ConfigDict(extra="forbid")
|
|
266
|
+
|
|
267
|
+
run_id: str
|
|
268
|
+
recipe: RecipeManifest
|
|
269
|
+
corpus_uri: str
|
|
270
|
+
catalog_generated_at: str
|
|
271
|
+
created_at: str
|
|
272
|
+
artifact_paths: List[str] = Field(default_factory=list)
|
|
273
|
+
stats: Dict[str, Any] = Field(default_factory=dict)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class RetrievalResult(BaseModel):
|
|
277
|
+
"""
|
|
278
|
+
Retrieval result bundle returned from a backend query.
|
|
279
|
+
|
|
280
|
+
:ivar query_text: Query text issued against the backend.
|
|
281
|
+
:vartype query_text: str
|
|
282
|
+
:ivar budget: Evidence selection budget applied to results.
|
|
283
|
+
:vartype budget: QueryBudget
|
|
284
|
+
:ivar run_id: Retrieval run identifier.
|
|
285
|
+
:vartype run_id: str
|
|
286
|
+
:ivar recipe_id: Recipe identifier used for this query.
|
|
287
|
+
:vartype recipe_id: str
|
|
288
|
+
:ivar backend_id: Backend identifier used for this query.
|
|
289
|
+
:vartype backend_id: str
|
|
290
|
+
:ivar generated_at: International Organization for Standardization 8601 timestamp for the query result.
|
|
291
|
+
:vartype generated_at: str
|
|
292
|
+
:ivar evidence: Evidence objects selected under the budget.
|
|
293
|
+
:vartype evidence: list[Evidence]
|
|
294
|
+
:ivar stats: Backend-specific query statistics.
|
|
295
|
+
:vartype stats: dict[str, Any]
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
model_config = ConfigDict(extra="forbid")
|
|
299
|
+
|
|
300
|
+
query_text: str
|
|
301
|
+
budget: QueryBudget
|
|
302
|
+
run_id: str
|
|
303
|
+
recipe_id: str
|
|
304
|
+
backend_id: str
|
|
305
|
+
generated_at: str
|
|
306
|
+
evidence: List[Evidence] = Field(default_factory=list)
|
|
307
|
+
stats: Dict[str, Any] = Field(default_factory=dict)
|