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
nl2sql/pipeline/graph.py
ADDED
|
@@ -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,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,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,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
|
+
}
|