nl2sql-engine 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.
- nl2sql/__init__.py +38 -0
- nl2sql/adapters/__init__.py +0 -0
- nl2sql/adapters/duckdb/__init__.py +0 -0
- nl2sql/adapters/duckdb/adapter.py +71 -0
- nl2sql/adapters/mssql/__init__.py +0 -0
- nl2sql/adapters/mssql/adapter.py +122 -0
- nl2sql/adapters/mysql/__init__.py +0 -0
- nl2sql/adapters/mysql/adapter.py +123 -0
- nl2sql/adapters/postgres/__init__.py +0 -0
- nl2sql/adapters/postgres/adapter.py +115 -0
- nl2sql/adapters/sqlalchemy_base/__init__.py +17 -0
- nl2sql/adapters/sqlalchemy_base/adapter.py +476 -0
- nl2sql/adapters/sqlalchemy_base/models.py +36 -0
- nl2sql/adapters/sqlite/__init__.py +0 -0
- nl2sql/adapters/sqlite/adapter.py +88 -0
- nl2sql/aggregation/__init__.py +3 -0
- nl2sql/aggregation/aggregator.py +98 -0
- nl2sql/aggregation/engines/__init__.py +3 -0
- nl2sql/aggregation/engines/polars_duckdb.py +125 -0
- nl2sql/api/__init__.py +0 -0
- nl2sql/api/auth_api.py +60 -0
- nl2sql/api/benchmark_api.py +114 -0
- nl2sql/api/datasource_api.py +132 -0
- nl2sql/api/indexing_api.py +59 -0
- nl2sql/api/llm_api.py +82 -0
- nl2sql/api/policy_api.py +135 -0
- nl2sql/api/query_api.py +138 -0
- nl2sql/api/result_api.py +24 -0
- nl2sql/api/settings_api.py +65 -0
- nl2sql/auth/__init__.py +8 -0
- nl2sql/auth/models.py +36 -0
- nl2sql/auth/rbac.py +25 -0
- nl2sql/cli/__init__.py +0 -0
- nl2sql/cli/checks.py +53 -0
- nl2sql/cli/commands/__init__.py +0 -0
- nl2sql/cli/commands/benchmark.py +34 -0
- nl2sql/cli/commands/doctor.py +49 -0
- nl2sql/cli/commands/indexing.py +126 -0
- nl2sql/cli/commands/info.py +25 -0
- nl2sql/cli/commands/install.py +27 -0
- nl2sql/cli/commands/policy.py +57 -0
- nl2sql/cli/commands/run.py +166 -0
- nl2sql/cli/commands/setup.py +415 -0
- nl2sql/cli/commands/visualize.py +34 -0
- nl2sql/cli/common/decorators.py +34 -0
- nl2sql/cli/config.py +24 -0
- nl2sql/cli/console.py +52 -0
- nl2sql/cli/demo/__init__.py +1 -0
- nl2sql/cli/demo/data.py +87 -0
- nl2sql/cli/demo/defaults.py +122 -0
- nl2sql/cli/demo/factory.py +289 -0
- nl2sql/cli/demo/manager.py +230 -0
- nl2sql/cli/demo/schemas.py +336 -0
- nl2sql/cli/demo/writers/__init__.py +0 -0
- nl2sql/cli/demo/writers/docker.py +182 -0
- nl2sql/cli/demo/writers/sqlite.py +88 -0
- nl2sql/cli/generators/datasources/__init__.py +3 -0
- nl2sql/cli/generators/datasources/generator.py +24 -0
- nl2sql/cli/generators/datasources/templates.py +7 -0
- nl2sql/cli/generators/env/__init__.py +3 -0
- nl2sql/cli/generators/env/generator.py +46 -0
- nl2sql/cli/generators/env/templates.py +25 -0
- nl2sql/cli/generators/llm/__init__.py +3 -0
- nl2sql/cli/generators/llm/generator.py +24 -0
- nl2sql/cli/generators/llm/templates.py +4 -0
- nl2sql/cli/generators/policies/__init__.py +3 -0
- nl2sql/cli/generators/policies/generator.py +20 -0
- nl2sql/cli/generators/policies/templates.py +2 -0
- nl2sql/cli/main.py +195 -0
- nl2sql/cli/reporting.py +878 -0
- nl2sql/cli/types.py +13 -0
- nl2sql/common/__init__.py +1 -0
- nl2sql/common/cancellation.py +25 -0
- nl2sql/common/context.py +5 -0
- nl2sql/common/errors.py +109 -0
- nl2sql/common/event_logger.py +88 -0
- nl2sql/common/exceptions.py +3 -0
- nl2sql/common/logger.py +119 -0
- nl2sql/common/metrics.py +50 -0
- nl2sql/common/resilience.py +59 -0
- nl2sql/common/settings.py +195 -0
- nl2sql/configs/__init__.py +6 -0
- nl2sql/configs/datasources.py +10 -0
- nl2sql/configs/llm.py +36 -0
- nl2sql/configs/manager.py +176 -0
- nl2sql/configs/policies.py +14 -0
- nl2sql/configs/sample_questions.py +11 -0
- nl2sql/configs/secrets.py +11 -0
- nl2sql/context.py +106 -0
- nl2sql/datasources/__init__.py +21 -0
- nl2sql/datasources/discovery.py +28 -0
- nl2sql/datasources/models.py +21 -0
- nl2sql/datasources/protocols.py +3 -0
- nl2sql/datasources/registry.py +172 -0
- nl2sql/evaluation/__init__.py +6 -0
- nl2sql/evaluation/benchmark_runner.py +320 -0
- nl2sql/evaluation/evaluator.py +134 -0
- nl2sql/evaluation/types.py +22 -0
- nl2sql/execution/__init__.py +4 -0
- nl2sql/execution/artifacts/__init__.py +3 -0
- nl2sql/execution/artifacts/parquet.py +41 -0
- nl2sql/execution/artifacts/store.py +165 -0
- nl2sql/execution/contracts.py +57 -0
- nl2sql/execution/execution_store.py +25 -0
- nl2sql/execution/executor/__init__.py +3 -0
- nl2sql/execution/executor/sql_executor.py +116 -0
- nl2sql/indexing/__init__.py +7 -0
- nl2sql/indexing/chunk_builder.py +227 -0
- nl2sql/indexing/embeddings.py +180 -0
- nl2sql/indexing/enrichment_service.py +316 -0
- nl2sql/indexing/models.py +209 -0
- nl2sql/indexing/orchestrator.py +90 -0
- nl2sql/indexing/vector_store.py +422 -0
- nl2sql/llm/__init__.py +8 -0
- nl2sql/llm/models.py +10 -0
- nl2sql/llm/registry.py +214 -0
- nl2sql/pipeline/__init__.py +1 -0
- nl2sql/pipeline/graph.py +73 -0
- nl2sql/pipeline/graph_utils.py +141 -0
- nl2sql/pipeline/nodes/__init__.py +25 -0
- nl2sql/pipeline/nodes/aggregator/__init__.py +4 -0
- nl2sql/pipeline/nodes/aggregator/node.py +55 -0
- nl2sql/pipeline/nodes/aggregator/prompts.py +20 -0
- nl2sql/pipeline/nodes/aggregator/schemas.py +28 -0
- nl2sql/pipeline/nodes/answer_synthesizer/__init__.py +4 -0
- nl2sql/pipeline/nodes/answer_synthesizer/node.py +98 -0
- nl2sql/pipeline/nodes/answer_synthesizer/prompts.py +19 -0
- nl2sql/pipeline/nodes/answer_synthesizer/schemas.py +24 -0
- nl2sql/pipeline/nodes/ast_planner/__init__.py +4 -0
- nl2sql/pipeline/nodes/ast_planner/node.py +104 -0
- nl2sql/pipeline/nodes/ast_planner/prompts.py +138 -0
- nl2sql/pipeline/nodes/ast_planner/schemas.py +236 -0
- nl2sql/pipeline/nodes/datasource_resolver/__init__.py +4 -0
- nl2sql/pipeline/nodes/datasource_resolver/node.py +253 -0
- nl2sql/pipeline/nodes/datasource_resolver/schemas.py +21 -0
- nl2sql/pipeline/nodes/decomposer/__init__.py +3 -0
- nl2sql/pipeline/nodes/decomposer/node.py +219 -0
- nl2sql/pipeline/nodes/decomposer/prompts.py +96 -0
- nl2sql/pipeline/nodes/decomposer/schemas.py +143 -0
- nl2sql/pipeline/nodes/executor/__init__.py +3 -0
- nl2sql/pipeline/nodes/executor/node.py +107 -0
- nl2sql/pipeline/nodes/generator/__init__.py +4 -0
- nl2sql/pipeline/nodes/generator/node.py +267 -0
- nl2sql/pipeline/nodes/generator/schemas.py +13 -0
- nl2sql/pipeline/nodes/global_planner/__init__.py +4 -0
- nl2sql/pipeline/nodes/global_planner/node.py +186 -0
- nl2sql/pipeline/nodes/global_planner/schemas.py +101 -0
- nl2sql/pipeline/nodes/refiner/__init__.py +4 -0
- nl2sql/pipeline/nodes/refiner/node.py +132 -0
- nl2sql/pipeline/nodes/refiner/prompts.py +28 -0
- nl2sql/pipeline/nodes/refiner/schemas.py +13 -0
- nl2sql/pipeline/nodes/schema_retriever/__init__.py +3 -0
- nl2sql/pipeline/nodes/schema_retriever/node.py +252 -0
- nl2sql/pipeline/nodes/schema_retriever/schema.py +27 -0
- nl2sql/pipeline/nodes/validator/__init__.py +7 -0
- nl2sql/pipeline/nodes/validator/node.py +839 -0
- nl2sql/pipeline/nodes/validator/schemas.py +12 -0
- nl2sql/pipeline/pipeline_runner.py +72 -0
- nl2sql/pipeline/routes.py +72 -0
- nl2sql/pipeline/runtime.py +153 -0
- nl2sql/pipeline/state.py +92 -0
- nl2sql/pipeline/subgraphs/__init__.py +5 -0
- nl2sql/pipeline/subgraphs/schemas.py +23 -0
- nl2sql/pipeline/subgraphs/sql_agent.py +167 -0
- nl2sql/public_api.py +199 -0
- nl2sql/schema/__init__.py +37 -0
- nl2sql/schema/in_memory_store.py +173 -0
- nl2sql/schema/protocol.py +88 -0
- nl2sql/schema/sqlite_store.py +233 -0
- nl2sql/schema/store.py +29 -0
- nl2sql/secrets/__init__.py +14 -0
- nl2sql/secrets/factory.py +85 -0
- nl2sql/secrets/interfaces.py +16 -0
- nl2sql/secrets/manager.py +139 -0
- nl2sql/secrets/models.py +56 -0
- nl2sql/secrets/providers/aws.py +30 -0
- nl2sql/secrets/providers/azure.py +49 -0
- nl2sql/secrets/providers/env.py +8 -0
- nl2sql/secrets/providers/hashi.py +46 -0
- nl2sql/services/__init__.py +0 -0
- nl2sql/services/callbacks/__init__.py +0 -0
- nl2sql/services/callbacks/monitor.py +84 -0
- nl2sql/services/callbacks/node_context.py +7 -0
- nl2sql/services/callbacks/node_handlers.py +187 -0
- nl2sql/services/callbacks/node_metrics.py +14 -0
- nl2sql/services/callbacks/presenter.py +12 -0
- nl2sql/services/callbacks/token_handler.py +56 -0
- nl2sql_engine-0.1.0.dist-info/METADATA +295 -0
- nl2sql_engine-0.1.0.dist-info/RECORD +192 -0
- nl2sql_engine-0.1.0.dist-info/WHEEL +5 -0
- nl2sql_engine-0.1.0.dist-info/entry_points.txt +9 -0
- nl2sql_engine-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
7
|
+
|
|
8
|
+
from nl2sql.schema import SchemaSnapshot
|
|
9
|
+
from nl2sql_adapter_sdk.schema import TableMetadata, ColumnMetadata
|
|
10
|
+
from nl2sql.common.logger import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger("indexing_enrichment")
|
|
13
|
+
|
|
14
|
+
# Name of the agent in configs/llm.yaml that enrichment calls.
|
|
15
|
+
ENRICHMENT_LLM_NAME = "indexing_enrichment"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DatasourceEnrichment(BaseModel):
|
|
19
|
+
description: Optional[str] = None
|
|
20
|
+
domains: List[str] = Field(default_factory=list)
|
|
21
|
+
sample_questions: List[str] = Field(default_factory=list)
|
|
22
|
+
citations: List[str] = Field(default_factory=list)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TableEnrichment(BaseModel):
|
|
26
|
+
description: Optional[str] = None
|
|
27
|
+
citations: List[str] = Field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ColumnEnrichment(BaseModel):
|
|
31
|
+
description: Optional[str] = None
|
|
32
|
+
synonyms: List[str] = Field(default_factory=list)
|
|
33
|
+
citations: List[str] = Field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SchemaEnrichment(BaseModel):
|
|
37
|
+
datasource: DatasourceEnrichment
|
|
38
|
+
tables: Dict[str, TableEnrichment] = Field(default_factory=dict)
|
|
39
|
+
columns: Dict[str, ColumnEnrichment] = Field(default_factory=dict)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
ENRICHMENT_PROMPT = """You are enriching database schema metadata for retrieval.
|
|
43
|
+
Use ONLY the evidence provided. Do not introduce any tables, columns, or facts
|
|
44
|
+
that are not present in evidence. If evidence is insufficient, return empty values.
|
|
45
|
+
|
|
46
|
+
Return a JSON object that matches this schema:
|
|
47
|
+
SchemaEnrichment = {{
|
|
48
|
+
"datasource": {{
|
|
49
|
+
"description": string|null,
|
|
50
|
+
"domains": [string],
|
|
51
|
+
"sample_questions": [string],
|
|
52
|
+
"citations": [string]
|
|
53
|
+
}},
|
|
54
|
+
"tables": {{
|
|
55
|
+
"<table_key>": {{
|
|
56
|
+
"description": string|null,
|
|
57
|
+
"citations": [string]
|
|
58
|
+
}}
|
|
59
|
+
}},
|
|
60
|
+
"columns": {{
|
|
61
|
+
"<table_key>.<column_name>": {{
|
|
62
|
+
"description": string|null,
|
|
63
|
+
"synonyms": [string],
|
|
64
|
+
"citations": [string]
|
|
65
|
+
}}
|
|
66
|
+
}}
|
|
67
|
+
}}
|
|
68
|
+
|
|
69
|
+
Evidence (JSON):
|
|
70
|
+
{evidence_json}
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def build_evidence(
|
|
75
|
+
snapshot: SchemaSnapshot,
|
|
76
|
+
datasource_description: Optional[str],
|
|
77
|
+
existing_questions: List[str],
|
|
78
|
+
) -> Dict[str, object]:
|
|
79
|
+
contract = snapshot.contract
|
|
80
|
+
metadata = snapshot.metadata
|
|
81
|
+
|
|
82
|
+
tables_payload = []
|
|
83
|
+
relationships_payload = []
|
|
84
|
+
|
|
85
|
+
for table_key, table_contract in contract.tables.items():
|
|
86
|
+
table_md = metadata.tables.get(table_key)
|
|
87
|
+
columns_payload = []
|
|
88
|
+
for col_name, col_contract in table_contract.columns.items():
|
|
89
|
+
col_md = table_md.columns.get(col_name) if table_md else None
|
|
90
|
+
stats = col_md.statistics.model_dump() if col_md and col_md.statistics else {}
|
|
91
|
+
columns_payload.append(
|
|
92
|
+
{
|
|
93
|
+
"name": col_name,
|
|
94
|
+
"type": col_contract.data_type,
|
|
95
|
+
"description": col_md.description if col_md else None,
|
|
96
|
+
"statistics": stats,
|
|
97
|
+
"sample_values": stats.get("sample_values", []),
|
|
98
|
+
"pii": bool(col_md.pii) if col_md else False,
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
tables_payload.append(
|
|
103
|
+
{
|
|
104
|
+
"table_key": table_key,
|
|
105
|
+
"description": table_md.description if table_md else None,
|
|
106
|
+
"row_count": table_md.row_count if table_md else None,
|
|
107
|
+
"columns": columns_payload,
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
for fk in table_contract.foreign_keys:
|
|
112
|
+
relationships_payload.append(
|
|
113
|
+
{
|
|
114
|
+
"from_table": table_key,
|
|
115
|
+
"to_table": fk.referred_table.full_name,
|
|
116
|
+
"from_columns": fk.constrained_columns,
|
|
117
|
+
"to_columns": fk.referred_columns,
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
"datasource_id": contract.datasource_id,
|
|
123
|
+
"engine_type": contract.engine_type,
|
|
124
|
+
"datasource_description": datasource_description or metadata.description or "",
|
|
125
|
+
"domains": metadata.domains or [],
|
|
126
|
+
"existing_questions": existing_questions,
|
|
127
|
+
"tables": tables_payload,
|
|
128
|
+
"relationships": relationships_payload,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _normalize_text(value: Optional[str]) -> Optional[str]:
|
|
133
|
+
if value is None:
|
|
134
|
+
return None
|
|
135
|
+
cleaned = value.strip()
|
|
136
|
+
return cleaned or None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _dedupe_list(items: List[str]) -> List[str]:
|
|
140
|
+
seen = set()
|
|
141
|
+
result = []
|
|
142
|
+
for item in items:
|
|
143
|
+
cleaned = item.strip()
|
|
144
|
+
if not cleaned:
|
|
145
|
+
continue
|
|
146
|
+
key = cleaned.lower()
|
|
147
|
+
if key in seen:
|
|
148
|
+
continue
|
|
149
|
+
seen.add(key)
|
|
150
|
+
result.append(cleaned)
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _question_mentions_schema(question: str, table_names: List[str], column_names: List[str]) -> bool:
|
|
155
|
+
text = question.lower()
|
|
156
|
+
return any(name in text for name in table_names + column_names)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def sanitize_enrichment(
|
|
160
|
+
snapshot: SchemaSnapshot,
|
|
161
|
+
enrichment: SchemaEnrichment,
|
|
162
|
+
max_questions: int = 100,
|
|
163
|
+
) -> SchemaEnrichment:
|
|
164
|
+
contract = snapshot.contract
|
|
165
|
+
valid_tables = set(contract.tables.keys())
|
|
166
|
+
valid_columns = set()
|
|
167
|
+
table_names = []
|
|
168
|
+
column_names = []
|
|
169
|
+
|
|
170
|
+
for table_key, table_contract in contract.tables.items():
|
|
171
|
+
table_names.append(table_contract.table.table_name.lower())
|
|
172
|
+
table_names.append(table_key.lower())
|
|
173
|
+
for col_name in table_contract.columns.keys():
|
|
174
|
+
valid_columns.add(f"{table_key}.{col_name}")
|
|
175
|
+
column_names.append(col_name.lower())
|
|
176
|
+
|
|
177
|
+
sanitized_tables: Dict[str, TableEnrichment] = {}
|
|
178
|
+
for table_key, table_enrichment in enrichment.tables.items():
|
|
179
|
+
if table_key not in valid_tables:
|
|
180
|
+
continue
|
|
181
|
+
sanitized_tables[table_key] = TableEnrichment(
|
|
182
|
+
description=_normalize_text(table_enrichment.description),
|
|
183
|
+
citations=_dedupe_list(table_enrichment.citations),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
sanitized_columns: Dict[str, ColumnEnrichment] = {}
|
|
187
|
+
for column_key, column_enrichment in enrichment.columns.items():
|
|
188
|
+
if column_key not in valid_columns:
|
|
189
|
+
continue
|
|
190
|
+
sanitized_columns[column_key] = ColumnEnrichment(
|
|
191
|
+
description=_normalize_text(column_enrichment.description),
|
|
192
|
+
synonyms=_dedupe_list(column_enrichment.synonyms),
|
|
193
|
+
citations=_dedupe_list(column_enrichment.citations),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
questions = [
|
|
197
|
+
q.strip()
|
|
198
|
+
for q in enrichment.datasource.sample_questions
|
|
199
|
+
if q and q.strip()
|
|
200
|
+
]
|
|
201
|
+
questions = [
|
|
202
|
+
q for q in questions if _question_mentions_schema(q, table_names, column_names)
|
|
203
|
+
]
|
|
204
|
+
questions = questions[:max_questions]
|
|
205
|
+
|
|
206
|
+
return SchemaEnrichment(
|
|
207
|
+
datasource=DatasourceEnrichment(
|
|
208
|
+
description=_normalize_text(enrichment.datasource.description),
|
|
209
|
+
domains=_dedupe_list(enrichment.datasource.domains),
|
|
210
|
+
sample_questions=questions,
|
|
211
|
+
citations=_dedupe_list(enrichment.datasource.citations),
|
|
212
|
+
),
|
|
213
|
+
tables=sanitized_tables,
|
|
214
|
+
columns=sanitized_columns,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def apply_enrichment(
|
|
219
|
+
snapshot: SchemaSnapshot,
|
|
220
|
+
enrichment: SchemaEnrichment,
|
|
221
|
+
) -> SchemaSnapshot:
|
|
222
|
+
metadata = snapshot.metadata.model_copy(deep=True)
|
|
223
|
+
|
|
224
|
+
if enrichment.datasource.description:
|
|
225
|
+
metadata.description = enrichment.datasource.description
|
|
226
|
+
if enrichment.datasource.domains:
|
|
227
|
+
metadata.domains = enrichment.datasource.domains
|
|
228
|
+
|
|
229
|
+
for table_key, table_contract in snapshot.contract.tables.items():
|
|
230
|
+
table_md = metadata.tables.get(table_key)
|
|
231
|
+
if not table_md:
|
|
232
|
+
table_md = TableMetadata(table=table_contract.table, columns={})
|
|
233
|
+
metadata.tables[table_key] = table_md
|
|
234
|
+
|
|
235
|
+
table_enrichment = enrichment.tables.get(table_key)
|
|
236
|
+
if table_enrichment and table_enrichment.description:
|
|
237
|
+
table_md.description = table_enrichment.description
|
|
238
|
+
|
|
239
|
+
for col_name in table_contract.columns.keys():
|
|
240
|
+
column_md = table_md.columns.get(col_name)
|
|
241
|
+
if not column_md:
|
|
242
|
+
column_md = ColumnMetadata()
|
|
243
|
+
table_md.columns[col_name] = column_md
|
|
244
|
+
|
|
245
|
+
column_key = f"{table_key}.{col_name}"
|
|
246
|
+
column_enrichment = enrichment.columns.get(column_key)
|
|
247
|
+
if not column_enrichment:
|
|
248
|
+
continue
|
|
249
|
+
if column_enrichment.description:
|
|
250
|
+
column_md.description = column_enrichment.description
|
|
251
|
+
if column_enrichment.synonyms:
|
|
252
|
+
column_md.synonyms = column_enrichment.synonyms
|
|
253
|
+
|
|
254
|
+
return snapshot.model_copy(update={"metadata": metadata})
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def enrich_schema_snapshot(
|
|
258
|
+
snapshot: SchemaSnapshot,
|
|
259
|
+
llm_registry,
|
|
260
|
+
datasource_description: Optional[str],
|
|
261
|
+
existing_questions: List[str],
|
|
262
|
+
max_questions: int = 100,
|
|
263
|
+
) -> Tuple[SchemaSnapshot, List[str]]:
|
|
264
|
+
"""Adds LLM-written descriptions to a schema snapshot, if an LLM is usable.
|
|
265
|
+
|
|
266
|
+
Enrichment is an embellishment, never a prerequisite: indexing has to work
|
|
267
|
+
without any LLM at all. Every step that can involve the model - resolving
|
|
268
|
+
the client, binding structured output, the call itself - therefore sits
|
|
269
|
+
inside one guard, and any failure returns the inputs untouched so the
|
|
270
|
+
caller indexes an unenriched schema instead of failing.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
snapshot: Schema snapshot to enrich.
|
|
274
|
+
llm_registry: Registry the ``indexing_enrichment`` client is built from.
|
|
275
|
+
datasource_description: Operator-supplied description, if any.
|
|
276
|
+
existing_questions: Example questions already configured.
|
|
277
|
+
max_questions: Upper bound on the merged question list.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
The enriched snapshot and merged questions, or the originals unchanged
|
|
281
|
+
when enrichment could not run.
|
|
282
|
+
"""
|
|
283
|
+
datasource_id = snapshot.contract.datasource_id
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
llm = llm_registry.get_llm(ENRICHMENT_LLM_NAME)
|
|
287
|
+
evidence = build_evidence(snapshot, datasource_description, existing_questions)
|
|
288
|
+
prompt = ChatPromptTemplate.from_template(ENRICHMENT_PROMPT)
|
|
289
|
+
chain = prompt | llm.with_structured_output(
|
|
290
|
+
SchemaEnrichment, method="function_calling"
|
|
291
|
+
)
|
|
292
|
+
enrichment = chain.invoke({"evidence_json": evidence})
|
|
293
|
+
except ValueError as exc:
|
|
294
|
+
# No usable LLM is configured - typically no API key. Expected on the
|
|
295
|
+
# key-free path, so it is reported without a stack trace.
|
|
296
|
+
logger.info(
|
|
297
|
+
f"Schema enrichment skipped for '{datasource_id}': {exc} "
|
|
298
|
+
"Indexing continues without LLM-written descriptions."
|
|
299
|
+
)
|
|
300
|
+
return snapshot, existing_questions
|
|
301
|
+
except Exception as exc:
|
|
302
|
+
# Anything else is a genuine surprise and stays fully visible, but it
|
|
303
|
+
# still must not take indexing down with it.
|
|
304
|
+
logger.warning(
|
|
305
|
+
f"Schema enrichment failed for '{datasource_id}': {exc}. "
|
|
306
|
+
"Indexing continues without LLM-written descriptions.",
|
|
307
|
+
exc_info=True,
|
|
308
|
+
)
|
|
309
|
+
return snapshot, existing_questions
|
|
310
|
+
|
|
311
|
+
sanitized = sanitize_enrichment(snapshot, enrichment, max_questions=max_questions)
|
|
312
|
+
updated_snapshot = apply_enrichment(snapshot, sanitized)
|
|
313
|
+
|
|
314
|
+
merged_questions = _dedupe_list(existing_questions + sanitized.datasource.sample_questions)
|
|
315
|
+
merged_questions = merged_questions[:max_questions]
|
|
316
|
+
return updated_snapshot, merged_questions
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import List, Dict, Optional, Literal, Any
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from nl2sql_adapter_sdk.schema import TableRef, ColumnRef
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseChunk(BaseModel):
|
|
8
|
+
"""
|
|
9
|
+
Base class for all schema chunks.
|
|
10
|
+
Chunk IDs MUST be deterministic and stable across runs.
|
|
11
|
+
"""
|
|
12
|
+
id: str
|
|
13
|
+
type: str
|
|
14
|
+
|
|
15
|
+
def get_page_content(self) -> str:
|
|
16
|
+
raise NotImplementedError("Each chunk must implement get_page_content()")
|
|
17
|
+
|
|
18
|
+
def get_metadata(self) -> Dict[str, Any]:
|
|
19
|
+
return {
|
|
20
|
+
"id": self.id,
|
|
21
|
+
"type": self.type,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DatasourceChunk(BaseChunk):
|
|
26
|
+
type: Literal["schema.datasource"] = Field(
|
|
27
|
+
default="schema.datasource", frozen=True
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
datasource_id: str
|
|
31
|
+
description: str
|
|
32
|
+
domains: Optional[List[str]] = None
|
|
33
|
+
schema_version: str
|
|
34
|
+
examples: Optional[List[str]] = None
|
|
35
|
+
|
|
36
|
+
def get_page_content(self) -> str:
|
|
37
|
+
domains = ", ".join(self.domains) if self.domains else "N/A"
|
|
38
|
+
examples = ", ".join(self.examples) if self.examples else "N/A"
|
|
39
|
+
return (
|
|
40
|
+
f"Datasource: {self.datasource_id}\n"
|
|
41
|
+
f"{self.description}\n"
|
|
42
|
+
f"Domains: {domains}\n"
|
|
43
|
+
f"Examples: {examples}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def get_metadata(self) -> Dict[str, Any]:
|
|
47
|
+
return {
|
|
48
|
+
**super().get_metadata(),
|
|
49
|
+
"datasource_id": self.datasource_id,
|
|
50
|
+
"schema_version": self.schema_version,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TableChunk(BaseChunk):
|
|
55
|
+
type: Literal["schema.table"] = Field(
|
|
56
|
+
default="schema.table", frozen=True
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
datasource_id: str
|
|
60
|
+
table: TableRef
|
|
61
|
+
description: Optional[str] = None
|
|
62
|
+
primary_key: List[str] = Field(default_factory=list)
|
|
63
|
+
columns: List[str] = Field(default_factory=list)
|
|
64
|
+
foreign_keys: List[str] = Field(
|
|
65
|
+
default_factory=list,
|
|
66
|
+
description="Human-readable FK summaries (retrieval only)"
|
|
67
|
+
)
|
|
68
|
+
row_count: Optional[int] = None
|
|
69
|
+
schema_version: str
|
|
70
|
+
|
|
71
|
+
def get_page_content(self) -> str:
|
|
72
|
+
pk = ", ".join(self.primary_key) if self.primary_key else "None"
|
|
73
|
+
column_list = ", ".join(self.columns) if self.columns else "None"
|
|
74
|
+
return (
|
|
75
|
+
f"Table: {self.table.full_name}\n"
|
|
76
|
+
f"{self.description or ''}\n"
|
|
77
|
+
f"Primary Key: {pk}\n"
|
|
78
|
+
f"Columns: {column_list}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def get_metadata(self) -> Dict[str, Any]:
|
|
82
|
+
return {
|
|
83
|
+
**super().get_metadata(),
|
|
84
|
+
"datasource_id": self.datasource_id,
|
|
85
|
+
"table": self.table.full_name,
|
|
86
|
+
"row_count": self.row_count,
|
|
87
|
+
"schema_version": self.schema_version,
|
|
88
|
+
"description": self.description,
|
|
89
|
+
"primary_key": ','.join(self.primary_key),
|
|
90
|
+
"foreign_keys": ','.join(self.foreign_keys),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ColumnChunk(BaseChunk):
|
|
95
|
+
type: Literal["schema.column"] = Field(
|
|
96
|
+
default="schema.column", frozen=True
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
datasource_id: str
|
|
100
|
+
column: ColumnRef
|
|
101
|
+
dtype: str
|
|
102
|
+
description: Optional[str] = None
|
|
103
|
+
column_stats: Dict[str, Any] = Field(default_factory=dict)
|
|
104
|
+
synonyms: Optional[List[str]] = None
|
|
105
|
+
pii: bool = False
|
|
106
|
+
schema_version: str
|
|
107
|
+
|
|
108
|
+
def get_page_content(self) -> str:
|
|
109
|
+
stats = f"Stats: {self.column_stats}" if self.column_stats else ""
|
|
110
|
+
synonyms = (
|
|
111
|
+
f"Synonyms: {', '.join(self.synonyms)}"
|
|
112
|
+
if self.synonyms
|
|
113
|
+
else ""
|
|
114
|
+
)
|
|
115
|
+
return (
|
|
116
|
+
f"Table: {self.column.table.full_name}\n"
|
|
117
|
+
f"Column: {self.column.column_name}\n"
|
|
118
|
+
f"Type: {self.dtype}\n"
|
|
119
|
+
f"{self.description or ''}\n"
|
|
120
|
+
f"{stats}\n"
|
|
121
|
+
f"{synonyms}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def get_metadata(self) -> Dict[str, Any]:
|
|
125
|
+
return {
|
|
126
|
+
**super().get_metadata(),
|
|
127
|
+
"datasource_id": self.datasource_id,
|
|
128
|
+
"column": self.column.column_name,
|
|
129
|
+
"table": self.column.table.full_name,
|
|
130
|
+
"dtype": self.dtype,
|
|
131
|
+
"pii": self.pii,
|
|
132
|
+
"schema_version": self.schema_version,
|
|
133
|
+
"description": self.description,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
class RelationshipChunk(BaseChunk):
|
|
137
|
+
type: Literal["schema.relationship"] = Field(
|
|
138
|
+
default="schema.relationship", frozen=True
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
datasource_id: str
|
|
142
|
+
from_table: TableRef
|
|
143
|
+
to_table: TableRef
|
|
144
|
+
|
|
145
|
+
from_columns: Optional[List[str]] = None
|
|
146
|
+
to_columns: Optional[List[str]] = None
|
|
147
|
+
|
|
148
|
+
cardinality: Literal[
|
|
149
|
+
"one-to-one",
|
|
150
|
+
"one-to-many",
|
|
151
|
+
"many-to-one",
|
|
152
|
+
"many-to-many",
|
|
153
|
+
"unknown"
|
|
154
|
+
] = "unknown"
|
|
155
|
+
|
|
156
|
+
business_meaning: Optional[str] = None
|
|
157
|
+
schema_version: str
|
|
158
|
+
|
|
159
|
+
def get_page_content(self) -> str:
|
|
160
|
+
return (
|
|
161
|
+
f"Relationship between {self.from_table.full_name} "
|
|
162
|
+
f"and {self.to_table.full_name}.\n"
|
|
163
|
+
f"{self.business_meaning or ''}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def get_metadata(self) -> Dict[str, Any]:
|
|
167
|
+
return {
|
|
168
|
+
**super().get_metadata(),
|
|
169
|
+
"datasource_id": self.datasource_id,
|
|
170
|
+
"from_table": self.from_table.full_name,
|
|
171
|
+
"to_table": self.to_table.full_name,
|
|
172
|
+
"table": self.from_table.full_name,
|
|
173
|
+
"from_columns": self.from_columns or [],
|
|
174
|
+
"to_columns": self.to_columns or [],
|
|
175
|
+
"cardinality": self.cardinality,
|
|
176
|
+
"schema_version": self.schema_version,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class MetricChunk(BaseChunk):
|
|
181
|
+
type: Literal["schema.metric"] = Field(
|
|
182
|
+
default="schema.metric", frozen=True
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
datasource_id: str
|
|
186
|
+
name: str
|
|
187
|
+
definition: Optional[str] = None
|
|
188
|
+
grain: Optional[str] = None
|
|
189
|
+
business_meaning: Optional[str] = None
|
|
190
|
+
owner: Optional[str] = None
|
|
191
|
+
version: str = "v1"
|
|
192
|
+
schema_version: str
|
|
193
|
+
|
|
194
|
+
def get_page_content(self) -> str:
|
|
195
|
+
return (
|
|
196
|
+
f"Metric: {self.name}\n"
|
|
197
|
+
f"{self.business_meaning or ''}\n"
|
|
198
|
+
f"Definition: {self.definition or ''}\n"
|
|
199
|
+
f"Grain: {self.grain or 'N/A'}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def get_metadata(self) -> Dict[str, Any]:
|
|
203
|
+
return {
|
|
204
|
+
**super().get_metadata(),
|
|
205
|
+
"datasource_id": self.datasource_id,
|
|
206
|
+
"name": self.name,
|
|
207
|
+
"version": self.version,
|
|
208
|
+
"schema_version": self.schema_version,
|
|
209
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from nl2sql.datasources.protocols import DatasourceAdapterProtocol
|
|
6
|
+
from nl2sql.common.logger import get_logger
|
|
7
|
+
from nl2sql.indexing.chunk_builder import SchemaChunkBuilder
|
|
8
|
+
from nl2sql.indexing.enrichment_service import enrich_schema_snapshot
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from nl2sql.context import NL2SQLContext
|
|
12
|
+
|
|
13
|
+
logger = get_logger("indexing_orchestrator")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class IndexingOrchestrator:
|
|
17
|
+
"""
|
|
18
|
+
Orchestrates schema indexing for datasources.
|
|
19
|
+
|
|
20
|
+
This class coordinates schema snapshot retrieval, schema version
|
|
21
|
+
registration, chunk construction, and vector store refresh.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, ctx: NL2SQLContext):
|
|
25
|
+
"""
|
|
26
|
+
Initializes the indexing orchestrator.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
ctx: Initialized NL2SQLContext.
|
|
30
|
+
"""
|
|
31
|
+
self.vector_store = ctx.vector_store
|
|
32
|
+
self.schema_store = ctx.schema_store
|
|
33
|
+
self.config_manager = ctx.config_manager
|
|
34
|
+
self.llm_registry = ctx.llm_registry
|
|
35
|
+
|
|
36
|
+
def clear_store(self) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Clears the vector store.
|
|
39
|
+
"""
|
|
40
|
+
self.vector_store.clear()
|
|
41
|
+
|
|
42
|
+
def index_datasource(
|
|
43
|
+
self,
|
|
44
|
+
adapter: DatasourceAdapterProtocol,
|
|
45
|
+
) -> Dict[str, int]:
|
|
46
|
+
"""
|
|
47
|
+
Indexes schema chunks for a datasource.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
adapter: SQLAlchemy adapter for the datasource.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Indexing statistics by chunk type.
|
|
54
|
+
"""
|
|
55
|
+
schema_snapshot = adapter.fetch_schema_snapshot()
|
|
56
|
+
|
|
57
|
+
questions = self.config_manager.get_example_questions(adapter.datasource_id)
|
|
58
|
+
datasource_description = self.config_manager.get_datasource_description(
|
|
59
|
+
adapter.datasource_id
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Best-effort: enrich_schema_snapshot owns resolving its own LLM and
|
|
63
|
+
# degrades to the unenriched snapshot when none is usable, so indexing
|
|
64
|
+
# never depends on an API key being present.
|
|
65
|
+
schema_snapshot, questions = enrich_schema_snapshot(
|
|
66
|
+
snapshot=schema_snapshot,
|
|
67
|
+
llm_registry=self.llm_registry,
|
|
68
|
+
datasource_description=datasource_description,
|
|
69
|
+
existing_questions=questions,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
schema_version, evicted_versions = self.schema_store.register_snapshot(
|
|
73
|
+
schema_snapshot
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
chunk_builder = SchemaChunkBuilder(
|
|
77
|
+
ds_id=adapter.datasource_id,
|
|
78
|
+
schema_snapshot=schema_snapshot,
|
|
79
|
+
schema_version=schema_version,
|
|
80
|
+
questions=questions,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
chunks = chunk_builder.build()
|
|
84
|
+
|
|
85
|
+
return self.vector_store.refresh_schema_chunks(
|
|
86
|
+
datasource_id=adapter.datasource_id,
|
|
87
|
+
schema_version=schema_version,
|
|
88
|
+
chunks=chunks,
|
|
89
|
+
evicted_versions=evicted_versions,
|
|
90
|
+
)
|