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,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,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,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
|