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,73 @@
1
+ from langgraph.graph import END, StateGraph
2
+
3
+ from nl2sql.context import NL2SQLContext
4
+ from nl2sql.pipeline.graph_utils import SQL_AGENT_SUBGRAPH, wrap_subgraph
5
+ from nl2sql.pipeline.nodes.aggregator import EngineAggregatorNode
6
+ from nl2sql.pipeline.nodes.answer_synthesizer import AnswerSynthesizerNode
7
+ from nl2sql.pipeline.nodes.datasource_resolver import DatasourceResolverNode
8
+ from nl2sql.pipeline.nodes.decomposer import DecomposerNode
9
+ from nl2sql.pipeline.nodes.global_planner import GlobalPlannerNode
10
+ from nl2sql.pipeline.routes import build_scan_layer_router, resolver_route
11
+ from nl2sql.pipeline.state import GraphState
12
+ from nl2sql.pipeline.subgraphs.sql_agent import build_sql_agent_graph
13
+
14
+ def build_graph(
15
+ ctx: NL2SQLContext,
16
+ execute: bool = True,
17
+ ) -> StateGraph:
18
+ """Builds the main LangGraph pipeline.
19
+
20
+ Constructs the graph with Semantic Analysis, Decomposer, Execution branches,
21
+ and Aggregator.
22
+
23
+ Args:
24
+ ctx (NL2SQLContext): The application context containing registries and services.
25
+ execute (bool): Whether to allow execution against real databases.
26
+
27
+ Returns:
28
+ StateGraph: The compiled LangGraph runnable.
29
+ """
30
+ graph = StateGraph(GraphState)
31
+
32
+ resolver_node = DatasourceResolverNode(ctx)
33
+ decomposer_node = DecomposerNode(ctx)
34
+ aggregator_node = EngineAggregatorNode(ctx)
35
+ synthesizer_node = AnswerSynthesizerNode(ctx)
36
+ global_planner_node = GlobalPlannerNode(ctx)
37
+
38
+ sql_agent_subgraph = build_sql_agent_graph(ctx)
39
+
40
+ graph.add_node("datasource_resolver", resolver_node)
41
+ graph.add_node("decomposer", decomposer_node)
42
+ graph.add_node("global_planner", global_planner_node)
43
+ graph.add_node(
44
+ SQL_AGENT_SUBGRAPH,
45
+ wrap_subgraph(sql_agent_subgraph, SQL_AGENT_SUBGRAPH, ctx),
46
+ )
47
+ graph.add_node("aggregator", aggregator_node)
48
+ graph.add_node("answer_synthesizer", synthesizer_node)
49
+ graph.add_node("layer_router", lambda state: {})
50
+
51
+ graph.set_entry_point("datasource_resolver")
52
+
53
+ graph.add_conditional_edges(
54
+ "datasource_resolver",
55
+ resolver_route,
56
+ {"continue": "decomposer", "end": END},
57
+ )
58
+
59
+ graph.add_edge("decomposer", "global_planner")
60
+ route_scan_layers = build_scan_layer_router(ctx)
61
+
62
+ graph.add_edge("global_planner", "layer_router")
63
+ graph.add_conditional_edges(
64
+ "layer_router",
65
+ route_scan_layers,
66
+ [SQL_AGENT_SUBGRAPH, "aggregator", END],
67
+ )
68
+
69
+ graph.add_edge(SQL_AGENT_SUBGRAPH, "layer_router")
70
+ graph.add_edge("aggregator", "answer_synthesizer")
71
+ graph.add_edge("answer_synthesizer", END)
72
+
73
+ return graph.compile()
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Dict, List, Optional
4
+
5
+ from langchain_core.runnables import Runnable, RunnableConfig
6
+
7
+ from nl2sql.context import NL2SQLContext
8
+ from nl2sql.pipeline.nodes.global_planner.schemas import ExecutionDAG
9
+ from nl2sql.pipeline.state import GraphState, SubgraphExecutionState
10
+ from nl2sql.pipeline.subgraphs import SubgraphOutput
11
+ from nl2sql_adapter_sdk.capabilities import DatasourceCapability
12
+ import logging
13
+
14
+ SQL_AGENT_SUBGRAPH = "sql_agent"
15
+ SQL_AGENT_REQUIRED_CAPABILITIES = {DatasourceCapability.SUPPORTS_SQL.value}
16
+
17
+
18
+ class StateAccessor:
19
+ """Adapter for GraphState/dict access to simplify routing logic."""
20
+
21
+ def __init__(self, state: Any):
22
+ self._state = state
23
+
24
+ def get(self, key: str, default: Any = None) -> Any:
25
+ if isinstance(self._state, dict):
26
+ return self._state.get(key, default)
27
+ return getattr(self._state, key, default)
28
+
29
+
30
+ def next_scan_layer_ids(
31
+ dag: ExecutionDAG,
32
+ artifact_refs: Dict[str, Any],
33
+ ) -> List[str]:
34
+ node_index = {n.node_id: n for n in dag.nodes}
35
+ for layer in dag.layers or []:
36
+ pending_scan = [
37
+ node_id
38
+ for node_id in layer
39
+ if node_id in node_index
40
+ and node_index[node_id].kind == "scan"
41
+ and node_id not in artifact_refs
42
+ ]
43
+ if pending_scan:
44
+ return pending_scan
45
+ return []
46
+
47
+
48
+ def resolve_subgraph(
49
+ datasource_id: str,
50
+ ctx: NL2SQLContext,
51
+ ) -> Optional[str]:
52
+ """Returns the subgraph able to serve a datasource, or None if none can.
53
+
54
+ The sql_agent subgraph is the only one, and it requires SUPPORTS_SQL. A
55
+ datasource without that capability resolves to None so the caller fails
56
+ instead of routing it into SQL generation.
57
+ """
58
+ try:
59
+ caps = ctx.ds_registry.get_capabilities(datasource_id)
60
+ except Exception:
61
+ return None
62
+
63
+ if SQL_AGENT_REQUIRED_CAPABILITIES.issubset(caps):
64
+ return SQL_AGENT_SUBGRAPH
65
+ return None
66
+
67
+
68
+ def build_scan_payload(
69
+ state: GraphState,
70
+ subgraph_name: str,
71
+ node_id: str,
72
+ ) -> Dict[str, Any]:
73
+ trace_id = state.trace_id
74
+ return {
75
+ "subgraph_id": f"{subgraph_name}:{node_id}:{trace_id}",
76
+ "subgraph_name": subgraph_name,
77
+ "trace_id": trace_id,
78
+ "user_context": state.user_context,
79
+ "decomposer_response": state.decomposer_response,
80
+ "datasource_resolver_response": state.datasource_resolver_response,
81
+ }
82
+
83
+ def wrap_subgraph(
84
+ subgraph: Runnable,
85
+ subgraph_name: str,
86
+ ctx: NL2SQLContext,
87
+ ) -> Callable[Dict[str, Any]]:
88
+ def _wrapper(state_dict: dict, config: Optional[RunnableConfig] = None) -> Dict[str, Any]:
89
+ trace_id = state_dict.get("trace_id")
90
+ subgraph_id = state_dict.get("subgraph_id")
91
+ sub_query_id = subgraph_id.split(":")[1]
92
+ sub_query = None
93
+ decomposer_response = state_dict.get("decomposer_response")
94
+ for sq in decomposer_response.sub_queries:
95
+ if sq.id == sub_query_id:
96
+ sub_query = sq
97
+ break
98
+
99
+ sub_state = SubgraphExecutionState(
100
+ trace_id=trace_id,
101
+ user_context=state_dict.get("user_context"),
102
+ sub_query=sub_query,
103
+ subgraph_id=subgraph_id,
104
+ subgraph_name=subgraph_name,
105
+ )
106
+ # Propagate the run config (carrying the cancellation token) into the subgraph.
107
+ result = subgraph.invoke(sub_state.model_dump(), config=config)
108
+
109
+ returned_state = SubgraphExecutionState.model_validate(result)
110
+
111
+ executor_response = returned_state.executor_response
112
+ planner_response = returned_state.ast_planner_response
113
+ generator_response = returned_state.generator_response
114
+ sub_reasoning = returned_state.reasoning
115
+ artifact_refs: Dict[str, Any] = {}
116
+ artifact = executor_response.artifact
117
+ artifact_refs[sub_query.id] = artifact
118
+
119
+ retry_count = returned_state.retry_count
120
+ status = "error" if returned_state.errors else "success"
121
+ subgraph_output = SubgraphOutput(
122
+ sub_query=sub_query,
123
+ subgraph_name=subgraph_name,
124
+ subgraph_id=subgraph_id,
125
+ retry_count=retry_count,
126
+ plan=planner_response.plan,
127
+ sql_draft=generator_response.sql_draft if generator_response else None,
128
+ artifact=artifact,
129
+ errors=returned_state.errors,
130
+ reasoning=sub_reasoning,
131
+ status=status,
132
+ )
133
+
134
+ return {
135
+ "artifact_refs": artifact_refs,
136
+ "subgraph_outputs": {subgraph_id: subgraph_output},
137
+ "errors": returned_state.errors,
138
+ "reasoning": returned_state.reasoning,
139
+ }
140
+
141
+ return _wrapper
@@ -0,0 +1,25 @@
1
+ from .decomposer.node import DecomposerNode
2
+ from .datasource_resolver.node import DatasourceResolverNode
3
+ from .global_planner.node import GlobalPlannerNode
4
+ from .aggregator.node import EngineAggregatorNode
5
+ from .ast_planner.node import ASTPlannerNode
6
+ from .schema_retriever.node import SchemaRetrieverNode
7
+ from .generator.node import GeneratorNode
8
+ from .executor.node import ExecutorNode
9
+ from .refiner.node import RefinerNode
10
+ from .validator.node import LogicalValidatorNode
11
+ from .answer_synthesizer.node import AnswerSynthesizerNode
12
+
13
+ __all__ = [
14
+ "ASTPlannerNode",
15
+ "SchemaRetrieverNode",
16
+ "GeneratorNode",
17
+ "ExecutorNode",
18
+ "RefinerNode",
19
+ "DecomposerNode",
20
+ "DatasourceResolverNode",
21
+ "GlobalPlannerNode",
22
+ "LogicalValidatorNode",
23
+ "EngineAggregatorNode",
24
+ "AnswerSynthesizerNode",
25
+ ]
@@ -0,0 +1,4 @@
1
+ from .node import EngineAggregatorNode
2
+ from .schemas import AggregatorResponse
3
+
4
+ __all__ = ["EngineAggregatorNode", "AggregatorResponse"]
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+ from typing import Dict, Any, TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from nl2sql.pipeline.state import GraphState
6
+
7
+ from nl2sql.common.errors import PipelineError, ErrorSeverity, ErrorCode
8
+ from nl2sql.pipeline.nodes.aggregator.schemas import AggregatorResponse
9
+ from nl2sql.pipeline.nodes.global_planner.schemas import GlobalPlannerResponse
10
+ from nl2sql.common.logger import get_logger
11
+ from nl2sql.context import NL2SQLContext
12
+ from nl2sql.aggregation import AggregationService
13
+ from nl2sql.aggregation.engines.polars_duckdb import PolarsDuckdbEngine
14
+
15
+ logger = get_logger("aggregator")
16
+
17
+
18
+ class EngineAggregatorNode:
19
+ """Thin wrapper for aggregation service using ExecutionDAG layers."""
20
+
21
+ def __init__(self, ctx: NL2SQLContext):
22
+ self.node_name = self.__class__.__name__.lower().replace("node", "")
23
+ self.ctx = ctx
24
+ self.service = AggregationService(PolarsDuckdbEngine())
25
+
26
+ def __call__(self, state: GraphState) -> Dict[str, Any]:
27
+ try:
28
+ planner_response = state.global_planner_response
29
+ artifact_refs = state.artifact_refs
30
+
31
+ dag = planner_response.execution_dag
32
+
33
+ terminal_results = self.service.execute(dag, artifact_refs)
34
+ aggregator_response = AggregatorResponse(
35
+ terminal_results=terminal_results,
36
+ computed_artifacts={},
37
+ )
38
+ return {
39
+ "aggregator_response": aggregator_response,
40
+ "reasoning": [{"node": self.node_name, "content": "ExecutionDAG aggregation executed successfully."}],
41
+ }
42
+ except Exception as exc:
43
+ logger.error(f"Node {self.node_name} failed: {exc}")
44
+ return {
45
+ "aggregator_response": AggregatorResponse(),
46
+ "reasoning": [{"node": self.node_name, "content": f"Error: {str(exc)}", "type": "error"}],
47
+ "errors": [
48
+ PipelineError(
49
+ node=self.node_name,
50
+ message=f"Aggregator failed: {str(exc)}",
51
+ severity=ErrorSeverity.ERROR,
52
+ error_code=ErrorCode.AGGREGATOR_FAILED,
53
+ )
54
+ ],
55
+ }
@@ -0,0 +1,20 @@
1
+ """Prompts for the Aggregator node."""
2
+
3
+ AGGREGATOR_PROMPT = """You are an expert data analyst. Your task is to synthesize results from multiple database queries into a single, coherent answer for the user.
4
+
5
+ User Query: {user_query}
6
+
7
+ Intermediate Results from Sub-Queries:
8
+ {intermediate_results}
9
+
10
+ Instructions:
11
+ 1. Analyze the intermediate results.
12
+ 2. Determine the best way to present the combined information to the user (Table, List, or Text).
13
+ - Use 'table' if comparing data or listing structured records with common fields.
14
+ - Use 'list' if enumerating items.
15
+ - Use 'text' if providing a summary or explanation.
16
+ 3. Generate a summary of the findings.
17
+ 4. Format the content accordingly.
18
+
19
+ If the results contain error messages, explain them clearly in the summary.
20
+ """
@@ -0,0 +1,28 @@
1
+ from typing import Literal, List, Dict, Any
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class AggregatedResponse(BaseModel):
6
+ """Structured response for the aggregator node.
7
+
8
+ Attributes:
9
+ summary (str): A concise summary of the aggregated results.
10
+ format_type (Literal): The format to present the data (table, list, text).
11
+ content (str): The aggregated content formatted according to format_type.
12
+ """
13
+ summary: str = Field(description="A concise summary of the aggregated results.")
14
+ format_type: Literal["table", "list", "text"] = Field(
15
+ description="The best format to present the data: 'table' for structured data, 'list' for items, 'text' for narrative."
16
+ )
17
+ content: str = Field(description="The aggregated content formatted according to format_type (e.g., Markdown table, bullet points, or paragraph).")
18
+ warnings: List[str] = Field(
19
+ default_factory=list,
20
+ description="Optional warnings to communicate missing or skipped results.",
21
+ )
22
+
23
+
24
+ class AggregatorResponse(BaseModel):
25
+ terminal_results: Dict[str, List[Dict[str, Any]]] = Field(default_factory=dict)
26
+ computed_artifacts: Dict[str, Any] = Field(default_factory=dict)
27
+ errors: List[Any] = Field(default_factory=list)
28
+ reasoning: List[Dict[str, Any]] = Field(default_factory=list)
@@ -0,0 +1,4 @@
1
+ from .node import AnswerSynthesizerNode
2
+ from .schemas import AggregatedResponse, AnswerSynthesizerResponse
3
+
4
+ __all__ = ["AnswerSynthesizerNode", "AggregatedResponse", "AnswerSynthesizerResponse"]
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Dict, Any, TYPE_CHECKING
5
+
6
+ from langchain_core.prompts import ChatPromptTemplate
7
+
8
+ if TYPE_CHECKING:
9
+ from nl2sql.pipeline.state import GraphState
10
+
11
+ from nl2sql.common.errors import PipelineError, ErrorSeverity, ErrorCode
12
+ from nl2sql.common.logger import get_logger
13
+ from nl2sql.context import NL2SQLContext
14
+ from .schemas import AggregatedResponse, AnswerSynthesizerResponse
15
+ from .prompts import ANSWER_SYNTHESIZER_PROMPT
16
+
17
+ logger = get_logger("answer_synthesizer")
18
+
19
+
20
+ class AnswerSynthesizerNode:
21
+ """Summarizes aggregated results into a user-facing answer."""
22
+
23
+ def __init__(self, ctx: NL2SQLContext):
24
+ self.node_name = self.__class__.__name__.lower().replace("node", "")
25
+ self.llm = ctx.llm_registry.get_llm(self.node_name)
26
+ self.prompt = ChatPromptTemplate.from_template(ANSWER_SYNTHESIZER_PROMPT)
27
+ self.chain = self.prompt | self.llm.with_structured_output(
28
+ AggregatedResponse, method="function_calling"
29
+ )
30
+
31
+ def _serialize_result(self, result: Any) -> str:
32
+ try:
33
+ return json.dumps(result, indent=2, ensure_ascii=True)
34
+ except TypeError:
35
+ return str(result)
36
+
37
+ def __call__(self, state: GraphState) -> Dict[str, Any]:
38
+ aggregated_result = None
39
+ if state.aggregator_response:
40
+ aggregated_result = state.aggregator_response.terminal_results
41
+ elif state.answer_synthesizer_response and state.answer_synthesizer_response.final_answer is not None:
42
+ aggregated_result = state.answer_synthesizer_response.final_answer
43
+
44
+ if aggregated_result is None:
45
+ return {
46
+ "errors": [
47
+ PipelineError(
48
+ node=self.node_name,
49
+ message="No aggregated result available for synthesis.",
50
+ severity=ErrorSeverity.ERROR,
51
+ error_code=ErrorCode.INVALID_STATE,
52
+ )
53
+ ]
54
+ }
55
+
56
+ try:
57
+ unmapped_subqueries = []
58
+ if state.decomposer_response:
59
+ unmapped_subqueries = [
60
+ u.model_dump()
61
+ for u in (state.decomposer_response.unmapped_subqueries or [])
62
+ ]
63
+ response: AggregatedResponse = self.chain.invoke(
64
+ {
65
+ "user_query": state.user_query,
66
+ "aggregated_result": self._serialize_result(aggregated_result),
67
+ "unmapped_subqueries": json.dumps(
68
+ unmapped_subqueries, indent=2, ensure_ascii=True
69
+ ),
70
+ }
71
+ )
72
+
73
+
74
+ return {
75
+ "answer_synthesizer_response": AnswerSynthesizerResponse(
76
+ final_answer=response.model_dump(),
77
+ ),
78
+ "reasoning": [
79
+ {
80
+ "node": self.node_name,
81
+ "content": response.summary,
82
+ }
83
+ ],
84
+ }
85
+
86
+ except Exception as exc:
87
+ logger.error(f"Node {self.node_name} failed: {exc}")
88
+ return {
89
+ "answer_synthesizer_response": AnswerSynthesizerResponse(),
90
+ "errors": [
91
+ PipelineError(
92
+ node=self.node_name,
93
+ message=f"Answer synthesis failed: {exc}",
94
+ severity=ErrorSeverity.ERROR,
95
+ error_code=ErrorCode.AGGREGATOR_FAILED,
96
+ )
97
+ ]
98
+ }
@@ -0,0 +1,19 @@
1
+ """Prompts for the AnswerSynthesizer node."""
2
+
3
+ ANSWER_SYNTHESIZER_PROMPT = """You are a data analyst. Summarize the aggregated results for the user.
4
+
5
+ User Query: {user_query}
6
+
7
+ Aggregated Results (keyed by terminal node id):
8
+ {aggregated_result}
9
+
10
+ Unmapped Subqueries (if any):
11
+ {unmapped_subqueries}
12
+
13
+ Instructions:
14
+ 1. Provide a concise summary.
15
+ 2. Choose the best output format: table, list, or text.
16
+ 3. Produce the formatted content for the chosen format.
17
+ 4. If results contain error messages, explain them clearly.
18
+ 5. If there are unmapped subqueries, add user-facing warnings that explain what was skipped and why.
19
+ """
@@ -0,0 +1,24 @@
1
+ from typing import Literal, List, Dict, Any, Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class AggregatedResponse(BaseModel):
7
+ """Structured response for the answer synthesizer."""
8
+ summary: str = Field(description="A concise summary of the aggregated results.")
9
+ format_type: Literal["table", "list", "text"] = Field(
10
+ description="The best format to present the data: 'table' for structured data, 'list' for items, 'text' for narrative."
11
+ )
12
+ content: str = Field(
13
+ description="The aggregated content formatted according to format_type (e.g., Markdown table, bullet points, or paragraph)."
14
+ )
15
+ warnings: List[str] = Field(
16
+ default_factory=list,
17
+ description="Optional warnings to communicate missing or skipped results.",
18
+ )
19
+
20
+
21
+ class AnswerSynthesizerResponse(BaseModel):
22
+ final_answer: Optional[Dict[str, Any]] = None
23
+ errors: List[Any] = Field(default_factory=list)
24
+ reasoning: List[Dict[str, Any]] = Field(default_factory=list)
@@ -0,0 +1,4 @@
1
+ from .node import ASTPlannerNode
2
+ from .schemas import ASTPlannerResponse
3
+
4
+ __all__ = ["ASTPlannerNode", "ASTPlannerResponse"]
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+ import traceback
3
+ from typing import Any, Dict, Optional, TYPE_CHECKING
4
+ from langchain_core.runnables import Runnable
5
+ from langchain_core.prompts import ChatPromptTemplate
6
+
7
+ from .prompts import PLANNER_PROMPT, PLANNER_EXAMPLES
8
+ from .schemas import PlanModel, ASTPlannerResponse
9
+ from nl2sql.common.errors import PipelineError, ErrorSeverity, ErrorCode
10
+ from nl2sql.common.logger import get_logger
11
+ from nl2sql.context import NL2SQLContext
12
+
13
+ if TYPE_CHECKING:
14
+ from nl2sql.pipeline.state import SubgraphExecutionState
15
+
16
+ logger = get_logger("planner")
17
+
18
+
19
+ class ASTPlannerNode:
20
+ """Generates a structured SQL execution plan (PlanModel).
21
+
22
+ Uses an LLM to interpret the user query and semantic context, producing a
23
+ deterministic Abstract Syntax Tree (AST) that represents the SQL query.
24
+
25
+ Attributes:
26
+ llm (Optional[Runnable]): The Language Model executable.
27
+ chain (Optional[Runnable]): The langchain chain for planning.
28
+ """
29
+
30
+ def __init__(self, ctx: NL2SQLContext):
31
+ """Initializes the PlannerNode.
32
+
33
+ Args:
34
+ ctx (NL2SQLContext): The context of the pipeline.
35
+ """
36
+ self.node_name = self.__class__.__name__.lower().replace('node', '')
37
+ self.llm = ctx.llm_registry.get_llm(self.node_name)
38
+
39
+ self.prompt = ChatPromptTemplate.from_template(PLANNER_PROMPT)
40
+ self.chain = self.prompt | self.llm.with_structured_output(PlanModel)
41
+
42
+ def __call__(self, state: SubgraphExecutionState) -> Dict[str, Any]:
43
+ """Executes the planning node.
44
+
45
+ Args:
46
+ state (GraphState): The current state of the execution graph.
47
+
48
+ Returns:s
49
+ Dict[str, Any]: A dictionary containing the generated 'plan', 'reasoning',
50
+ and any 'errors' encountered.
51
+ """
52
+ try:
53
+ relevant_tables = '\n'.join(
54
+ t.model_dump_json(indent=2) for t in state.relevant_tables
55
+ )
56
+
57
+
58
+ feedback = ""
59
+ if state.errors:
60
+ feedback = "\n".join(e.model_dump_json(indent=2) for e in state.errors)
61
+
62
+ query_text = state.sub_query.intent if state.sub_query else ""
63
+ expected_schema = []
64
+ if state.sub_query and state.sub_query.expected_schema:
65
+ expected_schema = [c.model_dump() for c in state.sub_query.expected_schema]
66
+ plan: PlanModel = self.chain.invoke(
67
+ {
68
+ "relevant_tables": relevant_tables,
69
+ "examples": PLANNER_EXAMPLES,
70
+ "feedback": feedback,
71
+ "expected_schema": expected_schema,
72
+ "semantic_context": "",
73
+ "user_query": query_text,
74
+ }
75
+ )
76
+
77
+ return {
78
+ "ast_planner_response": ASTPlannerResponse(plan=plan),
79
+ "reasoning": [
80
+ {
81
+ "node": self.node_name,
82
+ "content": [
83
+ f"Reasoning: {plan.reasoning or 'None'}",
84
+ f"Tables: {', '.join(t.name for t in plan.tables)}",
85
+ ],
86
+ }
87
+ ],
88
+ "errors": [],
89
+ }
90
+
91
+ except Exception as exc:
92
+ logger.exception("Planner failed")
93
+ return {
94
+ "ast_planner_response": ASTPlannerResponse(plan=None),
95
+ "errors": [
96
+ PipelineError(
97
+ node=self.node_name,
98
+ message="Planner failed.",
99
+ severity=ErrorSeverity.ERROR,
100
+ error_code=ErrorCode.PLANNING_FAILURE,
101
+ stack_trace=traceback.format_exc(),
102
+ )
103
+ ],
104
+ }