ragaudit-cli 0.3.0__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.
Files changed (71) hide show
  1. ragaudit/__init__.py +1 -0
  2. ragaudit/adapters/__init__.py +0 -0
  3. ragaudit/adapters/base.py +109 -0
  4. ragaudit/adapters/callable_adapter.py +15 -0
  5. ragaudit/cli.py +540 -0
  6. ragaudit/cli_easy.py +458 -0
  7. ragaudit/cli_v2.py +507 -0
  8. ragaudit/env.py +46 -0
  9. ragaudit/generate/__init__.py +0 -0
  10. ragaudit/generate/pipeline.py +486 -0
  11. ragaudit/generate/prompts.py +116 -0
  12. ragaudit/generate/testset_generator.py +146 -0
  13. ragaudit/generate/text.py +75 -0
  14. ragaudit/ingest/__init__.py +0 -0
  15. ragaudit/ingest/chunker.py +53 -0
  16. ragaudit/ingest/corpus.py +120 -0
  17. ragaudit/ingest/loader.py +49 -0
  18. ragaudit/ingest/versioning.py +92 -0
  19. ragaudit/llm/__init__.py +0 -0
  20. ragaudit/llm/client.py +86 -0
  21. ragaudit/llm/factory.py +70 -0
  22. ragaudit/llm/json_output.py +72 -0
  23. ragaudit/llm/mock.py +552 -0
  24. ragaudit/models/__init__.py +0 -0
  25. ragaudit/models/adapter.py +39 -0
  26. ragaudit/models/document.py +28 -0
  27. ragaudit/models/evaluation.py +70 -0
  28. ragaudit/models/question.py +152 -0
  29. ragaudit/models/run.py +44 -0
  30. ragaudit/models/scoring.py +82 -0
  31. ragaudit/models/testset.py +30 -0
  32. ragaudit/models/tune.py +56 -0
  33. ragaudit/report/__init__.py +0 -0
  34. ragaudit/report/generator.py +155 -0
  35. ragaudit/report/plain_summary.py +122 -0
  36. ragaudit/report/templates/_v2_tooltip.jinja +43 -0
  37. ragaudit/report/templates/generate_report.html.jinja +175 -0
  38. ragaudit/report/templates/report.html.jinja +252 -0
  39. ragaudit/report/templates/style.css +1095 -0
  40. ragaudit/report/templates/tune_report.html.jinja +158 -0
  41. ragaudit/report/templates/v2.css +286 -0
  42. ragaudit/report/v2.py +380 -0
  43. ragaudit/runner/__init__.py +0 -0
  44. ragaudit/runner/ablation.py +46 -0
  45. ragaudit/runner/evaluate.py +325 -0
  46. ragaudit/runner/executor.py +98 -0
  47. ragaudit/runner/locked.py +37 -0
  48. ragaudit/runner/summary.py +45 -0
  49. ragaudit/scaffold.py +252 -0
  50. ragaudit/scoring/__init__.py +0 -0
  51. ragaudit/scoring/correctness.py +94 -0
  52. ragaudit/scoring/groundedness.py +86 -0
  53. ragaudit/scoring/retrieval.py +42 -0
  54. ragaudit/scoring/text_match.py +51 -0
  55. ragaudit/scoring/verdict.py +14 -0
  56. ragaudit/storage/__init__.py +0 -0
  57. ragaudit/storage/db.py +81 -0
  58. ragaudit/storage/lock.py +36 -0
  59. ragaudit/storage/repository.py +216 -0
  60. ragaudit/storage/store.py +536 -0
  61. ragaudit/tune/__init__.py +7 -0
  62. ragaudit/tune/devset.py +16 -0
  63. ragaudit/tune/loop.py +208 -0
  64. ragaudit/tune/search_space.py +140 -0
  65. ragaudit/tune/strategies.py +197 -0
  66. ragaudit/yaml_io.py +199 -0
  67. ragaudit_cli-0.3.0.dist-info/METADATA +429 -0
  68. ragaudit_cli-0.3.0.dist-info/RECORD +71 -0
  69. ragaudit_cli-0.3.0.dist-info/WHEEL +4 -0
  70. ragaudit_cli-0.3.0.dist-info/entry_points.txt +2 -0
  71. ragaudit_cli-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,216 @@
1
+ """Lossless save/load functions for TestSet and Run.
2
+
3
+ Simple flat fields get their own columns; nested scoring structures
4
+ (ScoredCase, RunSummary) are stored as JSON via Pydantic's own
5
+ model_dump_json/model_validate_json, so round-tripping is exactly as
6
+ lossless as Pydantic's own serialization — no hand-rolled re-encoding
7
+ of nested objects.
8
+ """
9
+
10
+ import json
11
+ import sqlite3
12
+ from datetime import datetime
13
+
14
+ from ragaudit.models.document import Chunk, Document
15
+ from ragaudit.models.run import Run, RunConfig, RunSummary
16
+ from ragaudit.models.scoring import ScoredCase
17
+ from ragaudit.models.testset import TestCase, TestSet
18
+
19
+
20
+ def save_documents(conn: sqlite3.Connection, documents: list[Document]) -> None:
21
+ """Replace the full set of ingested documents with `documents`."""
22
+ conn.execute("DELETE FROM documents")
23
+ for document in documents:
24
+ conn.execute(
25
+ "INSERT INTO documents (doc_id, source_path, content, content_hash, metadata_json) VALUES (?, ?, ?, ?, ?)",
26
+ (document.doc_id, document.source_path, document.content, document.content_hash, json.dumps(document.metadata)),
27
+ )
28
+ conn.commit()
29
+
30
+
31
+ def load_documents(conn: sqlite3.Connection) -> list[Document]:
32
+ """Load every ingested Document."""
33
+ rows = conn.execute("SELECT doc_id, source_path, content, content_hash, metadata_json FROM documents").fetchall()
34
+ return [
35
+ Document(doc_id=r[0], source_path=r[1], content=r[2], content_hash=r[3], metadata=json.loads(r[4]))
36
+ for r in rows
37
+ ]
38
+
39
+
40
+ def save_chunks(conn: sqlite3.Connection, chunks: list[Chunk]) -> None:
41
+ """Replace the full set of ingested chunks with `chunks`."""
42
+ conn.execute("DELETE FROM chunks")
43
+ for chunk in chunks:
44
+ conn.execute(
45
+ "INSERT INTO chunks (chunk_id, doc_id, position, text, token_count, metadata_json) VALUES (?, ?, ?, ?, ?, ?)",
46
+ (chunk.chunk_id, chunk.doc_id, chunk.position, chunk.text, chunk.token_count, json.dumps(chunk.metadata)),
47
+ )
48
+ conn.commit()
49
+
50
+
51
+ def load_chunks(conn: sqlite3.Connection) -> list[Chunk]:
52
+ """Load every ingested Chunk, ordered by document then position."""
53
+ rows = conn.execute(
54
+ "SELECT chunk_id, doc_id, position, text, token_count, metadata_json FROM chunks ORDER BY doc_id, position"
55
+ ).fetchall()
56
+ return [
57
+ Chunk(chunk_id=r[0], doc_id=r[1], position=r[2], text=r[3], token_count=r[4], metadata=json.loads(r[5]))
58
+ for r in rows
59
+ ]
60
+
61
+
62
+ def get_latest_testset_id(conn: sqlite3.Connection) -> str | None:
63
+ """Return the testset_id of the most recently created TestSet, or None if there is none."""
64
+ row = conn.execute("SELECT testset_id FROM testsets ORDER BY created_at DESC LIMIT 1").fetchone()
65
+ return row[0] if row else None
66
+
67
+
68
+ def get_latest_run_id(conn: sqlite3.Connection) -> str | None:
69
+ """Return the run_id of the most recently started Run, or None if there is none."""
70
+ row = conn.execute("SELECT run_id FROM runs ORDER BY started_at DESC LIMIT 1").fetchone()
71
+ return row[0] if row else None
72
+
73
+
74
+ def save_test_set(conn: sqlite3.Connection, test_set: TestSet) -> None:
75
+ """Persist a TestSet and its TestCases, replacing any existing rows with the same testset_id."""
76
+ conn.execute("DELETE FROM test_cases WHERE testset_id = ?", (test_set.testset_id,))
77
+ conn.execute("DELETE FROM testsets WHERE testset_id = ?", (test_set.testset_id,))
78
+
79
+ conn.execute(
80
+ "INSERT INTO testsets (testset_id, corpus_hash, created_at, generation_model) VALUES (?, ?, ?, ?)",
81
+ (test_set.testset_id, test_set.corpus_hash, test_set.created_at.isoformat(), test_set.generation_model),
82
+ )
83
+
84
+ for position, test_case in enumerate(test_set.test_cases):
85
+ conn.execute(
86
+ """
87
+ INSERT INTO test_cases (
88
+ test_id, testset_id, position, question, expected_answer,
89
+ source_chunk_id, source_doc_id, generation_model, created_at, metadata_json
90
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
91
+ """,
92
+ (
93
+ test_case.test_id,
94
+ test_set.testset_id,
95
+ position,
96
+ test_case.question,
97
+ test_case.expected_answer,
98
+ test_case.source_chunk_id,
99
+ test_case.source_doc_id,
100
+ test_case.generation_model,
101
+ test_case.created_at.isoformat(),
102
+ json.dumps(test_case.metadata),
103
+ ),
104
+ )
105
+
106
+ conn.commit()
107
+
108
+
109
+ def load_test_set(conn: sqlite3.Connection, testset_id: str) -> TestSet | None:
110
+ """Load a TestSet by id, with its TestCases in their original order. None if not found."""
111
+ row = conn.execute(
112
+ "SELECT testset_id, corpus_hash, created_at, generation_model FROM testsets WHERE testset_id = ?",
113
+ (testset_id,),
114
+ ).fetchone()
115
+ if row is None:
116
+ return None
117
+
118
+ case_rows = conn.execute(
119
+ """
120
+ SELECT test_id, question, expected_answer, source_chunk_id, source_doc_id,
121
+ generation_model, created_at, metadata_json
122
+ FROM test_cases WHERE testset_id = ? ORDER BY position ASC
123
+ """,
124
+ (testset_id,),
125
+ ).fetchall()
126
+
127
+ test_cases = [
128
+ TestCase(
129
+ test_id=r[0],
130
+ question=r[1],
131
+ expected_answer=r[2],
132
+ source_chunk_id=r[3],
133
+ source_doc_id=r[4],
134
+ generation_model=r[5],
135
+ created_at=datetime.fromisoformat(r[6]),
136
+ metadata=json.loads(r[7]),
137
+ )
138
+ for r in case_rows
139
+ ]
140
+
141
+ return TestSet(
142
+ testset_id=row[0],
143
+ corpus_hash=row[1],
144
+ created_at=datetime.fromisoformat(row[2]),
145
+ generation_model=row[3],
146
+ test_cases=test_cases,
147
+ )
148
+
149
+
150
+ def save_run(conn: sqlite3.Connection, run: Run) -> None:
151
+ """Persist a Run: config, scored cases, and summary. Replaces any existing rows with the same run_id."""
152
+ run_id = run.config.run_id
153
+ conn.execute("DELETE FROM scored_cases WHERE run_id = ?", (run_id,))
154
+ conn.execute("DELETE FROM runs WHERE run_id = ?", (run_id,))
155
+
156
+ conn.execute(
157
+ """
158
+ INSERT INTO runs (
159
+ run_id, testset_id, adapter_path, correctness_judge_model,
160
+ groundedness_judge_model, run_ablation, started_at, finished_at, summary_json
161
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
162
+ """,
163
+ (
164
+ run_id,
165
+ run.config.testset_id,
166
+ run.config.adapter_path,
167
+ run.config.correctness_judge_model,
168
+ run.config.groundedness_judge_model,
169
+ int(run.config.run_ablation),
170
+ run.config.started_at.isoformat(),
171
+ run.finished_at.isoformat() if run.finished_at else None,
172
+ run.summary.model_dump_json() if run.summary is not None else None,
173
+ ),
174
+ )
175
+
176
+ for position, scored_case in enumerate(run.scored_cases):
177
+ conn.execute(
178
+ "INSERT INTO scored_cases (run_id, test_id, position, data_json) VALUES (?, ?, ?, ?)",
179
+ (run_id, scored_case.test_id, position, scored_case.model_dump_json()),
180
+ )
181
+
182
+ conn.commit()
183
+
184
+
185
+ def load_run(conn: sqlite3.Connection, run_id: str) -> Run | None:
186
+ """Load a Run by id, with its ScoredCases in their original order. None if not found."""
187
+ row = conn.execute(
188
+ """
189
+ SELECT run_id, testset_id, adapter_path, correctness_judge_model,
190
+ groundedness_judge_model, run_ablation, started_at, finished_at, summary_json
191
+ FROM runs WHERE run_id = ?
192
+ """,
193
+ (run_id,),
194
+ ).fetchone()
195
+ if row is None:
196
+ return None
197
+
198
+ config = RunConfig(
199
+ run_id=row[0],
200
+ testset_id=row[1],
201
+ adapter_path=row[2],
202
+ correctness_judge_model=row[3],
203
+ groundedness_judge_model=row[4],
204
+ run_ablation=bool(row[5]),
205
+ started_at=datetime.fromisoformat(row[6]),
206
+ )
207
+ finished_at = datetime.fromisoformat(row[7]) if row[7] else None
208
+ summary = RunSummary.model_validate_json(row[8]) if row[8] is not None else None
209
+
210
+ case_rows = conn.execute(
211
+ "SELECT data_json FROM scored_cases WHERE run_id = ? ORDER BY position ASC",
212
+ (run_id,),
213
+ ).fetchall()
214
+ scored_cases = [ScoredCase.model_validate_json(r[0]) for r in case_rows]
215
+
216
+ return Run(config=config, finished_at=finished_at, scored_cases=scored_cases, summary=summary)