querypilot 0.1.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 (54) hide show
  1. querypilot/__init__.py +36 -0
  2. querypilot/access/__init__.py +3 -0
  3. querypilot/access/policy.py +32 -0
  4. querypilot/adapters/__init__.py +1 -0
  5. querypilot/adapters/anthropic.py +49 -0
  6. querypilot/adapters/openai.py +53 -0
  7. querypilot/audit/__init__.py +10 -0
  8. querypilot/audit/sinks.py +46 -0
  9. querypilot/audit/types.py +53 -0
  10. querypilot/cli.py +401 -0
  11. querypilot/connectors/__init__.py +5 -0
  12. querypilot/connectors/base.py +24 -0
  13. querypilot/connectors/postgres.py +48 -0
  14. querypilot/connectors/sqlite.py +49 -0
  15. querypilot/core/__init__.py +1 -0
  16. querypilot/core/client.py +286 -0
  17. querypilot/core/config.py +29 -0
  18. querypilot/core/safety_defaults.py +12 -0
  19. querypilot/core/types.py +89 -0
  20. querypilot/evals/__init__.py +81 -0
  21. querypilot/evals/check.py +222 -0
  22. querypilot/evals/compare.py +286 -0
  23. querypilot/evals/cost.py +202 -0
  24. querypilot/evals/factory.py +83 -0
  25. querypilot/evals/init.py +104 -0
  26. querypilot/evals/loader.py +179 -0
  27. querypilot/evals/pipeline.py +484 -0
  28. querypilot/evals/replay.py +217 -0
  29. querypilot/evals/report.py +246 -0
  30. querypilot/evals/suite.py +91 -0
  31. querypilot/evals/suite_runner.py +361 -0
  32. querypilot/execution/__init__.py +1 -0
  33. querypilot/execution/formatter.py +11 -0
  34. querypilot/execution/runner.py +13 -0
  35. querypilot/generation/__init__.py +9 -0
  36. querypilot/generation/llm.py +180 -0
  37. querypilot/generation/prompt_builder.py +49 -0
  38. querypilot/generation/sql_generator.py +82 -0
  39. querypilot/mcp/__init__.py +3 -0
  40. querypilot/mcp/server.py +51 -0
  41. querypilot/py.typed +0 -0
  42. querypilot/schema/__init__.py +3 -0
  43. querypilot/schema/introspector.py +8 -0
  44. querypilot/schema/search.py +33 -0
  45. querypilot/server/__init__.py +3 -0
  46. querypilot/server/app.py +157 -0
  47. querypilot/validation/__init__.py +3 -0
  48. querypilot/validation/policies.py +3 -0
  49. querypilot/validation/validator.py +592 -0
  50. querypilot-0.1.0.dist-info/METADATA +601 -0
  51. querypilot-0.1.0.dist-info/RECORD +54 -0
  52. querypilot-0.1.0.dist-info/WHEEL +4 -0
  53. querypilot-0.1.0.dist-info/entry_points.txt +2 -0
  54. querypilot-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Sequence
5
+
6
+ from querypilot.core.types import DatabaseSchema
7
+
8
+
9
+ class BaseConnector(ABC):
10
+ def __init__(self, database_url: str, timeout_seconds: int = 10) -> None:
11
+ self.database_url = database_url
12
+ self.timeout_seconds = timeout_seconds
13
+
14
+ @abstractmethod
15
+ def get_schema(self) -> DatabaseSchema:
16
+ raise NotImplementedError
17
+
18
+ @abstractmethod
19
+ def execute_readonly(self, sql: str) -> tuple[list[dict], int]:
20
+ raise NotImplementedError
21
+
22
+ @staticmethod
23
+ def rows_to_dicts(keys: Sequence[str], rows: Sequence[Sequence[object]]) -> list[dict]:
24
+ return [dict(zip(keys, row, strict=False)) for row in rows]
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlalchemy import create_engine, inspect, text
4
+ from sqlalchemy.engine import Engine
5
+
6
+ from querypilot.connectors.base import BaseConnector
7
+ from querypilot.core.types import ColumnSchema, DatabaseSchema, TableSchema
8
+
9
+
10
+ class PostgresConnector(BaseConnector):
11
+ def __init__(self, database_url: str, timeout_seconds: int = 10) -> None:
12
+ super().__init__(database_url, timeout_seconds)
13
+ self.engine: Engine = create_engine(_normalize_postgres_url(database_url))
14
+
15
+ def get_schema(self) -> DatabaseSchema:
16
+ inspector = inspect(self.engine)
17
+ tables: list[TableSchema] = []
18
+ for table_name in inspector.get_table_names():
19
+ columns = [
20
+ ColumnSchema(
21
+ name=column["name"],
22
+ type=str(column["type"]),
23
+ nullable=bool(column.get("nullable", True)),
24
+ )
25
+ for column in inspector.get_columns(table_name)
26
+ ]
27
+ tables.append(TableSchema(name=table_name, columns=columns))
28
+ return DatabaseSchema(dialect="postgres", tables=tables)
29
+
30
+ def execute_readonly(self, sql: str) -> tuple[list[dict], int]:
31
+ with self.engine.connect() as conn:
32
+ readonly_result = conn.execute(text("SET TRANSACTION READ ONLY"))
33
+ readonly_result.close()
34
+ timeout_result = conn.execute(
35
+ text(f"SET LOCAL statement_timeout = {self.timeout_seconds * 1000}")
36
+ )
37
+ timeout_result.close()
38
+ query_result = conn.execute(text(sql))
39
+ rows = [dict(row._mapping) for row in query_result.fetchall()]
40
+ return rows, len(rows)
41
+
42
+
43
+ def _normalize_postgres_url(database_url: str) -> str:
44
+ if database_url.startswith("postgresql://"):
45
+ return database_url.replace("postgresql://", "postgresql+psycopg://", 1)
46
+ if database_url.startswith("postgres://"):
47
+ return database_url.replace("postgres://", "postgresql+psycopg://", 1)
48
+ return database_url
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlalchemy import create_engine, inspect, text
4
+ from sqlalchemy.engine import Engine
5
+
6
+ from querypilot.connectors.base import BaseConnector
7
+ from querypilot.core.types import ColumnSchema, DatabaseSchema, TableSchema
8
+
9
+
10
+ class SQLiteConnector(BaseConnector):
11
+ def __init__(self, database_url: str, timeout_seconds: int = 10) -> None:
12
+ super().__init__(database_url, timeout_seconds)
13
+ self.engine: Engine = create_engine(database_url)
14
+
15
+ def get_schema(self) -> DatabaseSchema:
16
+ inspector = inspect(self.engine)
17
+ tables: list[TableSchema] = []
18
+ for table_name in inspector.get_table_names():
19
+ columns = [
20
+ ColumnSchema(
21
+ name=column["name"],
22
+ type=str(column["type"]),
23
+ nullable=bool(column.get("nullable", True)),
24
+ )
25
+ for column in inspector.get_columns(table_name)
26
+ ]
27
+ tables.append(TableSchema(name=table_name, columns=columns))
28
+ return DatabaseSchema(dialect="sqlite", tables=tables)
29
+
30
+ def execute_readonly(self, sql: str) -> tuple[list[dict], int]:
31
+ with self.engine.connect() as conn:
32
+ conn.connection.set_progress_handler(
33
+ _timeout_handler(self.timeout_seconds),
34
+ 1000,
35
+ )
36
+ result = conn.execute(text(sql))
37
+ rows = [dict(row._mapping) for row in result.fetchall()]
38
+ return rows, len(rows)
39
+
40
+
41
+ def _timeout_handler(timeout_seconds: int):
42
+ import time
43
+
44
+ deadline = time.monotonic() + timeout_seconds
45
+
46
+ def handler() -> int:
47
+ return 1 if time.monotonic() > deadline else 0
48
+
49
+ return handler
@@ -0,0 +1 @@
1
+ """Core QueryPilot package."""
@@ -0,0 +1,286 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import TypeAdapter
6
+
7
+ from querypilot.access import AccessPolicy
8
+ from querypilot.adapters.anthropic import anthropic_tools
9
+ from querypilot.adapters.openai import openai_tools
10
+ from querypilot.audit import AuditMetadata, AuditSink, InMemoryAuditSink, QueryAuditRecord
11
+ from querypilot.connectors.base import BaseConnector
12
+ from querypilot.connectors.postgres import PostgresConnector
13
+ from querypilot.connectors.sqlite import SQLiteConnector
14
+ from querypilot.core.config import QueryPilotConfig, SafetyPolicy
15
+ from querypilot.core.types import (
16
+ DatabaseSchema,
17
+ GeneratedSQL,
18
+ QueryPilotAnswer,
19
+ QueryResult,
20
+ SchemaMatch,
21
+ ValidationResult,
22
+ )
23
+ from querypilot.execution.formatter import explain_result
24
+ from querypilot.execution.runner import execute
25
+ from querypilot.generation.sql_generator import DemoSQLGenerator, SQLGenerator
26
+ from querypilot.schema.search import search_schema as search_schema_impl
27
+ from querypilot.validation.validator import SQLValidator
28
+
29
+
30
+ class QueryPilot:
31
+ def __init__(
32
+ self,
33
+ connector: BaseConnector,
34
+ config: QueryPilotConfig,
35
+ generator: SQLGenerator | None = None,
36
+ audit_sink: AuditSink | None = None,
37
+ audit_metadata: AuditMetadata | None = None,
38
+ ) -> None:
39
+ self.connector = connector
40
+ self.config = config
41
+ self.generator = generator or DemoSQLGenerator()
42
+ self.validator = SQLValidator(config)
43
+ self.audit_sink = audit_sink or InMemoryAuditSink()
44
+ self.audit_metadata = audit_metadata or AuditMetadata()
45
+
46
+ @classmethod
47
+ def connect(
48
+ cls,
49
+ database_url: str,
50
+ dialect: str = "sqlite",
51
+ readonly: bool = True,
52
+ max_rows: int = 100,
53
+ timeout_seconds: int = 10,
54
+ max_generation_attempts: int = 2,
55
+ allowed_tables: list[str] | None = None,
56
+ blocked_tables: list[str] | None = None,
57
+ safety_policy: SafetyPolicy | None = None,
58
+ access_policy: AccessPolicy | None = None,
59
+ generator: SQLGenerator | None = None,
60
+ audit_sink: AuditSink | None = None,
61
+ audit_metadata: AuditMetadata | None = None,
62
+ ) -> "QueryPilot":
63
+ config = QueryPilotConfig(
64
+ dialect=dialect,
65
+ readonly=readonly,
66
+ max_rows=max_rows,
67
+ timeout_seconds=timeout_seconds,
68
+ max_generation_attempts=max_generation_attempts,
69
+ allowed_tables=allowed_tables,
70
+ blocked_tables=blocked_tables,
71
+ safety_policy=safety_policy or SafetyPolicy(),
72
+ access_policy=access_policy or AccessPolicy(),
73
+ )
74
+ connector = _connector_for(database_url, dialect, timeout_seconds)
75
+ return cls(
76
+ connector=connector,
77
+ config=config,
78
+ generator=generator,
79
+ audit_sink=audit_sink,
80
+ audit_metadata=audit_metadata,
81
+ )
82
+
83
+ def get_schema(self) -> DatabaseSchema:
84
+ return self.connector.get_schema()
85
+
86
+ def search_schema(self, query: str) -> list[SchemaMatch]:
87
+ matches = search_schema_impl(self.get_schema(), query)
88
+ self._write_audit_record(
89
+ operation="schema_search",
90
+ question=query,
91
+ row_count=len(matches),
92
+ executed=True,
93
+ )
94
+ return matches
95
+
96
+ def generate_sql(self, question: str) -> GeneratedSQL:
97
+ generated = self.generator.generate(question, self.get_schema(), self.config.max_rows)
98
+ self._write_audit_record(
99
+ operation="generate_sql",
100
+ question=question,
101
+ sql=generated.sql,
102
+ error="; ".join(generated.errors) if generated.errors else None,
103
+ executed=generated.sql is not None,
104
+ )
105
+ return generated
106
+
107
+ def validate_sql(self, sql: str) -> ValidationResult:
108
+ validation = self.validator.validate(sql, self.get_schema())
109
+ record = self._write_audit_record(
110
+ operation="validate_sql",
111
+ sql=sql,
112
+ rewritten_sql=validation.rewritten_sql,
113
+ validation=validation,
114
+ valid=validation.valid,
115
+ access_policy=validation.access_policy,
116
+ error="; ".join(validation.errors) if validation.errors else None,
117
+ )
118
+ validation.audit_id = record.audit_id
119
+ return validation
120
+
121
+ def execute_sql(self, sql: str) -> QueryResult:
122
+ validation = self.validator.validate(sql, self.get_schema())
123
+ if not validation.valid or validation.rewritten_sql is None:
124
+ details = "; ".join(validation.errors) or "unknown validation error"
125
+ self._write_audit_record(
126
+ operation="execute_sql",
127
+ sql=sql,
128
+ rewritten_sql=validation.rewritten_sql,
129
+ validation=validation,
130
+ valid=validation.valid,
131
+ executed=False,
132
+ access_policy=validation.access_policy,
133
+ error=details,
134
+ )
135
+ raise ValueError(f"SQL validation failed: {details}")
136
+ result = execute(self.connector, validation.rewritten_sql)
137
+ result.rows = self._apply_masking(result.rows, validation)
138
+ result.access_policy = validation.access_policy
139
+ record = self._write_audit_record(
140
+ operation="execute_sql",
141
+ sql=sql,
142
+ rewritten_sql=result.sql,
143
+ validation=validation,
144
+ valid=True,
145
+ executed=True,
146
+ row_count=result.row_count,
147
+ execution_time_ms=result.execution_time_ms,
148
+ access_policy=validation.access_policy,
149
+ )
150
+ validation.audit_id = record.audit_id
151
+ result.audit_id = record.audit_id
152
+ return result
153
+
154
+ def ask(self, question: str) -> QueryPilotAnswer:
155
+ generated = self.generate_sql(question)
156
+ validation: ValidationResult | None = None
157
+
158
+ for attempt in range(self.config.max_generation_attempts):
159
+ if generated.sql is None:
160
+ details = "; ".join(generated.errors) or "no SQL was returned"
161
+ raise ValueError(f"Could not generate SQL: {details}")
162
+
163
+ validation = self.validate_sql(generated.sql)
164
+ if validation.valid and validation.rewritten_sql is not None:
165
+ break
166
+
167
+ can_repair = hasattr(self.generator, "repair")
168
+ if not can_repair or attempt >= self.config.max_generation_attempts - 1:
169
+ details = "; ".join(validation.errors) or "unknown validation error"
170
+ raise ValueError(f"SQL validation failed: {details}")
171
+
172
+ generated = self.generator.repair( # type: ignore[attr-defined]
173
+ question,
174
+ self.get_schema(),
175
+ self.config.max_rows,
176
+ generated.sql,
177
+ validation,
178
+ )
179
+
180
+ if validation is None or not validation.valid or validation.rewritten_sql is None:
181
+ raise ValueError("SQL validation failed: unknown validation error")
182
+
183
+ result = execute(self.connector, validation.rewritten_sql)
184
+ result.rows = self._apply_masking(result.rows, validation)
185
+ result.access_policy = validation.access_policy
186
+ explanation = generated.explanation or explain_result(question, result.sql, result.rows)
187
+ record = self._write_audit_record(
188
+ operation="ask",
189
+ question=question,
190
+ sql=generated.sql,
191
+ rewritten_sql=result.sql,
192
+ validation=validation,
193
+ valid=True,
194
+ executed=True,
195
+ row_count=result.row_count,
196
+ execution_time_ms=result.execution_time_ms,
197
+ access_policy=validation.access_policy,
198
+ )
199
+ return QueryPilotAnswer(
200
+ audit_id=record.audit_id,
201
+ question=question,
202
+ sql=result.sql,
203
+ rows=result.rows,
204
+ explanation=explanation,
205
+ validation=validation,
206
+ execution_time_ms=result.execution_time_ms,
207
+ access_policy=validation.access_policy,
208
+ )
209
+
210
+ def as_openai_tools(self) -> list[dict[str, Any]]:
211
+ return openai_tools()
212
+
213
+ def as_anthropic_tools(self) -> list[dict[str, Any]]:
214
+ return anthropic_tools()
215
+
216
+ def handle_anthropic_tool_call(self, tool_name: str, tool_input: dict[str, Any]) -> Any:
217
+ if tool_name == "ask_database":
218
+ return self.ask(str(tool_input["question"])).model_dump()
219
+ if tool_name == "search_schema":
220
+ matches = self.search_schema(str(tool_input["query"]))
221
+ return TypeAdapter(list[SchemaMatch]).dump_python(matches)
222
+ if tool_name == "validate_sql":
223
+ return self.validate_sql(str(tool_input["sql"])).model_dump()
224
+ if tool_name == "execute_sql":
225
+ return self.execute_sql(str(tool_input["sql"])).model_dump()
226
+ raise ValueError(f"Unknown Anthropic tool: {tool_name}")
227
+
228
+ def get_audit_records(self, limit: int = 100) -> list[QueryAuditRecord]:
229
+ return self.audit_sink.recent(limit)
230
+
231
+ def with_audit_metadata(self, metadata: AuditMetadata | None) -> "QueryPilot":
232
+ return QueryPilot(
233
+ connector=self.connector,
234
+ config=self.config,
235
+ generator=self.generator,
236
+ audit_sink=self.audit_sink,
237
+ audit_metadata=metadata or AuditMetadata(),
238
+ )
239
+
240
+ def _write_audit_record(self, operation: str, **kwargs) -> QueryAuditRecord:
241
+ record = QueryAuditRecord.create(
242
+ operation=operation,
243
+ metadata=self.audit_metadata,
244
+ **kwargs,
245
+ )
246
+ self.audit_sink.write(record)
247
+ return record
248
+
249
+ def _apply_masking(self, rows: list[dict], validation: ValidationResult) -> list[dict]:
250
+ if not rows or not self.config.access_policy.masking_rules:
251
+ return rows
252
+
253
+ rules: dict[str, str] = {}
254
+ for table_name in validation.tables:
255
+ for column_name, rule in self.config.access_policy.masking_rules.get(table_name, {}).items():
256
+ rules[column_name] = rule.mode
257
+
258
+ if not rules:
259
+ return rows
260
+
261
+ return [
262
+ {
263
+ key: _mask_value(value, rules[key]) if key in rules else value
264
+ for key, value in row.items()
265
+ }
266
+ for row in rows
267
+ ]
268
+
269
+
270
+ def _connector_for(database_url: str, dialect: str, timeout_seconds: int) -> BaseConnector:
271
+ normalized = dialect.lower()
272
+ if normalized == "sqlite":
273
+ return SQLiteConnector(database_url, timeout_seconds=timeout_seconds)
274
+ if normalized in {"postgres", "postgresql"}:
275
+ return PostgresConnector(database_url, timeout_seconds=timeout_seconds)
276
+ raise ValueError(f"Unsupported dialect: {dialect}")
277
+
278
+
279
+ def _mask_value(value: Any, mode: str) -> Any:
280
+ if mode == "null":
281
+ return None
282
+ if mode == "hash":
283
+ import hashlib
284
+
285
+ return hashlib.sha256(str(value).encode("utf-8")).hexdigest()
286
+ return "[REDACTED]"
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from querypilot.access import AccessPolicy
6
+ from querypilot.core.safety_defaults import BLOCKED_POSTGRES_FUNCTIONS
7
+
8
+
9
+ class SafetyPolicy(BaseModel):
10
+ allow_select_star: bool = True
11
+ reject_cartesian_joins: bool = True
12
+ reject_multi_statement: bool = True
13
+ warn_on_select_star: bool = True
14
+ blocked_functions: list[str] = Field(
15
+ default_factory=lambda: sorted(BLOCKED_POSTGRES_FUNCTIONS)
16
+ )
17
+ allowed_functions: list[str] | None = None
18
+
19
+
20
+ class QueryPilotConfig(BaseModel):
21
+ dialect: str = "sqlite"
22
+ readonly: bool = True
23
+ max_rows: int = Field(default=100, ge=1)
24
+ timeout_seconds: int = Field(default=10, ge=1)
25
+ max_generation_attempts: int = Field(default=2, ge=1)
26
+ allowed_tables: list[str] | None = None
27
+ blocked_tables: list[str] | None = None
28
+ safety_policy: SafetyPolicy = Field(default_factory=SafetyPolicy)
29
+ access_policy: AccessPolicy = Field(default_factory=AccessPolicy)
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ BLOCKED_POSTGRES_FUNCTIONS = {
4
+ "nextval",
5
+ "pg_cancel_backend",
6
+ "pg_ls_dir",
7
+ "pg_read_binary_file",
8
+ "pg_read_file",
9
+ "pg_sleep",
10
+ "pg_terminate_backend",
11
+ "setval",
12
+ }
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class ColumnSchema(BaseModel):
7
+ name: str
8
+ type: str
9
+ nullable: bool = True
10
+
11
+
12
+ class TableSchema(BaseModel):
13
+ name: str
14
+ columns: list[ColumnSchema] = Field(default_factory=list)
15
+
16
+ def get_column(self, name: str) -> ColumnSchema | None:
17
+ lowered = name.lower()
18
+ return next((column for column in self.columns if column.name.lower() == lowered), None)
19
+
20
+
21
+ class DatabaseSchema(BaseModel):
22
+ dialect: str
23
+ tables: list[TableSchema] = Field(default_factory=list)
24
+
25
+ def get_table(self, name: str) -> TableSchema | None:
26
+ normalized = name.split(".")[-1].strip('"').lower()
27
+ return next((table for table in self.tables if table.name.lower() == normalized), None)
28
+
29
+ @property
30
+ def table_names(self) -> set[str]:
31
+ return {table.name.lower() for table in self.tables}
32
+
33
+
34
+ class SchemaMatch(BaseModel):
35
+ table: str
36
+ column: str | None = None
37
+ score: int
38
+ reason: str
39
+
40
+
41
+ class GeneratedSQL(BaseModel):
42
+ question: str
43
+ sql: str | None
44
+ explanation: str | None = None
45
+ errors: list[str] = Field(default_factory=list)
46
+
47
+
48
+ class PolicyCheck(BaseModel):
49
+ name: str
50
+ passed: bool
51
+ message: str
52
+ severity: str = "low"
53
+
54
+
55
+ class ValidationResult(BaseModel):
56
+ audit_id: str | None = None
57
+ valid: bool
58
+ readonly: bool
59
+ tables: list[str] = Field(default_factory=list)
60
+ columns: list[str] = Field(default_factory=list)
61
+ limit_applied: bool = False
62
+ rewritten_sql: str | None = None
63
+ risk_level: str = "low"
64
+ blocked_reason: str | None = None
65
+ policy_checks: list[PolicyCheck] = Field(default_factory=list)
66
+ query_fingerprint: str | None = None
67
+ access_policy: dict = Field(default_factory=dict)
68
+ warnings: list[str] = Field(default_factory=list)
69
+ errors: list[str] = Field(default_factory=list)
70
+
71
+
72
+ class QueryResult(BaseModel):
73
+ audit_id: str | None = None
74
+ sql: str
75
+ rows: list[dict] = Field(default_factory=list)
76
+ row_count: int
77
+ execution_time_ms: int
78
+ access_policy: dict = Field(default_factory=dict)
79
+
80
+
81
+ class QueryPilotAnswer(BaseModel):
82
+ audit_id: str | None = None
83
+ question: str
84
+ sql: str
85
+ rows: list[dict] = Field(default_factory=list)
86
+ explanation: str
87
+ validation: ValidationResult
88
+ execution_time_ms: int
89
+ access_policy: dict = Field(default_factory=dict)
@@ -0,0 +1,81 @@
1
+ from querypilot.evals.check import (
2
+ CaseRegression,
3
+ CheckOutcome,
4
+ CheckSummary,
5
+ check_report,
6
+ format_outcome,
7
+ load_report,
8
+ write_outcome,
9
+ )
10
+ from querypilot.evals.compare import RowsetMatch, ValueMismatch, compare_rows, has_order_by
11
+ from querypilot.evals.cost import (
12
+ AnthropicCostTracker,
13
+ CostTracker,
14
+ NullCostTracker,
15
+ OpenAICostTracker,
16
+ TokenUsage,
17
+ )
18
+ from querypilot.evals.factory import (
19
+ build_cost_tracker_factory,
20
+ build_generator,
21
+ build_qp_factory,
22
+ load_suite_or_dir,
23
+ )
24
+ from querypilot.evals.loader import SuiteLoadError, load_suite, load_suite_dir, write_suite
25
+ from querypilot.evals.replay import replay_from_jsonl, replay_from_sink
26
+ from querypilot.evals.pipeline import (
27
+ CaseResult,
28
+ FailureCategory,
29
+ StageTimings,
30
+ run_case,
31
+ )
32
+ from querypilot.evals.report import render_terminal, write_json
33
+ from querypilot.evals.suite import (
34
+ BenchmarkCase,
35
+ BenchmarkSuite,
36
+ ComparisonConfig,
37
+ SuiteThresholds,
38
+ )
39
+ from querypilot.evals.suite_runner import SuiteReport, TagRollup, run_suite
40
+
41
+ __all__ = [
42
+ "AnthropicCostTracker",
43
+ "BenchmarkCase",
44
+ "BenchmarkSuite",
45
+ "CaseRegression",
46
+ "CaseResult",
47
+ "CheckOutcome",
48
+ "CheckSummary",
49
+ "ComparisonConfig",
50
+ "CostTracker",
51
+ "FailureCategory",
52
+ "NullCostTracker",
53
+ "OpenAICostTracker",
54
+ "RowsetMatch",
55
+ "StageTimings",
56
+ "SuiteLoadError",
57
+ "SuiteReport",
58
+ "SuiteThresholds",
59
+ "TagRollup",
60
+ "TokenUsage",
61
+ "ValueMismatch",
62
+ "build_cost_tracker_factory",
63
+ "build_generator",
64
+ "build_qp_factory",
65
+ "check_report",
66
+ "compare_rows",
67
+ "format_outcome",
68
+ "has_order_by",
69
+ "load_report",
70
+ "load_suite",
71
+ "load_suite_dir",
72
+ "load_suite_or_dir",
73
+ "render_terminal",
74
+ "replay_from_jsonl",
75
+ "replay_from_sink",
76
+ "run_case",
77
+ "run_suite",
78
+ "write_json",
79
+ "write_outcome",
80
+ "write_suite",
81
+ ]