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,12 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from nl2sql.common.errors import PipelineError
8
+
9
+
10
+ class LogicalValidatorResponse(BaseModel):
11
+ errors: List[PipelineError] = Field(default_factory=list)
12
+ reasoning: List[Dict[str, Any]] = Field(default_factory=list)
@@ -0,0 +1,72 @@
1
+ import time
2
+ import traceback
3
+ from typing import Dict, Any, Optional, List
4
+ from dataclasses import dataclass, field
5
+
6
+ from nl2sql.auth import UserContext
7
+ from nl2sql.pipeline.runtime import run_with_graph
8
+ from nl2sql.common.settings import settings
9
+ from nl2sql.context import NL2SQLContext
10
+
11
+
12
+ @dataclass
13
+ class PipelineResult:
14
+ """Standardized result object from the pipeline execution."""
15
+
16
+ success: bool
17
+ final_state: Dict[str, Any] = field(default_factory=dict)
18
+ duration: float = 0.0
19
+ error: Optional[str] = None
20
+ traceback: Optional[str] = None
21
+
22
+
23
+ class PipelineRunner:
24
+ """
25
+ Orchestrates the execution of the NL2SQL pipeline.
26
+ Decoupled from CLI presentation logic.
27
+ """
28
+
29
+ def __init__(self, ctx: NL2SQLContext):
30
+ self.ctx = ctx
31
+
32
+ def run(
33
+ self,
34
+ query: str,
35
+ role: str = "admin",
36
+ datasource_id: Optional[str] = None,
37
+ execute: bool = True,
38
+ callbacks: List[Any] = None,
39
+ ) -> PipelineResult:
40
+ """
41
+ Executes the pipeline graph.
42
+ """
43
+ start_time = time.perf_counter()
44
+
45
+ user_context = UserContext(
46
+ roles=[role],
47
+ tenant_id=settings.tenant_id,
48
+ )
49
+
50
+ try:
51
+ final_state = run_with_graph(
52
+ self.ctx,
53
+ user_query=query,
54
+ datasource_id=datasource_id,
55
+ execute=execute,
56
+ callbacks=callbacks or [],
57
+ user_context=user_context,
58
+ )
59
+
60
+ return PipelineResult(
61
+ success=True,
62
+ final_state=final_state,
63
+ duration=time.perf_counter() - start_time,
64
+ )
65
+
66
+ except Exception as e:
67
+ return PipelineResult(
68
+ success=False,
69
+ error=str(e),
70
+ traceback=traceback.format_exc(),
71
+ duration=time.perf_counter() - start_time,
72
+ )
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ from langgraph.graph import END
4
+ from langgraph.types import Send
5
+
6
+ from nl2sql.common.errors import PipelineError, ErrorSeverity, ErrorCode
7
+ from nl2sql.context import NL2SQLContext
8
+ from nl2sql.pipeline.graph_utils import (
9
+ StateAccessor,
10
+ build_scan_payload,
11
+ next_scan_layer_ids,
12
+ resolve_subgraph,
13
+ )
14
+ from nl2sql.pipeline.state import GraphState
15
+ from nl2sql.common.logger import get_logger
16
+
17
+ logger = get_logger("router")
18
+
19
+
20
+ def resolver_route(state: GraphState) -> str:
21
+ accessor = StateAccessor(state)
22
+ resolver_response = accessor.get("datasource_resolver_response")
23
+ if not resolver_response:
24
+ return "end"
25
+ if not resolver_response.resolved_datasources or not resolver_response.allowed_datasource_ids:
26
+ return "end"
27
+ return "continue"
28
+
29
+
30
+ def build_scan_layer_router(ctx: NL2SQLContext):
31
+ def route_scan_layers(state: GraphState):
32
+ global_planner_response = state.global_planner_response
33
+ dag = global_planner_response.execution_dag if global_planner_response else None
34
+ decomposer_response = state.decomposer_response
35
+ sub_queries = decomposer_response.sub_queries if decomposer_response else []
36
+ sub_query_map = {sq.id: sq for sq in sub_queries}
37
+ artifact_refs = state.artifact_refs or {}
38
+ if not dag or not dag.layers:
39
+ return END
40
+
41
+ node_index = {n.node_id: n for n in dag.nodes}
42
+ target_ids = next_scan_layer_ids(dag, artifact_refs)
43
+ if not target_ids:
44
+ return [
45
+ Send("aggregator",state)
46
+ ]
47
+
48
+ branches = []
49
+ for node_id in target_ids:
50
+ if node_id in sub_query_map:
51
+ sq = sub_query_map[node_id]
52
+ datasource_id = sq.datasource_id
53
+ else:
54
+ node = node_index.get(node_id) if dag else None
55
+ if not node:
56
+ continue
57
+ datasource_id = (node.attributes or {}).get("datasource_id")
58
+
59
+ target = resolve_subgraph(datasource_id, ctx)
60
+ if not target:
61
+ raise PipelineError(
62
+ node="layer_router",
63
+ message=f"No compatible subgraph found for datasource '{datasource_id}'.",
64
+ severity=ErrorSeverity.ERROR,
65
+ error_code=ErrorCode.INVALID_STATE,
66
+ )
67
+ payload = build_scan_payload(state, target, node_id)
68
+ branches.append(Send(target, payload))
69
+
70
+ return branches
71
+
72
+ return route_scan_layers
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ import concurrent.futures
4
+ import signal
5
+ import sys
6
+ import threading
7
+ import traceback
8
+ from typing import Callable, Dict, List, Optional
9
+
10
+ from nl2sql.auth import UserContext
11
+ from nl2sql.common.cancellation import CancellationToken
12
+ from nl2sql.common.errors import PipelineError, ErrorSeverity, ErrorCode
13
+ from nl2sql.common.settings import settings
14
+ from nl2sql.context import NL2SQLContext
15
+ from nl2sql.pipeline.graph import build_graph
16
+ from nl2sql.pipeline.state import GraphState
17
+
18
+
19
+ def _start_keyboard_cancel_listener(
20
+ token: CancellationToken,
21
+ done: threading.Event,
22
+ ) -> None:
23
+ """Cancel ``token`` when Ctrl+X is pressed, until ``done`` is set.
24
+
25
+ One listener per run: the thread exits once the run finishes, so a later run
26
+ starts its own listener bound to its own token.
27
+ """
28
+ if sys.platform != "win32":
29
+ return
30
+
31
+ if not sys.stdin or not sys.stdin.isatty():
32
+ return
33
+
34
+ try:
35
+ import msvcrt
36
+ except Exception:
37
+ return
38
+
39
+ def _listen():
40
+ while not done.is_set():
41
+ if msvcrt.kbhit():
42
+ char = msvcrt.getch()
43
+ if char == b"\x18": # Ctrl+X
44
+ token.cancel()
45
+ return
46
+ done.wait(0.05)
47
+
48
+ threading.Thread(target=_listen, daemon=True).start()
49
+
50
+
51
+ def _install_signal_handlers(token: CancellationToken) -> Callable[[], None]:
52
+ previous = {}
53
+
54
+ def _handler(signum, frame):
55
+ token.cancel()
56
+
57
+ for sig in (getattr(signal, "SIGINT", None), getattr(signal, "SIGTERM", None)):
58
+ if sig is None:
59
+ continue
60
+ previous[sig] = signal.getsignal(sig)
61
+ signal.signal(sig, _handler)
62
+
63
+ def _restore():
64
+ for sig, handler in previous.items():
65
+ signal.signal(sig, handler)
66
+
67
+ return _restore
68
+
69
+
70
+ def run_with_graph(
71
+ ctx: NL2SQLContext,
72
+ user_query: str,
73
+ datasource_id: Optional[str] = None,
74
+ execute: bool = True,
75
+ callbacks: Optional[List] = None,
76
+ user_context: UserContext = None,
77
+ ) -> Dict:
78
+ """Convenience function to run the full pipeline."""
79
+ token = CancellationToken()
80
+ run_done = threading.Event()
81
+ restore_signals = _install_signal_handlers(token)
82
+ _start_keyboard_cancel_listener(token, run_done)
83
+
84
+ graph = build_graph(
85
+ ctx,
86
+ execute=execute,
87
+ )
88
+
89
+ initial_state = GraphState(
90
+ user_query=user_query,
91
+ user_context=user_context,
92
+ datasource_id=datasource_id,
93
+ )
94
+
95
+ timeout_sec = settings.global_timeout_sec
96
+
97
+ def _invoke():
98
+ return graph.invoke(
99
+ initial_state.model_dump(),
100
+ config={
101
+ "configurable": {"cancellation_token": token},
102
+ "callbacks": callbacks,
103
+ },
104
+ )
105
+
106
+ try:
107
+ # Use configured thread pool size for pipeline execution
108
+ with concurrent.futures.ThreadPoolExecutor(max_workers=settings.sandbox_exec_workers) as executor:
109
+ future = executor.submit(_invoke)
110
+ result = future.result(timeout=timeout_sec)
111
+
112
+ # Nodes observe the token and unwind, so a cancelled run returns normally.
113
+ if token.is_cancelled():
114
+ return {
115
+ "errors": [
116
+ PipelineError(
117
+ node="orchestrator",
118
+ message="Pipeline cancelled by user.",
119
+ severity=ErrorSeverity.ERROR,
120
+ error_code=ErrorCode.CANCELLED,
121
+ )
122
+ ]
123
+ }
124
+ return result
125
+ except concurrent.futures.TimeoutError:
126
+ error_msg = f"Pipeline execution timed out after {timeout_sec} seconds."
127
+ return {
128
+ "errors": [
129
+ PipelineError(
130
+ node="orchestrator",
131
+ message=error_msg,
132
+ severity=ErrorSeverity.ERROR,
133
+ error_code=ErrorCode.PIPELINE_TIMEOUT,
134
+ )
135
+ ],
136
+ "final_answer": "I apologize, but the request timed out. Please try again with a simpler query.",
137
+ }
138
+ except Exception as e:
139
+ # Fallback for other runtime crashes
140
+ return {
141
+ "errors": [
142
+ PipelineError(
143
+ node="orchestrator",
144
+ message=f"Pipeline crashed: {str(e)}",
145
+ severity=ErrorSeverity.ERROR,
146
+ error_code=ErrorCode.UNKNOWN_ERROR,
147
+ stack_trace=traceback.format_exc(),
148
+ )
149
+ ]
150
+ }
151
+ finally:
152
+ run_done.set()
153
+ restore_signals()
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional, Annotated
4
+ import operator
5
+ import uuid
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+ from nl2sql.common.errors import PipelineError
10
+ from nl2sql.auth import UserContext
11
+ from nl2sql.pipeline.nodes.datasource_resolver.schemas import DatasourceResolverResponse
12
+ from nl2sql.pipeline.nodes.decomposer.schemas import DecomposerResponse, SubQuery
13
+ from nl2sql.pipeline.nodes.global_planner.schemas import GlobalPlannerResponse
14
+ from nl2sql.pipeline.nodes.aggregator.schemas import AggregatorResponse
15
+ from nl2sql.pipeline.nodes.answer_synthesizer.schemas import AnswerSynthesizerResponse
16
+ from nl2sql.pipeline.nodes.ast_planner.schemas import ASTPlannerResponse
17
+ from nl2sql.pipeline.nodes.validator.schemas import LogicalValidatorResponse
18
+ from nl2sql.pipeline.nodes.generator.schemas import GeneratorResponse
19
+ from nl2sql.execution.contracts import ArtifactRef, ExecutorResponse
20
+ from nl2sql.pipeline.nodes.refiner.schemas import RefinerResponse
21
+ from nl2sql.pipeline.subgraphs.schemas import SubgraphOutput
22
+ from nl2sql.pipeline.nodes.schema_retriever.schema import Table
23
+
24
+
25
+ def update_results(current: Dict, new: Dict) -> Dict:
26
+ """Reducer to merge execution results from parallel branches."""
27
+ if current is None:
28
+ return new
29
+ return {**current, **new}
30
+
31
+
32
+ class GraphState(BaseModel):
33
+ """Represents the shared state of the NL2SQL pipeline execution graph.
34
+
35
+ Attributes:
36
+ trace_id (str): Distributed unique trace ID.
37
+ user_query (str): Canonical user query.
38
+ user_context (UserContext): User identity and permissions context.
39
+ datasource_id (Optional[str]): Optional datasource override for resolution.
40
+ datasource_resolver_response (Optional[DatasourceResolverResponse]): Output of resolver node.
41
+ decomposer_response (Optional[DecomposerResponse]): Output of decomposer node.
42
+ global_planner_response (Optional[GlobalPlannerResponse]): Output of planner node.
43
+ aggregator_response (Optional[AggregatorResponse]): Output of aggregator node.
44
+ answer_synthesizer_response (Optional[AnswerSynthesizerResponse]): Output of synthesizer node.
45
+ artifact_refs (Dict[str, ArtifactRef]): Artifact refs keyed by ExecutionDAG node_id.
46
+ subgraph_outputs (Dict[str, SubgraphOutput]): Per-subgraph diagnostic outputs.
47
+ errors (List[PipelineError]): List of errors encountered during execution.
48
+ reasoning (List[Dict[str, Any]]): Log of reasoning steps from nodes.
49
+ warnings (List[Dict[str, Any]]): Warning messages emitted by nodes.
50
+ subgraph_id (Optional[str]): ID of the subgraph execution.
51
+ subgraph_name (Optional[str]): Name of the subgraph execution.
52
+ """
53
+ model_config = ConfigDict(extra="ignore", arbitrary_types_allowed=True)
54
+
55
+ trace_id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Distributed unique trace ID.")
56
+ user_query: str = Field(description="Canonical user query.")
57
+ user_context: UserContext = Field(default_factory=UserContext, description="User identity and permissions context.")
58
+ datasource_id: Optional[str] = Field(default=None, description="Optional datasource override for resolution.")
59
+ datasource_resolver_response: Optional[DatasourceResolverResponse] = Field(default=None)
60
+ decomposer_response: Optional[DecomposerResponse] = Field(default=None)
61
+ global_planner_response: Optional[GlobalPlannerResponse] = Field(default=None)
62
+ aggregator_response: Optional[AggregatorResponse] = Field(default=None)
63
+ answer_synthesizer_response: Optional[AnswerSynthesizerResponse] = Field(default=None)
64
+ artifact_refs: Annotated[Dict[str, ArtifactRef], update_results] = Field(default_factory=dict)
65
+ subgraph_outputs: Annotated[Dict[str, SubgraphOutput], update_results] = Field(default_factory=dict)
66
+ errors: Annotated[List[PipelineError], operator.add] = Field(default_factory=list)
67
+ reasoning: Annotated[List[Dict[str, Any]], operator.add] = Field(default_factory=list)
68
+ warnings: Annotated[List[Dict[str, Any]], operator.add] = Field(default_factory=list)
69
+ subgraph_id: Optional[str] = Field(default=None)
70
+ subgraph_name: Optional[str] = Field(default=None)
71
+
72
+
73
+ class SubgraphExecutionState(BaseModel):
74
+ model_config = ConfigDict(extra="ignore", arbitrary_types_allowed=True)
75
+
76
+ trace_id: str
77
+ sub_query: Optional[SubQuery] = None
78
+ user_context: Optional[UserContext] = None
79
+ subgraph_id: Optional[str] = None
80
+ subgraph_name: Optional[str] = None
81
+ relevant_tables: List[Table] = Field(default_factory=list)
82
+
83
+ ast_planner_response: Optional[ASTPlannerResponse] = Field(default=None)
84
+ logical_validator_response: Optional[LogicalValidatorResponse] = Field(default=None)
85
+ generator_response: Optional[GeneratorResponse] = Field(default=None)
86
+ executor_response: Optional[ExecutorResponse] = Field(default=None)
87
+ refiner_response: Optional[RefinerResponse] = Field(default=None)
88
+
89
+ retry_count: int = 0
90
+ errors: Annotated[List[PipelineError], operator.add] = Field(default_factory=list)
91
+ reasoning: Annotated[List[Dict[str, Any]], operator.add] = Field(default_factory=list)
92
+ warnings: Annotated[List[Dict[str, Any]], operator.add] = Field(default_factory=list)
@@ -0,0 +1,5 @@
1
+ """Subgraph implementations."""
2
+
3
+ from .schemas import SubgraphOutput
4
+
5
+ __all__ = ["SubgraphOutput"]
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from nl2sql.common.errors import PipelineError
8
+ from nl2sql.pipeline.nodes.ast_planner.schemas import PlanModel
9
+ from nl2sql.execution.contracts import ArtifactRef
10
+ from nl2sql.pipeline.nodes.decomposer.schemas import SubQuery
11
+
12
+
13
+ class SubgraphOutput(BaseModel):
14
+ sub_query: Optional[SubQuery] = None
15
+ subgraph_id: str
16
+ subgraph_name: Optional[str] = None
17
+ retry_count: int = 0
18
+ plan: Optional[PlanModel] = None
19
+ sql_draft: Optional[str] = None
20
+ artifact: Optional[ArtifactRef] = None
21
+ errors: List[PipelineError] = Field(default_factory=list)
22
+ reasoning: List[Dict[str, Any]] = Field(default_factory=list)
23
+ status: Optional[str] = None
@@ -0,0 +1,167 @@
1
+ from typing import Dict, Optional
2
+ from langchain_core.runnables import Runnable, RunnableConfig
3
+ from langgraph.graph import END, StateGraph
4
+
5
+ from nl2sql.common.cancellation import CancellationToken
6
+ from nl2sql.common.errors import PipelineError, ErrorSeverity, ErrorCode
7
+ from nl2sql.common.settings import settings
8
+ from nl2sql.pipeline.state import SubgraphExecutionState
9
+ from nl2sql.pipeline.nodes.ast_planner import ASTPlannerNode
10
+ from nl2sql.pipeline.nodes.schema_retriever import SchemaRetrieverNode
11
+ from nl2sql.pipeline.nodes.validator import LogicalValidatorNode
12
+ from nl2sql.pipeline.nodes.refiner import RefinerNode
13
+ from nl2sql.pipeline.nodes.generator import GeneratorNode
14
+ from nl2sql.pipeline.nodes.executor import ExecutorNode
15
+ from nl2sql.datasources import DatasourceRegistry
16
+ from nl2sql.context import NL2SQLContext
17
+
18
+
19
+
20
+ import random
21
+ import time
22
+
23
+
24
+ def build_sql_agent_graph(
25
+ ctx: NL2SQLContext,
26
+ ):
27
+ """Builds the SQL Agent Subgraph.
28
+
29
+ Pipeline Flow:
30
+ Planner -> LogicalValidator -> Generator -> Executor
31
+
32
+ Feedback Loop:
33
+ (LogicalValidator Error) -> RetryHandler -> Refiner -> Planner
34
+ """
35
+ graph = StateGraph(SubgraphExecutionState)
36
+
37
+ schema_retriever = SchemaRetrieverNode(ctx)
38
+ ast_planner = ASTPlannerNode(ctx)
39
+ logical_validator = LogicalValidatorNode(ctx)
40
+ refiner = RefinerNode(ctx)
41
+ generator = GeneratorNode(ctx)
42
+ executor = ExecutorNode(ctx)
43
+
44
+ def _get_subgraph_id(state: SubgraphExecutionState) -> str:
45
+ if state.subgraph_id:
46
+ return state.subgraph_id
47
+ sub_query_id = state.sub_query.id if state.sub_query else None
48
+ return f"sql_agent:{sub_query_id}:{state.trace_id}"
49
+
50
+ def _get_retry_count(state: SubgraphExecutionState) -> int:
51
+ return state.retry_count
52
+
53
+ def _token(config: Optional[RunnableConfig]) -> Optional[CancellationToken]:
54
+ """Read the current run's cancellation token from the RunnableConfig.
55
+
56
+ Returns None when invoked without a config (e.g. a direct unit-test call),
57
+ which is treated as "not cancelled".
58
+ """
59
+ if not config:
60
+ return None
61
+ return (config.get("configurable") or {}).get("cancellation_token")
62
+
63
+ def _is_cancelled(config: Optional[RunnableConfig]) -> bool:
64
+ token = _token(config)
65
+ return bool(token and token.is_cancelled())
66
+
67
+ def retry_node(state: SubgraphExecutionState, config: Optional[RunnableConfig] = None) -> Dict:
68
+ """Increments retry count with exponential backoff and jitter."""
69
+ count = _get_retry_count(state)
70
+ if _is_cancelled(config):
71
+ return {
72
+ "errors": [
73
+ PipelineError(
74
+ node="retry_handler",
75
+ message="Pipeline cancelled by user.",
76
+ severity=ErrorSeverity.ERROR,
77
+ error_code=ErrorCode.CANCELLED,
78
+ )
79
+ ]
80
+ }
81
+ if count >= settings.sql_agent_max_retries:
82
+ return {"retry_count": count}
83
+
84
+ base_delay = min(settings.sql_agent_retry_max_delay_sec, settings.sql_agent_retry_base_delay_sec * (2 ** count))
85
+ jitter = random.uniform(0.0, settings.sql_agent_retry_jitter_sec)
86
+ sleep_time = base_delay + jitter
87
+
88
+ token = _token(config)
89
+ if token is None:
90
+ time.sleep(sleep_time)
91
+ elif token.wait(timeout=sleep_time):
92
+ return {
93
+ "errors": [
94
+ PipelineError(
95
+ node="retry_handler",
96
+ message="Pipeline cancelled by user.",
97
+ severity=ErrorSeverity.ERROR,
98
+ error_code=ErrorCode.CANCELLED,
99
+ )
100
+ ]
101
+ }
102
+
103
+ return {
104
+ "retry_count": count + 1,
105
+ }
106
+
107
+ def check_planner(state: SubgraphExecutionState, config: Optional[RunnableConfig] = None) -> str:
108
+ """Routes based on planner result."""
109
+ if _is_cancelled(config):
110
+ return "end"
111
+ if not (state.ast_planner_response and state.ast_planner_response.plan):
112
+ # If explicit errors exist, check retryability
113
+ if state.errors:
114
+ if not all(e.is_retryable for e in state.errors):
115
+ return "end"
116
+
117
+ if _get_retry_count(state) < settings.sql_agent_max_retries:
118
+ return "retry"
119
+ return "end"
120
+ return "ok"
121
+
122
+ def check_logical_validation(state: SubgraphExecutionState, config: Optional[RunnableConfig] = None) -> str:
123
+ """Routes based on logical validation result."""
124
+ if _is_cancelled(config):
125
+ return "end"
126
+ if state.logical_validator_response and state.logical_validator_response.errors:
127
+ # Critical/Fatal errors stop execution immediately
128
+ if not all(e.is_retryable for e in state.logical_validator_response.errors):
129
+ return "end"
130
+
131
+ if _get_retry_count(state) < settings.sql_agent_max_retries:
132
+ return "retry"
133
+ return "end"
134
+ return "ok"
135
+
136
+ graph.add_node("schema_retriever", schema_retriever)
137
+ graph.add_node("ast_planner", ast_planner)
138
+ graph.add_node("logical_validator", logical_validator)
139
+ graph.add_node("generator", generator)
140
+ graph.add_node("executor", executor)
141
+ graph.add_node("refiner", refiner)
142
+ graph.add_node("retry_handler", retry_node)
143
+
144
+ graph.set_entry_point("schema_retriever")
145
+
146
+ graph.add_edge("schema_retriever", "ast_planner")
147
+
148
+ graph.add_conditional_edges(
149
+ "ast_planner",
150
+ check_planner,
151
+ {"ok": "logical_validator", "retry": "retry_handler", "end": END},
152
+ )
153
+
154
+ graph.add_conditional_edges(
155
+ "logical_validator",
156
+ check_logical_validation,
157
+ {"ok": "generator", "retry": "retry_handler", "end": END},
158
+ )
159
+
160
+ graph.add_edge("generator", "executor")
161
+
162
+ graph.add_edge("executor", END)
163
+
164
+ graph.add_edge("retry_handler", "refiner")
165
+ graph.add_edge("refiner", "ast_planner")
166
+
167
+ return graph.compile()