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,11 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def explain_result(question: str, sql: str, rows: list[dict]) -> str:
5
+ if not rows:
6
+ return f"The validated query returned no rows for: {question}"
7
+ columns = ", ".join(rows[0].keys())
8
+ return (
9
+ f"The validated query answers the question using columns {columns}. "
10
+ f"It returned {len(rows)} row(s) after QueryPilot safety checks."
11
+ )
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+
5
+ from querypilot.connectors.base import BaseConnector
6
+ from querypilot.core.types import QueryResult
7
+
8
+
9
+ def execute(connector: BaseConnector, sql: str) -> QueryResult:
10
+ start = time.perf_counter()
11
+ rows, row_count = connector.execute_readonly(sql)
12
+ elapsed_ms = int((time.perf_counter() - start) * 1000)
13
+ return QueryResult(sql=sql, rows=rows, row_count=row_count, execution_time_ms=elapsed_ms)
@@ -0,0 +1,9 @@
1
+ from querypilot.generation.llm import AnthropicSQLGenerator, OpenAISQLGenerator
2
+ from querypilot.generation.sql_generator import DemoSQLGenerator, SQLGenerator
3
+
4
+ __all__ = [
5
+ "AnthropicSQLGenerator",
6
+ "DemoSQLGenerator",
7
+ "OpenAISQLGenerator",
8
+ "SQLGenerator",
9
+ ]
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from typing import Any
6
+
7
+ from querypilot.core.types import DatabaseSchema, GeneratedSQL, ValidationResult
8
+ from querypilot.generation.prompt_builder import build_sql_generation_prompt
9
+
10
+
11
+ SQL_JSON_SCHEMA: dict[str, Any] = {
12
+ "name": "querypilot_sql",
13
+ "schema": {
14
+ "type": "object",
15
+ "properties": {
16
+ "sql": {"type": "string"},
17
+ "explanation": {"type": "string"},
18
+ },
19
+ "required": ["sql", "explanation"],
20
+ "additionalProperties": False,
21
+ },
22
+ "strict": True,
23
+ }
24
+
25
+
26
+ class OpenAISQLGenerator:
27
+ def __init__(
28
+ self,
29
+ client: Any | None = None,
30
+ model: str = "gpt-5.1",
31
+ max_output_tokens: int = 800,
32
+ ) -> None:
33
+ self.client = client or _default_openai_client()
34
+ self.model = model
35
+ self.max_output_tokens = max_output_tokens
36
+
37
+ def generate(self, question: str, schema: DatabaseSchema, max_rows: int) -> GeneratedSQL:
38
+ prompt = build_sql_generation_prompt(question, schema, max_rows)
39
+ return self._create(question, prompt.instructions, prompt.user_prompt)
40
+
41
+ def repair(
42
+ self,
43
+ question: str,
44
+ schema: DatabaseSchema,
45
+ max_rows: int,
46
+ previous_sql: str,
47
+ validation: ValidationResult,
48
+ ) -> GeneratedSQL:
49
+ prompt = build_sql_generation_prompt(
50
+ question,
51
+ schema,
52
+ max_rows,
53
+ validation_errors=validation.errors,
54
+ previous_sql=previous_sql,
55
+ )
56
+ return self._create(question, prompt.instructions, prompt.user_prompt)
57
+
58
+ def _create(self, question: str, instructions: str, user_prompt: str) -> GeneratedSQL:
59
+ response = self.client.responses.create(
60
+ model=self.model,
61
+ instructions=instructions,
62
+ input=user_prompt,
63
+ max_output_tokens=self.max_output_tokens,
64
+ text={"format": {"type": "json_schema", **SQL_JSON_SCHEMA}},
65
+ )
66
+ return _generated_from_text(question, _extract_openai_text(response))
67
+
68
+
69
+ class AnthropicSQLGenerator:
70
+ def __init__(
71
+ self,
72
+ client: Any | None = None,
73
+ model: str = "claude-sonnet-4-20250514",
74
+ max_tokens: int = 800,
75
+ ) -> None:
76
+ self.client = client or _default_anthropic_client()
77
+ self.model = model
78
+ self.max_tokens = max_tokens
79
+
80
+ def generate(self, question: str, schema: DatabaseSchema, max_rows: int) -> GeneratedSQL:
81
+ prompt = build_sql_generation_prompt(question, schema, max_rows)
82
+ return self._create(question, prompt.instructions, prompt.user_prompt)
83
+
84
+ def repair(
85
+ self,
86
+ question: str,
87
+ schema: DatabaseSchema,
88
+ max_rows: int,
89
+ previous_sql: str,
90
+ validation: ValidationResult,
91
+ ) -> GeneratedSQL:
92
+ prompt = build_sql_generation_prompt(
93
+ question,
94
+ schema,
95
+ max_rows,
96
+ validation_errors=validation.errors,
97
+ previous_sql=previous_sql,
98
+ )
99
+ return self._create(question, prompt.instructions, prompt.user_prompt)
100
+
101
+ def _create(self, question: str, system_prompt: str, user_prompt: str) -> GeneratedSQL:
102
+ message = self.client.messages.create(
103
+ model=self.model,
104
+ max_tokens=self.max_tokens,
105
+ system=system_prompt,
106
+ messages=[{"role": "user", "content": user_prompt}],
107
+ )
108
+ return _generated_from_text(question, _extract_anthropic_text(message))
109
+
110
+
111
+ def _generated_from_text(question: str, text: str) -> GeneratedSQL:
112
+ try:
113
+ payload = json.loads(_strip_code_fence(text))
114
+ except json.JSONDecodeError as exc:
115
+ return GeneratedSQL(
116
+ question=question,
117
+ sql=None,
118
+ errors=[f"LLM response was not valid JSON: {exc}"],
119
+ )
120
+
121
+ sql = payload.get("sql")
122
+ if not isinstance(sql, str) or not sql.strip():
123
+ return GeneratedSQL(question=question, sql=None, errors=["LLM response did not include SQL."])
124
+ explanation = payload.get("explanation")
125
+ return GeneratedSQL(
126
+ question=question,
127
+ sql=sql.strip(),
128
+ explanation=explanation if isinstance(explanation, str) else None,
129
+ )
130
+
131
+
132
+ def _strip_code_fence(text: str) -> str:
133
+ stripped = text.strip()
134
+ match = re.fullmatch(r"```(?:json)?\s*(.*?)\s*```", stripped, flags=re.DOTALL)
135
+ return match.group(1).strip() if match else stripped
136
+
137
+
138
+ def _extract_openai_text(response: Any) -> str:
139
+ output_text = getattr(response, "output_text", None)
140
+ if isinstance(output_text, str):
141
+ return output_text
142
+ output = getattr(response, "output", None)
143
+ if isinstance(output, list):
144
+ for item in output:
145
+ for content in getattr(item, "content", []) or []:
146
+ text = getattr(content, "text", None)
147
+ if isinstance(text, str):
148
+ return text
149
+ return str(response)
150
+
151
+
152
+ def _extract_anthropic_text(message: Any) -> str:
153
+ content = getattr(message, "content", None)
154
+ if isinstance(content, list):
155
+ chunks: list[str] = []
156
+ for block in content:
157
+ text = getattr(block, "text", None)
158
+ if isinstance(text, str):
159
+ chunks.append(text)
160
+ elif isinstance(block, dict) and isinstance(block.get("text"), str):
161
+ chunks.append(block["text"])
162
+ if chunks:
163
+ return "".join(chunks)
164
+ return str(message)
165
+
166
+
167
+ def _default_openai_client() -> Any:
168
+ try:
169
+ from openai import OpenAI
170
+ except ImportError as exc:
171
+ raise ImportError("Install querypilot[openai] to use OpenAISQLGenerator.") from exc
172
+ return OpenAI()
173
+
174
+
175
+ def _default_anthropic_client() -> Any:
176
+ try:
177
+ import anthropic
178
+ except ImportError as exc:
179
+ raise ImportError("Install querypilot[anthropic] to use AnthropicSQLGenerator.") from exc
180
+ return anthropic.Anthropic()
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from querypilot.core.types import DatabaseSchema
6
+
7
+
8
+ class SQLGenerationPrompt(BaseModel):
9
+ instructions: str
10
+ user_prompt: str
11
+
12
+
13
+ def build_sql_generation_prompt(
14
+ question: str,
15
+ schema: DatabaseSchema,
16
+ max_rows: int,
17
+ validation_errors: list[str] | None = None,
18
+ previous_sql: str | None = None,
19
+ ) -> SQLGenerationPrompt:
20
+ instructions = (
21
+ "You generate safe SQL for QueryPilot. Return only JSON with keys "
22
+ "`sql` and `explanation`. Generate SELECT-only SQL. Use only tables "
23
+ "and columns present in the provided schema. Do not use mutations, DDL, "
24
+ "multiple statements, comments, or vendor-specific unsafe commands. "
25
+ f"The maximum row limit is {max_rows}; include a LIMIT no larger than that "
26
+ "unless the query is an aggregate that returns one row."
27
+ )
28
+ if validation_errors:
29
+ instructions += " Repair the SQL using the validation errors."
30
+
31
+ parts = [
32
+ f"Question: {question}",
33
+ "",
34
+ "Schema:",
35
+ build_schema_context(schema),
36
+ ]
37
+ if previous_sql:
38
+ parts.extend(["", f"Previous SQL: {previous_sql}"])
39
+ if validation_errors:
40
+ parts.extend(["", "Validation errors:", "\n".join(f"- {error}" for error in validation_errors)])
41
+ return SQLGenerationPrompt(instructions=instructions, user_prompt="\n".join(parts))
42
+
43
+
44
+ def build_schema_context(schema: DatabaseSchema) -> str:
45
+ lines: list[str] = []
46
+ for table in schema.tables:
47
+ columns = ", ".join(f"{column.name} {column.type}" for column in table.columns)
48
+ lines.append(f"{table.name}({columns})")
49
+ return "\n".join(lines)
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Protocol
5
+
6
+ from querypilot.core.types import DatabaseSchema, GeneratedSQL
7
+
8
+
9
+ class SQLGenerator(Protocol):
10
+ def generate(self, question: str, schema: DatabaseSchema, max_rows: int) -> GeneratedSQL:
11
+ ...
12
+
13
+
14
+ class DemoSQLGenerator:
15
+ def generate(self, question: str, schema: DatabaseSchema, max_rows: int) -> GeneratedSQL:
16
+ normalized = question.lower()
17
+ explicit_limit = _extract_limit(normalized)
18
+ limit = min(explicit_limit or max_rows, max_rows)
19
+
20
+ if "count" in normalized and schema.tables:
21
+ table = _best_table(normalized, schema) or schema.tables[0]
22
+ return GeneratedSQL(
23
+ question=question,
24
+ sql=f"SELECT COUNT(*) AS count FROM {table.name} LIMIT {limit}",
25
+ explanation=f"Counts rows in {table.name}.",
26
+ )
27
+
28
+ customer_table = schema.get_table("customers")
29
+ if customer_table and any(term in normalized for term in ["customer", "customers"]):
30
+ if "revenue" in normalized and customer_table.get_column("revenue"):
31
+ return GeneratedSQL(
32
+ question=question,
33
+ sql=(
34
+ "SELECT customer_name, revenue FROM customers "
35
+ f"ORDER BY revenue DESC LIMIT {limit}"
36
+ ),
37
+ explanation="Ranks customers by revenue.",
38
+ )
39
+ if "arr" in normalized and customer_table.get_column("arr"):
40
+ return GeneratedSQL(
41
+ question=question,
42
+ sql=f"SELECT customer_name, arr FROM customers ORDER BY arr DESC LIMIT {limit}",
43
+ explanation="Ranks customers by ARR.",
44
+ )
45
+ return GeneratedSQL(
46
+ question=question,
47
+ sql=f"SELECT * FROM customers LIMIT {limit}",
48
+ explanation="Returns customer records.",
49
+ )
50
+
51
+ table = _best_table(normalized, schema)
52
+ if table and any(term in normalized for term in ["show", "list", "top"]):
53
+ return GeneratedSQL(
54
+ question=question,
55
+ sql=f"SELECT * FROM {table.name} LIMIT {limit}",
56
+ explanation=f"Returns rows from {table.name}.",
57
+ )
58
+
59
+ return GeneratedSQL(
60
+ question=question,
61
+ sql=None,
62
+ explanation=None,
63
+ errors=[
64
+ "Offline generator could not map this question to a safe SQL template. "
65
+ "Configure a SQLGenerator provider for broader natural-language support."
66
+ ],
67
+ )
68
+
69
+
70
+ def _extract_limit(question: str) -> int | None:
71
+ match = re.search(r"\btop\s+(\d+)\b|\blimit\s+(\d+)\b", question)
72
+ if not match:
73
+ return None
74
+ value = match.group(1) or match.group(2)
75
+ return int(value)
76
+
77
+
78
+ def _best_table(question: str, schema: DatabaseSchema):
79
+ for table in schema.tables:
80
+ if table.name.lower() in question or table.name.lower().rstrip("s") in question:
81
+ return table
82
+ return None
@@ -0,0 +1,3 @@
1
+ from querypilot.mcp.server import create_mcp_server
2
+
3
+ __all__ = ["create_mcp_server"]
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable
4
+
5
+ from querypilot import QueryPilot
6
+
7
+
8
+ def create_mcp_server(
9
+ querypilot: QueryPilot,
10
+ *,
11
+ fastmcp_cls: type | None = None,
12
+ ):
13
+ fastmcp = fastmcp_cls or _load_fastmcp()
14
+ server = fastmcp("QueryPilot", stateless_http=True, json_response=True)
15
+
16
+ @server.tool()
17
+ def ask_database(question: str) -> dict[str, Any]:
18
+ """Ask a database question through QueryPilot's safe SQL flow."""
19
+ return _safe_call(lambda: querypilot.ask(question).model_dump())
20
+
21
+ @server.tool()
22
+ def search_schema(query: str) -> list[dict[str, Any]] | dict[str, str]:
23
+ """Search tables and columns relevant to a question."""
24
+ return _safe_call(lambda: [match.model_dump() for match in querypilot.search_schema(query)])
25
+
26
+ @server.tool()
27
+ def validate_sql(sql: str) -> dict[str, Any]:
28
+ """Validate SQL and return policy metadata."""
29
+ return _safe_call(lambda: querypilot.validate_sql(sql).model_dump())
30
+
31
+ @server.tool()
32
+ def execute_sql(sql: str) -> dict[str, Any]:
33
+ """Validate and execute read-only SQL safely."""
34
+ return _safe_call(lambda: querypilot.execute_sql(sql).model_dump())
35
+
36
+ return server
37
+
38
+
39
+ def _safe_call(callback: Callable[[], Any]) -> Any:
40
+ try:
41
+ return callback()
42
+ except ValueError as exc:
43
+ return {"error": str(exc)}
44
+
45
+
46
+ def _load_fastmcp():
47
+ try:
48
+ from mcp.server.fastmcp import FastMCP
49
+ except ImportError as exc:
50
+ raise ImportError("Install querypilot[mcp] to run the MCP server.") from exc
51
+ return FastMCP
querypilot/py.typed ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ from querypilot.schema.search import search_schema
2
+
3
+ __all__ = ["search_schema"]
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from querypilot.connectors.base import BaseConnector
4
+ from querypilot.core.types import DatabaseSchema
5
+
6
+
7
+ def introspect(connector: BaseConnector) -> DatabaseSchema:
8
+ return connector.get_schema()
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from querypilot.core.types import DatabaseSchema, SchemaMatch
6
+
7
+
8
+ def search_schema(schema: DatabaseSchema, query: str) -> list[SchemaMatch]:
9
+ tokens = set(re.findall(r"[a-z0-9_]+", query.lower()))
10
+ matches: list[SchemaMatch] = []
11
+ for table in schema.tables:
12
+ table_tokens = _name_tokens(table.name)
13
+ table_score = len(tokens & table_tokens) * 3
14
+ column_hits: list[str] = []
15
+ for column in table.columns:
16
+ column_score = len(tokens & _name_tokens(column.name))
17
+ if column_score:
18
+ column_hits.append(column.name)
19
+ table_score += column_score
20
+ if table_score:
21
+ matches.append(
22
+ SchemaMatch(
23
+ table=table.name,
24
+ column=", ".join(column_hits) if column_hits else None,
25
+ score=table_score,
26
+ reason="Matched table or column name",
27
+ )
28
+ )
29
+ return sorted(matches, key=lambda match: (-match.score, match.table))
30
+
31
+
32
+ def _name_tokens(name: str) -> set[str]:
33
+ return set(re.findall(r"[a-z0-9]+", name.lower()))
@@ -0,0 +1,3 @@
1
+ from querypilot.server.app import create_app
2
+
3
+ __all__ = ["create_app"]
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from fastapi import FastAPI, HTTPException
6
+ from pydantic import BaseModel, Field
7
+
8
+ from querypilot import QueryPilot
9
+ from querypilot.access import AccessPolicy
10
+ from querypilot.audit import AuditMetadata
11
+ from querypilot.core.config import SafetyPolicy
12
+ from querypilot.evals.pipeline import run_case
13
+ from querypilot.evals.suite import BenchmarkCase, ComparisonConfig
14
+
15
+
16
+ class QuestionRequest(BaseModel):
17
+ question: str
18
+ metadata: AuditMetadata | None = None
19
+
20
+
21
+ class SQLRequest(BaseModel):
22
+ sql: str
23
+ metadata: AuditMetadata | None = None
24
+
25
+
26
+ class SchemaSearchRequest(BaseModel):
27
+ query: str
28
+ metadata: AuditMetadata | None = None
29
+
30
+
31
+ class EvalRunRequest(BaseModel):
32
+ cases: list[BenchmarkCase] = Field(default_factory=list)
33
+ comparison: ComparisonConfig | None = None
34
+
35
+
36
+ def create_app(
37
+ querypilot: QueryPilot | None = None,
38
+ *,
39
+ database_url: str | None = None,
40
+ dialect: str = "sqlite",
41
+ readonly: bool = True,
42
+ max_rows: int = 100,
43
+ timeout_seconds: int = 10,
44
+ max_generation_attempts: int = 2,
45
+ allowed_tables: list[str] | None = None,
46
+ blocked_tables: list[str] | None = None,
47
+ safety_policy: SafetyPolicy | None = None,
48
+ access_policy: AccessPolicy | None = None,
49
+ ) -> FastAPI:
50
+ qp = querypilot or _connect_querypilot(
51
+ database_url=database_url,
52
+ dialect=dialect,
53
+ readonly=readonly,
54
+ max_rows=max_rows,
55
+ timeout_seconds=timeout_seconds,
56
+ max_generation_attempts=max_generation_attempts,
57
+ allowed_tables=allowed_tables,
58
+ blocked_tables=blocked_tables,
59
+ safety_policy=safety_policy,
60
+ access_policy=access_policy,
61
+ )
62
+
63
+ app = FastAPI(
64
+ title="QueryPilot",
65
+ description="Safe SQL tool layer for AI agents.",
66
+ version="0.1.0",
67
+ )
68
+ app.state.querypilot = qp
69
+
70
+ @app.get("/health")
71
+ def health() -> dict[str, str]:
72
+ return {"status": "ok"}
73
+
74
+ @app.get("/schema")
75
+ def get_schema() -> dict[str, Any]:
76
+ return qp.get_schema().model_dump()
77
+
78
+ @app.post("/search-schema")
79
+ def search_schema(request: SchemaSearchRequest) -> list[dict[str, Any]]:
80
+ scoped_qp = qp.with_audit_metadata(request.metadata)
81
+ return [match.model_dump() for match in scoped_qp.search_schema(request.query)]
82
+
83
+ @app.post("/ask")
84
+ def ask(request: QuestionRequest) -> dict[str, Any]:
85
+ scoped_qp = qp.with_audit_metadata(request.metadata)
86
+ try:
87
+ return scoped_qp.ask(request.question).model_dump()
88
+ except ValueError as exc:
89
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
90
+
91
+ @app.post("/generate-sql")
92
+ def generate_sql(request: QuestionRequest) -> dict[str, Any]:
93
+ scoped_qp = qp.with_audit_metadata(request.metadata)
94
+ return scoped_qp.generate_sql(request.question).model_dump()
95
+
96
+ @app.post("/validate-sql")
97
+ def validate_sql(request: SQLRequest) -> dict[str, Any]:
98
+ scoped_qp = qp.with_audit_metadata(request.metadata)
99
+ return scoped_qp.validate_sql(request.sql).model_dump()
100
+
101
+ @app.post("/execute-sql")
102
+ def execute_sql(request: SQLRequest) -> dict[str, Any]:
103
+ scoped_qp = qp.with_audit_metadata(request.metadata)
104
+ try:
105
+ return scoped_qp.execute_sql(request.sql).model_dump()
106
+ except ValueError as exc:
107
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
108
+
109
+ @app.post("/evals/run")
110
+ def run_evals(request: EvalRunRequest) -> dict[str, Any]:
111
+ comparison = request.comparison or ComparisonConfig()
112
+ results = [
113
+ run_case(case, lambda _: qp, comparison=comparison).model_dump(mode="json")
114
+ for case in request.cases
115
+ ]
116
+ passed = sum(1 for r in results if r["passed"])
117
+ return {
118
+ "total": len(results),
119
+ "passed": passed,
120
+ "failed": len(results) - passed,
121
+ "case_results": results,
122
+ }
123
+
124
+ @app.get("/audit/recent")
125
+ def recent_audit(limit: int = 100) -> list[dict[str, Any]]:
126
+ return [record.model_dump() for record in qp.get_audit_records(limit)]
127
+
128
+ return app
129
+
130
+
131
+ def _connect_querypilot(
132
+ *,
133
+ database_url: str | None,
134
+ dialect: str,
135
+ readonly: bool,
136
+ max_rows: int,
137
+ timeout_seconds: int,
138
+ max_generation_attempts: int,
139
+ allowed_tables: list[str] | None,
140
+ blocked_tables: list[str] | None,
141
+ safety_policy: SafetyPolicy | None,
142
+ access_policy: AccessPolicy | None,
143
+ ) -> QueryPilot:
144
+ if database_url is None:
145
+ raise ValueError("database_url is required when querypilot is not provided.")
146
+ return QueryPilot.connect(
147
+ database_url=database_url,
148
+ dialect=dialect,
149
+ readonly=readonly,
150
+ max_rows=max_rows,
151
+ timeout_seconds=timeout_seconds,
152
+ max_generation_attempts=max_generation_attempts,
153
+ allowed_tables=allowed_tables,
154
+ blocked_tables=blocked_tables,
155
+ safety_policy=safety_policy,
156
+ access_policy=access_policy,
157
+ )
@@ -0,0 +1,3 @@
1
+ from querypilot.validation.validator import SQLValidator
2
+
3
+ __all__ = ["SQLValidator"]
@@ -0,0 +1,3 @@
1
+ from querypilot.core.safety_defaults import BLOCKED_POSTGRES_FUNCTIONS
2
+
3
+ __all__ = ["BLOCKED_POSTGRES_FUNCTIONS"]