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.
Files changed (192) hide show
  1. nl2sql/__init__.py +38 -0
  2. nl2sql/adapters/__init__.py +0 -0
  3. nl2sql/adapters/duckdb/__init__.py +0 -0
  4. nl2sql/adapters/duckdb/adapter.py +71 -0
  5. nl2sql/adapters/mssql/__init__.py +0 -0
  6. nl2sql/adapters/mssql/adapter.py +122 -0
  7. nl2sql/adapters/mysql/__init__.py +0 -0
  8. nl2sql/adapters/mysql/adapter.py +123 -0
  9. nl2sql/adapters/postgres/__init__.py +0 -0
  10. nl2sql/adapters/postgres/adapter.py +115 -0
  11. nl2sql/adapters/sqlalchemy_base/__init__.py +17 -0
  12. nl2sql/adapters/sqlalchemy_base/adapter.py +476 -0
  13. nl2sql/adapters/sqlalchemy_base/models.py +36 -0
  14. nl2sql/adapters/sqlite/__init__.py +0 -0
  15. nl2sql/adapters/sqlite/adapter.py +88 -0
  16. nl2sql/aggregation/__init__.py +3 -0
  17. nl2sql/aggregation/aggregator.py +98 -0
  18. nl2sql/aggregation/engines/__init__.py +3 -0
  19. nl2sql/aggregation/engines/polars_duckdb.py +125 -0
  20. nl2sql/api/__init__.py +0 -0
  21. nl2sql/api/auth_api.py +60 -0
  22. nl2sql/api/benchmark_api.py +114 -0
  23. nl2sql/api/datasource_api.py +132 -0
  24. nl2sql/api/indexing_api.py +59 -0
  25. nl2sql/api/llm_api.py +82 -0
  26. nl2sql/api/policy_api.py +135 -0
  27. nl2sql/api/query_api.py +138 -0
  28. nl2sql/api/result_api.py +24 -0
  29. nl2sql/api/settings_api.py +65 -0
  30. nl2sql/auth/__init__.py +8 -0
  31. nl2sql/auth/models.py +36 -0
  32. nl2sql/auth/rbac.py +25 -0
  33. nl2sql/cli/__init__.py +0 -0
  34. nl2sql/cli/checks.py +53 -0
  35. nl2sql/cli/commands/__init__.py +0 -0
  36. nl2sql/cli/commands/benchmark.py +34 -0
  37. nl2sql/cli/commands/doctor.py +49 -0
  38. nl2sql/cli/commands/indexing.py +126 -0
  39. nl2sql/cli/commands/info.py +25 -0
  40. nl2sql/cli/commands/install.py +27 -0
  41. nl2sql/cli/commands/policy.py +57 -0
  42. nl2sql/cli/commands/run.py +166 -0
  43. nl2sql/cli/commands/setup.py +415 -0
  44. nl2sql/cli/commands/visualize.py +34 -0
  45. nl2sql/cli/common/decorators.py +34 -0
  46. nl2sql/cli/config.py +24 -0
  47. nl2sql/cli/console.py +52 -0
  48. nl2sql/cli/demo/__init__.py +1 -0
  49. nl2sql/cli/demo/data.py +87 -0
  50. nl2sql/cli/demo/defaults.py +122 -0
  51. nl2sql/cli/demo/factory.py +289 -0
  52. nl2sql/cli/demo/manager.py +230 -0
  53. nl2sql/cli/demo/schemas.py +336 -0
  54. nl2sql/cli/demo/writers/__init__.py +0 -0
  55. nl2sql/cli/demo/writers/docker.py +182 -0
  56. nl2sql/cli/demo/writers/sqlite.py +88 -0
  57. nl2sql/cli/generators/datasources/__init__.py +3 -0
  58. nl2sql/cli/generators/datasources/generator.py +24 -0
  59. nl2sql/cli/generators/datasources/templates.py +7 -0
  60. nl2sql/cli/generators/env/__init__.py +3 -0
  61. nl2sql/cli/generators/env/generator.py +46 -0
  62. nl2sql/cli/generators/env/templates.py +25 -0
  63. nl2sql/cli/generators/llm/__init__.py +3 -0
  64. nl2sql/cli/generators/llm/generator.py +24 -0
  65. nl2sql/cli/generators/llm/templates.py +4 -0
  66. nl2sql/cli/generators/policies/__init__.py +3 -0
  67. nl2sql/cli/generators/policies/generator.py +20 -0
  68. nl2sql/cli/generators/policies/templates.py +2 -0
  69. nl2sql/cli/main.py +195 -0
  70. nl2sql/cli/reporting.py +878 -0
  71. nl2sql/cli/types.py +13 -0
  72. nl2sql/common/__init__.py +1 -0
  73. nl2sql/common/cancellation.py +25 -0
  74. nl2sql/common/context.py +5 -0
  75. nl2sql/common/errors.py +109 -0
  76. nl2sql/common/event_logger.py +88 -0
  77. nl2sql/common/exceptions.py +3 -0
  78. nl2sql/common/logger.py +119 -0
  79. nl2sql/common/metrics.py +50 -0
  80. nl2sql/common/resilience.py +59 -0
  81. nl2sql/common/settings.py +195 -0
  82. nl2sql/configs/__init__.py +6 -0
  83. nl2sql/configs/datasources.py +10 -0
  84. nl2sql/configs/llm.py +36 -0
  85. nl2sql/configs/manager.py +176 -0
  86. nl2sql/configs/policies.py +14 -0
  87. nl2sql/configs/sample_questions.py +11 -0
  88. nl2sql/configs/secrets.py +11 -0
  89. nl2sql/context.py +106 -0
  90. nl2sql/datasources/__init__.py +21 -0
  91. nl2sql/datasources/discovery.py +28 -0
  92. nl2sql/datasources/models.py +21 -0
  93. nl2sql/datasources/protocols.py +3 -0
  94. nl2sql/datasources/registry.py +172 -0
  95. nl2sql/evaluation/__init__.py +6 -0
  96. nl2sql/evaluation/benchmark_runner.py +320 -0
  97. nl2sql/evaluation/evaluator.py +134 -0
  98. nl2sql/evaluation/types.py +22 -0
  99. nl2sql/execution/__init__.py +4 -0
  100. nl2sql/execution/artifacts/__init__.py +3 -0
  101. nl2sql/execution/artifacts/parquet.py +41 -0
  102. nl2sql/execution/artifacts/store.py +165 -0
  103. nl2sql/execution/contracts.py +57 -0
  104. nl2sql/execution/execution_store.py +25 -0
  105. nl2sql/execution/executor/__init__.py +3 -0
  106. nl2sql/execution/executor/sql_executor.py +116 -0
  107. nl2sql/indexing/__init__.py +7 -0
  108. nl2sql/indexing/chunk_builder.py +227 -0
  109. nl2sql/indexing/embeddings.py +180 -0
  110. nl2sql/indexing/enrichment_service.py +316 -0
  111. nl2sql/indexing/models.py +209 -0
  112. nl2sql/indexing/orchestrator.py +90 -0
  113. nl2sql/indexing/vector_store.py +422 -0
  114. nl2sql/llm/__init__.py +8 -0
  115. nl2sql/llm/models.py +10 -0
  116. nl2sql/llm/registry.py +214 -0
  117. nl2sql/pipeline/__init__.py +1 -0
  118. nl2sql/pipeline/graph.py +73 -0
  119. nl2sql/pipeline/graph_utils.py +141 -0
  120. nl2sql/pipeline/nodes/__init__.py +25 -0
  121. nl2sql/pipeline/nodes/aggregator/__init__.py +4 -0
  122. nl2sql/pipeline/nodes/aggregator/node.py +55 -0
  123. nl2sql/pipeline/nodes/aggregator/prompts.py +20 -0
  124. nl2sql/pipeline/nodes/aggregator/schemas.py +28 -0
  125. nl2sql/pipeline/nodes/answer_synthesizer/__init__.py +4 -0
  126. nl2sql/pipeline/nodes/answer_synthesizer/node.py +98 -0
  127. nl2sql/pipeline/nodes/answer_synthesizer/prompts.py +19 -0
  128. nl2sql/pipeline/nodes/answer_synthesizer/schemas.py +24 -0
  129. nl2sql/pipeline/nodes/ast_planner/__init__.py +4 -0
  130. nl2sql/pipeline/nodes/ast_planner/node.py +104 -0
  131. nl2sql/pipeline/nodes/ast_planner/prompts.py +138 -0
  132. nl2sql/pipeline/nodes/ast_planner/schemas.py +236 -0
  133. nl2sql/pipeline/nodes/datasource_resolver/__init__.py +4 -0
  134. nl2sql/pipeline/nodes/datasource_resolver/node.py +253 -0
  135. nl2sql/pipeline/nodes/datasource_resolver/schemas.py +21 -0
  136. nl2sql/pipeline/nodes/decomposer/__init__.py +3 -0
  137. nl2sql/pipeline/nodes/decomposer/node.py +219 -0
  138. nl2sql/pipeline/nodes/decomposer/prompts.py +96 -0
  139. nl2sql/pipeline/nodes/decomposer/schemas.py +143 -0
  140. nl2sql/pipeline/nodes/executor/__init__.py +3 -0
  141. nl2sql/pipeline/nodes/executor/node.py +107 -0
  142. nl2sql/pipeline/nodes/generator/__init__.py +4 -0
  143. nl2sql/pipeline/nodes/generator/node.py +267 -0
  144. nl2sql/pipeline/nodes/generator/schemas.py +13 -0
  145. nl2sql/pipeline/nodes/global_planner/__init__.py +4 -0
  146. nl2sql/pipeline/nodes/global_planner/node.py +186 -0
  147. nl2sql/pipeline/nodes/global_planner/schemas.py +101 -0
  148. nl2sql/pipeline/nodes/refiner/__init__.py +4 -0
  149. nl2sql/pipeline/nodes/refiner/node.py +132 -0
  150. nl2sql/pipeline/nodes/refiner/prompts.py +28 -0
  151. nl2sql/pipeline/nodes/refiner/schemas.py +13 -0
  152. nl2sql/pipeline/nodes/schema_retriever/__init__.py +3 -0
  153. nl2sql/pipeline/nodes/schema_retriever/node.py +252 -0
  154. nl2sql/pipeline/nodes/schema_retriever/schema.py +27 -0
  155. nl2sql/pipeline/nodes/validator/__init__.py +7 -0
  156. nl2sql/pipeline/nodes/validator/node.py +839 -0
  157. nl2sql/pipeline/nodes/validator/schemas.py +12 -0
  158. nl2sql/pipeline/pipeline_runner.py +72 -0
  159. nl2sql/pipeline/routes.py +72 -0
  160. nl2sql/pipeline/runtime.py +153 -0
  161. nl2sql/pipeline/state.py +92 -0
  162. nl2sql/pipeline/subgraphs/__init__.py +5 -0
  163. nl2sql/pipeline/subgraphs/schemas.py +23 -0
  164. nl2sql/pipeline/subgraphs/sql_agent.py +167 -0
  165. nl2sql/public_api.py +199 -0
  166. nl2sql/schema/__init__.py +37 -0
  167. nl2sql/schema/in_memory_store.py +173 -0
  168. nl2sql/schema/protocol.py +88 -0
  169. nl2sql/schema/sqlite_store.py +233 -0
  170. nl2sql/schema/store.py +29 -0
  171. nl2sql/secrets/__init__.py +14 -0
  172. nl2sql/secrets/factory.py +85 -0
  173. nl2sql/secrets/interfaces.py +16 -0
  174. nl2sql/secrets/manager.py +139 -0
  175. nl2sql/secrets/models.py +56 -0
  176. nl2sql/secrets/providers/aws.py +30 -0
  177. nl2sql/secrets/providers/azure.py +49 -0
  178. nl2sql/secrets/providers/env.py +8 -0
  179. nl2sql/secrets/providers/hashi.py +46 -0
  180. nl2sql/services/__init__.py +0 -0
  181. nl2sql/services/callbacks/__init__.py +0 -0
  182. nl2sql/services/callbacks/monitor.py +84 -0
  183. nl2sql/services/callbacks/node_context.py +7 -0
  184. nl2sql/services/callbacks/node_handlers.py +187 -0
  185. nl2sql/services/callbacks/node_metrics.py +14 -0
  186. nl2sql/services/callbacks/presenter.py +12 -0
  187. nl2sql/services/callbacks/token_handler.py +56 -0
  188. nl2sql_engine-0.1.0.dist-info/METADATA +295 -0
  189. nl2sql_engine-0.1.0.dist-info/RECORD +192 -0
  190. nl2sql_engine-0.1.0.dist-info/WHEEL +5 -0
  191. nl2sql_engine-0.1.0.dist-info/entry_points.txt +9 -0
  192. nl2sql_engine-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Dict, List, Optional, Literal, Any
5
+
6
+ from pydantic import BaseModel, Field, ConfigDict
7
+
8
+ from nl2sql.auth.models import UserContext
9
+ from nl2sql.common.errors import PipelineError
10
+
11
+
12
+ ArtifactBackend = Literal["local", "s3", "adls"]
13
+ ArtifactFormat = Literal["parquet"]
14
+
15
+
16
+ class ArtifactRef(BaseModel):
17
+ uri: str
18
+ backend: ArtifactBackend
19
+ format: ArtifactFormat
20
+ row_count: int
21
+ columns: List[str]
22
+ bytes: int
23
+ content_hash: str
24
+ created_at: datetime
25
+ schema_version: Optional[str] = None
26
+ path_template: str
27
+
28
+ model_config = ConfigDict(extra="ignore")
29
+
30
+
31
+ class ExecutorRequest(BaseModel):
32
+ node_id: str
33
+ trace_id: str
34
+ subgraph_name: str
35
+ datasource_id: Optional[str] = None
36
+ schema_version: Optional[str] = None
37
+ sql: Optional[str] = None
38
+ user_context: Optional[UserContext] = None
39
+ tenant_id: str
40
+
41
+ model_config = ConfigDict(extra="ignore")
42
+
43
+
44
+ class ExecutorResponse(BaseModel):
45
+ executor_name: str
46
+ subgraph_name: str
47
+ node_id: str
48
+ trace_id: str
49
+ datasource_id: Optional[str] = None
50
+ schema_version: Optional[str] = None
51
+ artifact: Optional[ArtifactRef] = None
52
+ metrics: Dict[str, float] = Field(default_factory=dict)
53
+ errors: List[PipelineError] = Field(default_factory=list)
54
+ reasoning: List[Dict[str, Any]] = Field(default_factory=list)
55
+ tenant_id: str
56
+
57
+ model_config = ConfigDict(extra="ignore")
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from threading import Lock
4
+ from typing import Dict, Optional
5
+
6
+ from .contracts import ArtifactRef
7
+
8
+
9
+ class ExecutionStore:
10
+ """In-memory store for artifact references keyed by node id."""
11
+
12
+ def __init__(self) -> None:
13
+ self._artifacts: Dict[str, ArtifactRef] = {}
14
+ self._lock = Lock()
15
+
16
+ def put(self, node_id: str, artifact: ArtifactRef) -> None:
17
+ with self._lock:
18
+ self._artifacts[node_id] = artifact
19
+
20
+ def get(self, node_id: str) -> Optional[ArtifactRef]:
21
+ return self._artifacts.get(node_id)
22
+
23
+ def snapshot(self) -> Dict[str, ArtifactRef]:
24
+ with self._lock:
25
+ return dict(self._artifacts)
@@ -0,0 +1,3 @@
1
+ from .sql_executor import SqlExecutorService
2
+
3
+ __all__ = ["SqlExecutorService"]
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, Dict, Any
4
+
5
+ from nl2sql.common.errors import PipelineError, ErrorSeverity, ErrorCode
6
+ from nl2sql.common.logger import get_logger
7
+ from nl2sql.execution.contracts import ExecutorRequest, ExecutorResponse
8
+ from nl2sql.execution.artifacts import build_artifact_store
9
+ from nl2sql_adapter_sdk.capabilities import DatasourceCapability
10
+ from nl2sql.datasources import DatasourceRegistry
11
+ from nl2sql_adapter_sdk.contracts import AdapterRequest
12
+
13
+ logger = get_logger("sql_executor")
14
+
15
+
16
+ class SqlExecutorService:
17
+ def __init__(self, ds_registry: DatasourceRegistry):
18
+ self.ds_registry = ds_registry
19
+ self.artifact_store = build_artifact_store()
20
+
21
+ def validate_request(self, request: ExecutorRequest) -> list[PipelineError]:
22
+ errors = []
23
+ if not request.sql:
24
+ errors.append(
25
+ PipelineError(
26
+ node="sql_executor",
27
+ message="No SQL to execute.",
28
+ severity=ErrorSeverity.ERROR,
29
+ error_code=ErrorCode.MISSING_SQL,
30
+ )
31
+ )
32
+
33
+ if not request.datasource_id:
34
+ errors.append(
35
+ PipelineError(
36
+ node="sql_executor",
37
+ message="No datasource_id provided.",
38
+ severity=ErrorSeverity.ERROR,
39
+ error_code=ErrorCode.MISSING_DATASOURCE_ID
40
+ )
41
+ )
42
+
43
+ ds_id = request.datasource_id
44
+ caps = self.ds_registry.get_capabilities(ds_id)
45
+ if DatasourceCapability.SUPPORTS_SQL.value not in caps:
46
+ errors.append(
47
+ PipelineError(
48
+ node="sql_executor",
49
+ message=f"Datasource '{ds_id}' does not support SQL execution.",
50
+ severity=ErrorSeverity.ERROR,
51
+ error_code=ErrorCode.INVALID_STATE,
52
+ )
53
+ )
54
+ return errors
55
+
56
+ def execute(self, request: ExecutorRequest) -> ExecutorResponse:
57
+ errors = self.validate_request(request)
58
+ if errors:
59
+ return ExecutorResponse(
60
+ executor_name="sql_executor",
61
+ subgraph_name=request.subgraph_name,
62
+ node_id=request.node_id,
63
+ trace_id=request.trace_id,
64
+ datasource_id=request.datasource_id,
65
+ schema_version=request.schema_version,
66
+ errors=errors,
67
+ tenant_id=request.tenant_id,
68
+ )
69
+
70
+ ds_id = request.datasource_id
71
+ adapter = self.ds_registry.get_adapter(ds_id)
72
+
73
+ adapter_request = AdapterRequest(
74
+ plan_type="sql",
75
+ payload={"sql": request.sql},
76
+ limits={}
77
+ )
78
+
79
+ result_frame = adapter.execute(adapter_request)
80
+
81
+ if not result_frame.success:
82
+ error_msg = result_frame.error.safe_message if result_frame.error else "SQL execution failed."
83
+ error = PipelineError(
84
+ node="sql_executor",
85
+ message=error_msg,
86
+ severity=ErrorSeverity.ERROR,
87
+ error_code=ErrorCode.EXECUTION_FAILED,
88
+ )
89
+ return ExecutorResponse(
90
+ executor_name="sql_executor",
91
+ subgraph_name=request.subgraph_name,
92
+ node_id=request.node_id,
93
+ trace_id=request.trace_id,
94
+ datasource_id=request.datasource_id,
95
+ schema_version=request.schema_version,
96
+ errors=[error],
97
+ tenant_id=request.tenant_id,
98
+ )
99
+
100
+ artifact_ref = self.artifact_store.create_artifact_ref(
101
+ result_frame, {"schema_version": request.schema_version, "request_id": request.trace_id, "tenant_id": request.tenant_id} )
102
+
103
+ return ExecutorResponse(
104
+ executor_name="sql_executor",
105
+ subgraph_name=request.subgraph_name,
106
+ node_id=request.node_id,
107
+ trace_id=request.trace_id,
108
+ datasource_id=request.datasource_id,
109
+ schema_version=request.schema_version,
110
+ artifact=artifact_ref,
111
+ metrics={
112
+ "row_count": result_frame.row_count,
113
+ "bytes_returned": result_frame.bytes or 0,
114
+ },
115
+ tenant_id=request.tenant_id,
116
+ )
@@ -0,0 +1,7 @@
1
+ from .orchestrator import IndexingOrchestrator
2
+ from .vector_store import VectorStore
3
+
4
+ __all__ = [
5
+ "IndexingOrchestrator",
6
+ "VectorStore"
7
+ ]
@@ -0,0 +1,227 @@
1
+ from typing import List
2
+
3
+ from .models import (
4
+ BaseChunk,
5
+ DatasourceChunk,
6
+ TableChunk,
7
+ ColumnChunk,
8
+ RelationshipChunk,
9
+ MetricChunk,
10
+ )
11
+ from nl2sql.schema import SchemaSnapshot
12
+ from nl2sql_adapter_sdk.schema import TableRef, ColumnRef
13
+
14
+
15
+ class SchemaChunkBuilder:
16
+ """
17
+ Builds schema chunks from a SchemaSnapshot.
18
+
19
+ This class performs a pure transformation from SchemaSnapshot
20
+ to a list of schema chunks used for retrieval and grounding.
21
+
22
+ No inference, optimization, or database access is performed.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ ds_id: str,
28
+ schema_snapshot: SchemaSnapshot,
29
+ schema_version: str,
30
+ questions: List[str],
31
+ ):
32
+ """
33
+ Initializes the SchemaChunkBuilder.
34
+
35
+ Args:
36
+ ds_id: Datasource identifier.
37
+ schema_snapshot: Snapshot containing schema contract and metadata.
38
+ schema_version: Version identifier for the schema snapshot.
39
+ questions: Example user questions for grounding the datasource.
40
+ """
41
+ self.ds_id = ds_id
42
+ self.schema_snapshot = schema_snapshot
43
+ self.schema_version = schema_version
44
+ self.questions = questions
45
+
46
+ def build(self) -> List[BaseChunk]:
47
+ """
48
+ Builds all schema chunks.
49
+
50
+ Returns:
51
+ List of schema chunks derived from the schema snapshot.
52
+ """
53
+ chunks: List[BaseChunk] = []
54
+
55
+ chunks.extend(self._build_datasource_chunks())
56
+ chunks.extend(self._build_table_chunks())
57
+ chunks.extend(self._build_column_chunks())
58
+ chunks.extend(self._build_relationship_chunks())
59
+ chunks.extend(self._build_metric_chunks())
60
+
61
+ return chunks
62
+
63
+ def _build_datasource_chunks(self) -> List[DatasourceChunk]:
64
+ """
65
+ Builds datasource-level chunks.
66
+
67
+ Returns:
68
+ List containing a single DatasourceChunk.
69
+ """
70
+ md = self.schema_snapshot.metadata
71
+
72
+ return [
73
+ DatasourceChunk(
74
+ id=f"schema.datasource:{md.datasource_id}:{self.schema_version}",
75
+ datasource_id=md.datasource_id,
76
+ description=md.description or "",
77
+ domains=md.domains,
78
+ schema_version=self.schema_version,
79
+ examples=self.questions,
80
+ )
81
+ ]
82
+
83
+ def _build_table_chunks(self) -> List[TableChunk]:
84
+ """
85
+ Builds table-level chunks.
86
+
87
+ Returns:
88
+ List of TableChunk objects.
89
+ """
90
+ chunks: List[TableChunk] = []
91
+
92
+ contract = self.schema_snapshot.contract
93
+ metadata = self.schema_snapshot.metadata
94
+
95
+ for table_key, table_contract in contract.tables.items():
96
+ table_md = metadata.tables.get(table_key)
97
+ table_ref = TableRef(
98
+ schema_name=table_contract.table.schema_name,
99
+ table_name=table_contract.table.table_name,
100
+ )
101
+
102
+ primary_keys = [
103
+ c.name for c in table_contract.columns.values() if c.is_primary_key
104
+ ]
105
+
106
+ column_names = sorted(table_contract.columns.keys())
107
+
108
+ foreign_keys = [
109
+ f"{table_ref.full_name} -> {fk.referred_table.full_name}"
110
+ for fk in table_contract.foreign_keys
111
+ ]
112
+
113
+ chunks.append(
114
+ TableChunk(
115
+ id=f"schema.table:{table_ref.full_name}:{self.schema_version}",
116
+ datasource_id=self.ds_id,
117
+ table=table_ref,
118
+ description=table_md.description if table_md else None,
119
+ primary_key=primary_keys,
120
+ columns=column_names,
121
+ foreign_keys=foreign_keys,
122
+ row_count=table_md.row_count if table_md else None,
123
+ schema_version=self.schema_version,
124
+ )
125
+ )
126
+
127
+ return chunks
128
+
129
+ def _build_column_chunks(self) -> List[ColumnChunk]:
130
+ """
131
+ Builds column-level chunks.
132
+
133
+ Returns:
134
+ List of ColumnChunk objects.
135
+ """
136
+ chunks: List[ColumnChunk] = []
137
+
138
+ contract = self.schema_snapshot.contract
139
+ metadata = self.schema_snapshot.metadata
140
+
141
+ for table_key, table_contract in contract.tables.items():
142
+ table_md = metadata.tables.get(table_key)
143
+ table_ref = TableRef(
144
+ schema_name=table_contract.table.schema_name,
145
+ table_name=table_contract.table.table_name,
146
+ )
147
+
148
+ for column_name, column_contract in table_contract.columns.items():
149
+ column_md = table_md.columns.get(column_name) if table_md else None
150
+
151
+ column_ref = ColumnRef(
152
+ table=table_ref,
153
+ column_name=column_name,
154
+ )
155
+
156
+ chunks.append(
157
+ ColumnChunk(
158
+ id=f"schema.column:{column_ref.table.full_name}:{column_ref.column_name}:{self.schema_version}",
159
+ datasource_id=self.ds_id,
160
+ column=column_ref,
161
+ dtype=column_contract.data_type,
162
+ description=column_md.description if column_md else None,
163
+ column_stats=(
164
+ column_md.statistics.model_dump()
165
+ if column_md and column_md.statistics
166
+ else {}
167
+ ),
168
+ synonyms=column_md.synonyms if column_md else None,
169
+ pii=column_md.pii if column_md else False,
170
+ schema_version=self.schema_version,
171
+ )
172
+ )
173
+
174
+ return chunks
175
+
176
+ def _build_relationship_chunks(self) -> List[RelationshipChunk]:
177
+ """
178
+ Builds relationship-level chunks.
179
+
180
+ Returns:
181
+ List of RelationshipChunk objects with unknown cardinality.
182
+ """
183
+ chunks: List[RelationshipChunk] = []
184
+
185
+ contract = self.schema_snapshot.contract
186
+
187
+ for table_contract in contract.tables.values():
188
+ from_table = TableRef(
189
+ schema_name=table_contract.table.schema_name,
190
+ table_name=table_contract.table.table_name,
191
+ )
192
+
193
+ for fk in table_contract.foreign_keys:
194
+ to_table = TableRef(
195
+ schema_name=fk.referred_table.schema_name,
196
+ table_name=fk.referred_table.table_name,
197
+ )
198
+
199
+ chunks.append(
200
+ RelationshipChunk(
201
+ id=(
202
+ f"schema.relationship:"
203
+ f"{from_table.full_name}"
204
+ f"->{to_table.full_name}:"
205
+ f"{self.schema_version}"
206
+ ),
207
+ datasource_id=self.ds_id,
208
+ from_table=from_table,
209
+ to_table=to_table,
210
+ from_columns=fk.constrained_columns,
211
+ to_columns=fk.referred_columns,
212
+ cardinality="unknown",
213
+ business_meaning=None,
214
+ schema_version=self.schema_version,
215
+ )
216
+ )
217
+
218
+ return chunks
219
+
220
+ def _build_metric_chunks(self) -> List[MetricChunk]:
221
+ """
222
+ Builds metric-level chunks.
223
+
224
+ Returns:
225
+ Empty list, as metrics are not part of SchemaSnapshot.
226
+ """
227
+ return []
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, List, Optional, Tuple
4
+
5
+ from langchain_core.embeddings import Embeddings
6
+ from langchain_openai import OpenAIEmbeddings
7
+
8
+ from nl2sql.common.logger import get_logger
9
+ from nl2sql.common.settings import settings
10
+
11
+ logger = get_logger(__name__)
12
+
13
+ SUPPORTED_EMBEDDING_PROVIDERS: Tuple[str, ...] = ("openai", "local")
14
+
15
+ LOCAL_EMBEDDING_MODEL = "all-MiniLM-L6-v2"
16
+ LOCAL_EMBEDDING_DIMENSION = 384
17
+
18
+ # Output dimensions of the embedding models this project knows about. Used only
19
+ # to detect an index built with a different embedder; unknown models are skipped.
20
+ KNOWN_EMBEDDING_DIMENSIONS: Dict[str, int] = {
21
+ "text-embedding-3-small": 1536,
22
+ "text-embedding-3-large": 3072,
23
+ "text-embedding-ada-002": 1536,
24
+ LOCAL_EMBEDDING_MODEL: LOCAL_EMBEDDING_DIMENSION,
25
+ }
26
+
27
+ PROVIDER_BY_DIMENSION: Dict[int, str] = {
28
+ LOCAL_EMBEDDING_DIMENSION: "local",
29
+ 1536: "openai",
30
+ 3072: "openai",
31
+ }
32
+
33
+
34
+ class LocalEmbeddings(Embeddings):
35
+ """
36
+ Key-free embeddings backed by the ONNX all-MiniLM-L6-v2 model bundled with
37
+ chromadb. Produces 384-dimensional vectors and requires no API key.
38
+ """
39
+
40
+ def __init__(self) -> None:
41
+ self._embedding_function = None
42
+
43
+ def _get_embedding_function(self):
44
+ """
45
+ Lazily builds chromadb's default embedding function.
46
+
47
+ Returns:
48
+ The chromadb embedding function instance.
49
+ """
50
+ if self._embedding_function is None:
51
+ # Imported here so nothing pays for chromadb's ONNX runtime unless
52
+ # local embeddings are actually selected.
53
+ from chromadb.utils.embedding_functions import DefaultEmbeddingFunction
54
+
55
+ logger.info(
56
+ f"Initializing local embeddings ({LOCAL_EMBEDDING_MODEL}). "
57
+ "The first run downloads the ONNX model (~79 MB) into the local "
58
+ "cache directory, which can take a few minutes."
59
+ )
60
+ self._embedding_function = DefaultEmbeddingFunction()
61
+ return self._embedding_function
62
+
63
+ def embed_documents(self, texts: List[str]) -> List[List[float]]:
64
+ """
65
+ Embeds a batch of documents.
66
+
67
+ Args:
68
+ texts: Documents to embed.
69
+
70
+ Returns:
71
+ One plain float vector per document.
72
+ """
73
+ vectors = self._get_embedding_function()(list(texts))
74
+ return [[float(value) for value in vector] for vector in vectors]
75
+
76
+ def embed_query(self, text: str) -> List[float]:
77
+ """
78
+ Embeds a single query.
79
+
80
+ Args:
81
+ text: Query to embed.
82
+
83
+ Returns:
84
+ A flat plain float vector.
85
+ """
86
+ return self.embed_documents([text])[0]
87
+
88
+
89
+ def describe_embeddings(embeddings: Embeddings) -> Tuple[str, str, Optional[int]]:
90
+ """
91
+ Describes an embedder for diagnostics.
92
+
93
+ Args:
94
+ embeddings: Embedding implementation in use.
95
+
96
+ Returns:
97
+ Tuple of provider name, model name and known output dimension. The
98
+ dimension is None when the model is not a known one.
99
+ """
100
+ if isinstance(embeddings, LocalEmbeddings):
101
+ return "local", LOCAL_EMBEDDING_MODEL, LOCAL_EMBEDDING_DIMENSION
102
+
103
+ model = getattr(embeddings, "model", None)
104
+ provider = "openai" if isinstance(embeddings, OpenAIEmbeddings) else type(embeddings).__name__
105
+ dimension = KNOWN_EMBEDDING_DIMENSIONS.get(model) if model else None
106
+ return provider, model or "unknown", dimension
107
+
108
+
109
+ class EmbeddingService:
110
+ """
111
+ Centralized service for managing embedding models.
112
+ Ensures consistency across the application.
113
+ """
114
+
115
+ _instance: Optional[Embeddings] = None
116
+ _instance_provider: Optional[str] = None
117
+
118
+ @classmethod
119
+ def get_embeddings(cls) -> Embeddings:
120
+ """
121
+ Returns the embeddings instance for the configured provider.
122
+
123
+ The instance is cached per provider so a runtime settings reload
124
+ (``reload_settings``) cannot hand back an embedder for the old provider.
125
+ """
126
+ provider = cls._resolve_provider()
127
+ if cls._instance is None or cls._instance_provider != provider:
128
+ cls._instance = cls._build_embeddings(provider)
129
+ cls._instance_provider = provider
130
+ return cls._instance
131
+
132
+ @classmethod
133
+ def reset(cls) -> None:
134
+ """Drops the cached embedder so the next call rebuilds it.
135
+
136
+ The cache lives on the class and outlives any single caller, so tests
137
+ and long-lived processes that change the configured provider need a way
138
+ to discard it.
139
+ """
140
+ cls._instance = None
141
+ cls._instance_provider = None
142
+
143
+ @classmethod
144
+ def _resolve_provider(cls) -> str:
145
+ """Returns the normalized embedding provider from settings."""
146
+ return (settings.embedding_provider or "openai").strip().lower()
147
+
148
+ @classmethod
149
+ def _build_embeddings(cls, provider: str) -> Embeddings:
150
+ """
151
+ Builds an embeddings instance for a provider.
152
+
153
+ Args:
154
+ provider: Normalized provider name.
155
+
156
+ Returns:
157
+ The embeddings implementation.
158
+
159
+ Raises:
160
+ ValueError: If the provider is not recognized.
161
+ """
162
+ if provider == "openai":
163
+ return OpenAIEmbeddings(
164
+ model=settings.embedding_model,
165
+ api_key=settings.openai_api_key,
166
+ )
167
+ if provider == "local":
168
+ return LocalEmbeddings()
169
+
170
+ raise ValueError(
171
+ f"Unknown EMBEDDING_PROVIDER '{provider}'. "
172
+ f"Valid options are: {', '.join(SUPPORTED_EMBEDDING_PROVIDERS)}."
173
+ )
174
+
175
+ @classmethod
176
+ def get_model_name(cls) -> str:
177
+ """Returns the name of the configured embedding model."""
178
+ if cls._resolve_provider() == "local":
179
+ return LOCAL_EMBEDDING_MODEL
180
+ return settings.embedding_model