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,422 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional, Dict, Any
|
|
4
|
+
|
|
5
|
+
from langchain_chroma import Chroma
|
|
6
|
+
from langchain_core.documents import Document
|
|
7
|
+
from langchain_core.embeddings import Embeddings
|
|
8
|
+
|
|
9
|
+
from nl2sql.indexing.embeddings import (
|
|
10
|
+
PROVIDER_BY_DIMENSION,
|
|
11
|
+
EmbeddingService,
|
|
12
|
+
describe_embeddings,
|
|
13
|
+
)
|
|
14
|
+
from nl2sql.common.exceptions import NL2SQLError
|
|
15
|
+
from nl2sql.common.logger import get_logger
|
|
16
|
+
from .models import BaseChunk
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EmbeddingDimensionMismatchError(NL2SQLError):
|
|
22
|
+
"""Raised when a persisted collection was built with a different embedder."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def check_embedding_dimension_compatibility(
|
|
26
|
+
persisted_dimension: Optional[int],
|
|
27
|
+
embeddings: Embeddings,
|
|
28
|
+
collection_name: str,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Verifies that a persisted collection can be queried with the current embedder.
|
|
32
|
+
|
|
33
|
+
Vectors from different embedding providers have different dimensionality
|
|
34
|
+
(OpenAI ``text-embedding-3-small`` is 1536, the local model is 384), so an
|
|
35
|
+
index built with one provider cannot be read with the other.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
persisted_dimension: Dimensionality of the persisted vectors, or None
|
|
39
|
+
when the collection is empty or the dimension cannot be determined.
|
|
40
|
+
embeddings: Embedding implementation configured for this process.
|
|
41
|
+
collection_name: Name of the Chroma collection.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
EmbeddingDimensionMismatchError: If the dimensions disagree.
|
|
45
|
+
"""
|
|
46
|
+
provider, model, expected_dimension = describe_embeddings(embeddings)
|
|
47
|
+
|
|
48
|
+
if persisted_dimension is None or expected_dimension is None:
|
|
49
|
+
return
|
|
50
|
+
if persisted_dimension == expected_dimension:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
built_with = PROVIDER_BY_DIMENSION.get(persisted_dimension)
|
|
54
|
+
origin = (
|
|
55
|
+
f"the '{built_with}' embedding provider"
|
|
56
|
+
if built_with
|
|
57
|
+
else "a different embedding provider"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
raise EmbeddingDimensionMismatchError(
|
|
61
|
+
f"Vector store collection '{collection_name}' was indexed with {origin} "
|
|
62
|
+
f"({persisted_dimension}-dimensional vectors), but the configured provider "
|
|
63
|
+
f"is '{provider}' ({model}, {expected_dimension}-dimensional vectors). "
|
|
64
|
+
"A vector index cannot be shared across embedding providers: re-index with "
|
|
65
|
+
"'nl2sql index' after changing EMBEDDING_PROVIDER, or point VECTOR_STORE at "
|
|
66
|
+
"a separate directory per provider."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class VectorStore:
|
|
71
|
+
"""
|
|
72
|
+
Vector store for NL2SQL orchestration.
|
|
73
|
+
|
|
74
|
+
This store indexes schema chunks and provides staged retrieval
|
|
75
|
+
for datasource routing, schema grounding, and planning context.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
collection_name: str,
|
|
81
|
+
persist_directory: str,
|
|
82
|
+
embeddings: Optional[Embeddings] = None,
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
Initializes the vector store.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
collection_name: Name of the Chroma collection.
|
|
89
|
+
persist_directory: Directory used for persistence.
|
|
90
|
+
embeddings: Embedding implementation to use.
|
|
91
|
+
"""
|
|
92
|
+
self.collection_name = collection_name
|
|
93
|
+
self.embeddings = embeddings or EmbeddingService.get_embeddings()
|
|
94
|
+
self.persist_directory = persist_directory
|
|
95
|
+
self._initialize_vector_store()
|
|
96
|
+
|
|
97
|
+
def _initialize_vector_store(self) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Initializes the underlying Chroma vector store.
|
|
100
|
+
"""
|
|
101
|
+
self.vectorstore = Chroma(
|
|
102
|
+
collection_name=self.collection_name,
|
|
103
|
+
embedding_function=self.embeddings,
|
|
104
|
+
persist_directory=self.persist_directory,
|
|
105
|
+
)
|
|
106
|
+
self._dimension_checked = False
|
|
107
|
+
|
|
108
|
+
def _verify_embedding_dimensions(self) -> None:
|
|
109
|
+
"""
|
|
110
|
+
Checks the persisted vectors against the configured embedder once per
|
|
111
|
+
store instance.
|
|
112
|
+
|
|
113
|
+
The check runs on the read path only: indexing clears the collection
|
|
114
|
+
before writing, so a provider switch is fixed by re-running
|
|
115
|
+
``nl2sql index`` rather than blocked by it.
|
|
116
|
+
"""
|
|
117
|
+
if self._dimension_checked:
|
|
118
|
+
return
|
|
119
|
+
check_embedding_dimension_compatibility(
|
|
120
|
+
self._persisted_dimension(),
|
|
121
|
+
self.embeddings,
|
|
122
|
+
self.collection_name,
|
|
123
|
+
)
|
|
124
|
+
self._dimension_checked = True
|
|
125
|
+
|
|
126
|
+
def _persisted_dimension(self) -> Optional[int]:
|
|
127
|
+
"""
|
|
128
|
+
Reads the dimensionality of the vectors already persisted in the collection.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Vector dimensionality, or None when the collection is empty or the
|
|
132
|
+
dimension cannot be determined.
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
stored = self.vectorstore._collection.peek(limit=1).get("embeddings")
|
|
136
|
+
if stored is None or len(stored) == 0:
|
|
137
|
+
return None
|
|
138
|
+
return len(stored[0])
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
logger.debug(f"Could not determine persisted embedding dimension: {exc}")
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def initialize_if_not_exists(self) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Initializes the vector store if it does not exist.
|
|
146
|
+
"""
|
|
147
|
+
try:
|
|
148
|
+
_ = self.vectorstore._collection.count()
|
|
149
|
+
except Exception:
|
|
150
|
+
logger.info("Vector store not found, initializing new store.")
|
|
151
|
+
self._initialize_vector_store()
|
|
152
|
+
|
|
153
|
+
self._verify_embedding_dimensions()
|
|
154
|
+
|
|
155
|
+
def is_empty(self) -> bool:
|
|
156
|
+
"""
|
|
157
|
+
Checks whether the vector store is empty.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
True if the store contains no documents.
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
return self.vectorstore._collection.count() == 0
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
logger.error(f"Failed to check vector store state: {exc}")
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
def clear(self) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Deletes the entire vector collection.
|
|
171
|
+
"""
|
|
172
|
+
try:
|
|
173
|
+
self.vectorstore.delete_collection()
|
|
174
|
+
self._initialize_vector_store()
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
logger.error(f"Failed to clear vector store: {exc}")
|
|
177
|
+
|
|
178
|
+
def delete_documents(self, filter: Dict[str, Any]) -> None:
|
|
179
|
+
"""
|
|
180
|
+
Deletes documents matching a metadata filter.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
filter: Metadata filter used for deletion.
|
|
184
|
+
"""
|
|
185
|
+
try:
|
|
186
|
+
where = (
|
|
187
|
+
{"$and": [{k: v} for k, v in filter.items()]}
|
|
188
|
+
if len(filter) > 1
|
|
189
|
+
else filter
|
|
190
|
+
)
|
|
191
|
+
self.vectorstore._collection.delete(where=where)
|
|
192
|
+
except Exception as exc:
|
|
193
|
+
logger.error(f"Failed to delete documents: {exc}")
|
|
194
|
+
|
|
195
|
+
def refresh_schema_chunks(
|
|
196
|
+
self,
|
|
197
|
+
datasource_id: str,
|
|
198
|
+
schema_version: str,
|
|
199
|
+
chunks: List[BaseChunk],
|
|
200
|
+
evicted_versions: List[str],
|
|
201
|
+
) -> Dict[str, int]:
|
|
202
|
+
"""
|
|
203
|
+
Indexes schema chunks for a datasource and evicts old versions.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
datasource_id: Datasource identifier.
|
|
207
|
+
schema_version: Active schema version.
|
|
208
|
+
chunks: Schema chunks to index.
|
|
209
|
+
evicted_versions: Schema versions to remove.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Indexing statistics by chunk type.
|
|
213
|
+
"""
|
|
214
|
+
self._delete_evicted_versions(datasource_id, evicted_versions)
|
|
215
|
+
|
|
216
|
+
self.delete_documents(
|
|
217
|
+
{
|
|
218
|
+
"datasource_id": datasource_id,
|
|
219
|
+
"schema_version": schema_version,
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
documents = self._prepare_chunk_documents(chunks)
|
|
224
|
+
|
|
225
|
+
if documents:
|
|
226
|
+
self.vectorstore.add_documents(documents)
|
|
227
|
+
|
|
228
|
+
stats: Dict[str, Any] = {}
|
|
229
|
+
stats["datasource_id"] = datasource_id
|
|
230
|
+
stats["schema_version"] = schema_version
|
|
231
|
+
for chunk in chunks:
|
|
232
|
+
stats[chunk.type] = stats.get(chunk.type, 0) + 1
|
|
233
|
+
|
|
234
|
+
return stats
|
|
235
|
+
|
|
236
|
+
def _delete_evicted_versions(
|
|
237
|
+
self,
|
|
238
|
+
datasource_id: str,
|
|
239
|
+
evicted_versions: List[str],
|
|
240
|
+
) -> None:
|
|
241
|
+
"""
|
|
242
|
+
Deletes all documents belonging to evicted schema versions.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
datasource_id: Datasource identifier.
|
|
246
|
+
evicted_versions: Schema versions to remove.
|
|
247
|
+
"""
|
|
248
|
+
for version in evicted_versions:
|
|
249
|
+
self.delete_documents(
|
|
250
|
+
{
|
|
251
|
+
"datasource_id": datasource_id,
|
|
252
|
+
"schema_version": version,
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def _prepare_chunk_documents(
|
|
257
|
+
self,
|
|
258
|
+
chunks: List[BaseChunk],
|
|
259
|
+
) -> List[Document]:
|
|
260
|
+
"""
|
|
261
|
+
Converts schema chunks into vector documents.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
chunks: Schema chunks to convert.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
List of vector documents.
|
|
268
|
+
"""
|
|
269
|
+
return [
|
|
270
|
+
Document(
|
|
271
|
+
page_content=chunk.get_page_content(),
|
|
272
|
+
metadata=chunk.get_metadata(),
|
|
273
|
+
)
|
|
274
|
+
for chunk in chunks
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
def retrieve_datasource_candidates(
|
|
278
|
+
self,
|
|
279
|
+
query: str,
|
|
280
|
+
k: int = 3,
|
|
281
|
+
) -> List[Document]:
|
|
282
|
+
"""
|
|
283
|
+
Retrieves candidate datasources for a user query.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
query: User query.
|
|
287
|
+
k: Number of datasource candidates to retrieve.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Retrieved datasource documents.
|
|
291
|
+
"""
|
|
292
|
+
self.initialize_if_not_exists()
|
|
293
|
+
from nl2sql.common.resilience import VECTOR_BREAKER
|
|
294
|
+
|
|
295
|
+
@VECTOR_BREAKER
|
|
296
|
+
def _execute():
|
|
297
|
+
return self.vectorstore.max_marginal_relevance_search(
|
|
298
|
+
query,
|
|
299
|
+
k=k,
|
|
300
|
+
fetch_k=k * 4,
|
|
301
|
+
lambda_mult=0.7,
|
|
302
|
+
filter={"type": "schema.datasource"},
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return _execute()
|
|
306
|
+
|
|
307
|
+
def retrieve_schema_context(
|
|
308
|
+
self,
|
|
309
|
+
query: str,
|
|
310
|
+
datasource_id: str,
|
|
311
|
+
k: int = 8,
|
|
312
|
+
) -> List[Document]:
|
|
313
|
+
"""
|
|
314
|
+
Retrieves schema-level context for a datasource.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
query: User query.
|
|
318
|
+
datasource_id: Selected datasource identifier.
|
|
319
|
+
k: Number of schema documents to retrieve.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Retrieved schema documents.
|
|
323
|
+
"""
|
|
324
|
+
self.initialize_if_not_exists()
|
|
325
|
+
from nl2sql.common.resilience import VECTOR_BREAKER
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@VECTOR_BREAKER
|
|
329
|
+
def _execute():
|
|
330
|
+
return self.vectorstore.max_marginal_relevance_search(
|
|
331
|
+
query,
|
|
332
|
+
k=k,
|
|
333
|
+
fetch_k=k * 4,
|
|
334
|
+
lambda_mult=0.7,
|
|
335
|
+
filter={
|
|
336
|
+
"$and": [
|
|
337
|
+
{"datasource_id": datasource_id},
|
|
338
|
+
{"type": {"$in": ["schema.table", "schema.metric"]}},
|
|
339
|
+
]
|
|
340
|
+
},
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return _execute()
|
|
344
|
+
|
|
345
|
+
def retrieve_column_candidates(
|
|
346
|
+
self,
|
|
347
|
+
query: str,
|
|
348
|
+
datasource_id: str,
|
|
349
|
+
k: int = 8,
|
|
350
|
+
) -> List[Document]:
|
|
351
|
+
"""
|
|
352
|
+
Retrieves candidate column documents for a datasource.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
query: User query.
|
|
356
|
+
datasource_id: Selected datasource identifier.
|
|
357
|
+
k: Number of column documents to retrieve.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Retrieved column documents.
|
|
361
|
+
"""
|
|
362
|
+
from nl2sql.common.resilience import VECTOR_BREAKER
|
|
363
|
+
|
|
364
|
+
self.initialize_if_not_exists()
|
|
365
|
+
|
|
366
|
+
@VECTOR_BREAKER
|
|
367
|
+
def _execute():
|
|
368
|
+
return self.vectorstore.max_marginal_relevance_search(
|
|
369
|
+
query,
|
|
370
|
+
k=k,
|
|
371
|
+
fetch_k=k * 4,
|
|
372
|
+
lambda_mult=0.7,
|
|
373
|
+
filter={
|
|
374
|
+
"$and": [
|
|
375
|
+
{"datasource_id": datasource_id},
|
|
376
|
+
{"type": "schema.column"},
|
|
377
|
+
]
|
|
378
|
+
},
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
return _execute()
|
|
382
|
+
|
|
383
|
+
def retrieve_planning_context(
|
|
384
|
+
self,
|
|
385
|
+
query: str,
|
|
386
|
+
datasource_id: str,
|
|
387
|
+
tables: List[str],
|
|
388
|
+
k: int = 12,
|
|
389
|
+
) -> List[Document]:
|
|
390
|
+
"""
|
|
391
|
+
Retrieves planning-level context for selected tables.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
query: User query.
|
|
395
|
+
datasource_id: Selected datasource identifier.
|
|
396
|
+
tables: Fully qualified table names.
|
|
397
|
+
k: Number of planning documents to retrieve.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Retrieved planning documents.
|
|
401
|
+
"""
|
|
402
|
+
from nl2sql.common.resilience import VECTOR_BREAKER
|
|
403
|
+
|
|
404
|
+
self.initialize_if_not_exists()
|
|
405
|
+
|
|
406
|
+
@VECTOR_BREAKER
|
|
407
|
+
def _execute():
|
|
408
|
+
return self.vectorstore.max_marginal_relevance_search(
|
|
409
|
+
query,
|
|
410
|
+
k=k,
|
|
411
|
+
fetch_k=k * 4,
|
|
412
|
+
lambda_mult=0.7,
|
|
413
|
+
filter={
|
|
414
|
+
"$and": [
|
|
415
|
+
{"datasource_id": datasource_id},
|
|
416
|
+
{"type": {"$in": ["schema.column", "schema.relationship"]}},
|
|
417
|
+
{"table": {"$in": tables}},
|
|
418
|
+
]
|
|
419
|
+
},
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
return _execute()
|
nl2sql/llm/__init__.py
ADDED
nl2sql/llm/models.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Re-export of the canonical ``AgentConfig``.
|
|
2
|
+
|
|
3
|
+
The model lives in :mod:`nl2sql.configs.llm`, which owns the on-disk file
|
|
4
|
+
schemas. This module keeps the ``nl2sql.llm.models`` import path working for
|
|
5
|
+
``LLMRegistry`` and existing callers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from nl2sql.configs.llm import AgentConfig
|
|
9
|
+
|
|
10
|
+
__all__ = ["AgentConfig"]
|
nl2sql/llm/registry.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from threading import RLock
|
|
3
|
+
from typing import Any, Dict, NamedTuple, Optional
|
|
4
|
+
|
|
5
|
+
from langchain_openai import ChatOpenAI
|
|
6
|
+
|
|
7
|
+
from nl2sql.secrets import SecretManager
|
|
8
|
+
from .models import AgentConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProviderPreset(NamedTuple):
|
|
12
|
+
"""Endpoint and credential defaults for one OpenAI-compatible provider.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
base_url: Endpoint the provider is reached on, or None to let the
|
|
16
|
+
OpenAI client resolve its own default.
|
|
17
|
+
api_key_env: Environment variable named in errors and used as the
|
|
18
|
+
last-resort source of the key.
|
|
19
|
+
api_key_placeholder: Stand-in key for providers that authenticate
|
|
20
|
+
nothing. Non-None means the provider needs no real credential.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
base_url: Optional[str]
|
|
24
|
+
api_key_env: Optional[str]
|
|
25
|
+
api_key_placeholder: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# OpenAI, OpenRouter and Ollama all speak the OpenAI wire protocol, so a single
|
|
29
|
+
# ChatOpenAI client serves all three; only the endpoint differs. A
|
|
30
|
+
# config-supplied ``base_url`` overrides the preset, which is what lets the same
|
|
31
|
+
# path serve vLLM, LiteLLM or any other OpenAI-compatible endpoint.
|
|
32
|
+
#
|
|
33
|
+
# Ollama needs no credential, but ChatOpenAI refuses to construct without an
|
|
34
|
+
# ``api_key`` (``openai.OpenAIError: Missing credentials``), so its preset
|
|
35
|
+
# supplies a placeholder the local daemon ignores.
|
|
36
|
+
PROVIDER_PRESETS: Dict[str, ProviderPreset] = {
|
|
37
|
+
"openai": ProviderPreset(
|
|
38
|
+
base_url=None,
|
|
39
|
+
api_key_env="OPENAI_API_KEY",
|
|
40
|
+
),
|
|
41
|
+
"openrouter": ProviderPreset(
|
|
42
|
+
base_url="https://openrouter.ai/api/v1",
|
|
43
|
+
api_key_env="OPENROUTER_API_KEY",
|
|
44
|
+
),
|
|
45
|
+
"ollama": ProviderPreset(
|
|
46
|
+
base_url="http://localhost:11434/v1",
|
|
47
|
+
api_key_env=None,
|
|
48
|
+
api_key_placeholder="ollama",
|
|
49
|
+
),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class LLMRegistry:
|
|
54
|
+
|
|
55
|
+
def __init__(self, secret_manager: SecretManager):
|
|
56
|
+
self.secret_manager = secret_manager
|
|
57
|
+
self.llms = {}
|
|
58
|
+
self._configs: Dict[str, AgentConfig] = {}
|
|
59
|
+
self._lock = RLock()
|
|
60
|
+
|
|
61
|
+
def register_llms(self, config: Dict[str, AgentConfig]):
|
|
62
|
+
for agent in config.values():
|
|
63
|
+
self.register_llm(agent)
|
|
64
|
+
|
|
65
|
+
def register_llm(self, agent: AgentConfig):
|
|
66
|
+
"""Validates an agent's configuration and records it for later use.
|
|
67
|
+
|
|
68
|
+
The client itself is built on first ``get_llm`` so that constructing a
|
|
69
|
+
context does not require credentials for every configured agent. What is
|
|
70
|
+
genuinely misconfiguration - an unknown provider, a missing model - still
|
|
71
|
+
fails here rather than mid-query.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
agent: Configuration for one agent.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ValueError: If the provider is unknown or the model is empty.
|
|
78
|
+
"""
|
|
79
|
+
if agent.provider not in PROVIDER_PRESETS:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Unsupported LLM provider: {agent.provider}. "
|
|
82
|
+
f"Valid providers are: {', '.join(sorted(PROVIDER_PRESETS))}."
|
|
83
|
+
)
|
|
84
|
+
if not agent.model or not agent.model.strip():
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"LLM agent '{agent.name}' has no model configured. "
|
|
87
|
+
"Set 'model' for it in configs/llm.yaml."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
with self._lock:
|
|
91
|
+
self._configs[agent.name] = agent
|
|
92
|
+
# A re-registration replaces any client built from the old config.
|
|
93
|
+
self.llms.pop(agent.name, None)
|
|
94
|
+
|
|
95
|
+
def get_llm(self, name: str) -> ChatOpenAI:
|
|
96
|
+
"""Returns the client for an agent, building it on first use.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
name: Agent name; falls back to the 'default' agent.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
ChatOpenAI: The cached client for that agent.
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
ValueError: If neither the named agent nor a 'default' agent is
|
|
106
|
+
registered, or if the provider needs an API key that cannot be
|
|
107
|
+
resolved.
|
|
108
|
+
"""
|
|
109
|
+
with self._lock:
|
|
110
|
+
if name in self.llms:
|
|
111
|
+
return self.llms[name]
|
|
112
|
+
|
|
113
|
+
config = self._configs.get(name) or self._configs.get("default")
|
|
114
|
+
if config is None:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"No LLM named '{name}' is configured and no 'default' LLM has "
|
|
117
|
+
"been registered. Add it to configs/llm.yaml (under 'agents', or "
|
|
118
|
+
"as the 'default' agent)."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if config.name in self.llms:
|
|
122
|
+
return self.llms[config.name]
|
|
123
|
+
|
|
124
|
+
client = self._build_client(config)
|
|
125
|
+
self.llms[config.name] = client
|
|
126
|
+
return client
|
|
127
|
+
|
|
128
|
+
def _build_client(self, agent: AgentConfig) -> ChatOpenAI:
|
|
129
|
+
"""Builds the ChatOpenAI client for one agent from its provider preset.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
agent: Validated configuration for the agent.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
ChatOpenAI: A client pointed at the configured endpoint.
|
|
136
|
+
"""
|
|
137
|
+
preset = PROVIDER_PRESETS[agent.provider]
|
|
138
|
+
api_key = self._resolve_api_key(agent, preset)
|
|
139
|
+
|
|
140
|
+
base_url = agent.base_url or preset.base_url
|
|
141
|
+
kwargs = {"base_url": base_url} if base_url else {}
|
|
142
|
+
|
|
143
|
+
return ChatOpenAI(
|
|
144
|
+
model=agent.model,
|
|
145
|
+
api_key=api_key,
|
|
146
|
+
temperature=agent.temperature,
|
|
147
|
+
tags=[agent.name],
|
|
148
|
+
seed=42,
|
|
149
|
+
**kwargs,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def _resolve_api_key(self, agent: AgentConfig, preset: ProviderPreset):
|
|
153
|
+
"""Resolves the API key for an agent, or explains what is missing.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
agent: Configuration for the agent.
|
|
157
|
+
preset: Preset for the agent's provider.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
The resolved key, the provider's placeholder for key-free providers,
|
|
161
|
+
or the value of the provider's environment variable.
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
ValueError: If the provider requires a key and none can be found.
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
resolved = self.secret_manager.resolve_object(agent.api_key)
|
|
168
|
+
except ValueError:
|
|
169
|
+
# An unresolvable "${env:...}" reference is the same situation as no
|
|
170
|
+
# key at all, and is reported as such below.
|
|
171
|
+
resolved = None
|
|
172
|
+
|
|
173
|
+
if resolved is not None and resolved.get_secret_value():
|
|
174
|
+
return resolved
|
|
175
|
+
|
|
176
|
+
if preset.api_key_placeholder:
|
|
177
|
+
return preset.api_key_placeholder
|
|
178
|
+
|
|
179
|
+
env_var = self._api_key_env_var(agent, preset)
|
|
180
|
+
from_env = os.environ.get(env_var) if env_var else None
|
|
181
|
+
if from_env:
|
|
182
|
+
return from_env
|
|
183
|
+
|
|
184
|
+
raise ValueError(
|
|
185
|
+
f"LLM agent '{agent.name}' uses provider '{agent.provider}', which "
|
|
186
|
+
f"requires an API key, but none could be resolved. Set {env_var} in "
|
|
187
|
+
"the environment, or give the agent an 'api_key' in configs/llm.yaml."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _api_key_env_var(agent: AgentConfig, preset: ProviderPreset) -> Optional[str]:
|
|
192
|
+
"""Returns the environment variable the agent's key should come from.
|
|
193
|
+
|
|
194
|
+
A config that already says ``${env:SOME_VAR}`` names its own variable;
|
|
195
|
+
anything else falls back to the provider's conventional one.
|
|
196
|
+
"""
|
|
197
|
+
raw = agent.api_key.get_secret_value() if agent.api_key else ""
|
|
198
|
+
if raw and raw.startswith("${env:") and raw.endswith("}"):
|
|
199
|
+
return raw[len("${env:") : -1]
|
|
200
|
+
return preset.api_key_env
|
|
201
|
+
|
|
202
|
+
def get_llm_config(self, name: str) -> Dict[str, Any]:
|
|
203
|
+
with self._lock:
|
|
204
|
+
if name not in self._configs:
|
|
205
|
+
name = "default"
|
|
206
|
+
config = self._configs[name]
|
|
207
|
+
return config.model_dump(exclude={"api_key"})
|
|
208
|
+
|
|
209
|
+
def list_llms(self) -> Dict[str, Dict[str, Any]]:
|
|
210
|
+
with self._lock:
|
|
211
|
+
return {
|
|
212
|
+
name: config.model_dump(exclude={"api_key"})
|
|
213
|
+
for name, config in self._configs.items()
|
|
214
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core pipeline module containing the graph definition and state management."""
|