nl2sql-engine 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nl2sql/__init__.py +38 -0
- nl2sql/adapters/__init__.py +0 -0
- nl2sql/adapters/duckdb/__init__.py +0 -0
- nl2sql/adapters/duckdb/adapter.py +71 -0
- nl2sql/adapters/mssql/__init__.py +0 -0
- nl2sql/adapters/mssql/adapter.py +122 -0
- nl2sql/adapters/mysql/__init__.py +0 -0
- nl2sql/adapters/mysql/adapter.py +123 -0
- nl2sql/adapters/postgres/__init__.py +0 -0
- nl2sql/adapters/postgres/adapter.py +115 -0
- nl2sql/adapters/sqlalchemy_base/__init__.py +17 -0
- nl2sql/adapters/sqlalchemy_base/adapter.py +476 -0
- nl2sql/adapters/sqlalchemy_base/models.py +36 -0
- nl2sql/adapters/sqlite/__init__.py +0 -0
- nl2sql/adapters/sqlite/adapter.py +88 -0
- nl2sql/aggregation/__init__.py +3 -0
- nl2sql/aggregation/aggregator.py +98 -0
- nl2sql/aggregation/engines/__init__.py +3 -0
- nl2sql/aggregation/engines/polars_duckdb.py +125 -0
- nl2sql/api/__init__.py +0 -0
- nl2sql/api/auth_api.py +60 -0
- nl2sql/api/benchmark_api.py +114 -0
- nl2sql/api/datasource_api.py +132 -0
- nl2sql/api/indexing_api.py +59 -0
- nl2sql/api/llm_api.py +82 -0
- nl2sql/api/policy_api.py +135 -0
- nl2sql/api/query_api.py +138 -0
- nl2sql/api/result_api.py +24 -0
- nl2sql/api/settings_api.py +65 -0
- nl2sql/auth/__init__.py +8 -0
- nl2sql/auth/models.py +36 -0
- nl2sql/auth/rbac.py +25 -0
- nl2sql/cli/__init__.py +0 -0
- nl2sql/cli/checks.py +53 -0
- nl2sql/cli/commands/__init__.py +0 -0
- nl2sql/cli/commands/benchmark.py +34 -0
- nl2sql/cli/commands/doctor.py +49 -0
- nl2sql/cli/commands/indexing.py +126 -0
- nl2sql/cli/commands/info.py +25 -0
- nl2sql/cli/commands/install.py +27 -0
- nl2sql/cli/commands/policy.py +57 -0
- nl2sql/cli/commands/run.py +166 -0
- nl2sql/cli/commands/setup.py +415 -0
- nl2sql/cli/commands/visualize.py +34 -0
- nl2sql/cli/common/decorators.py +34 -0
- nl2sql/cli/config.py +24 -0
- nl2sql/cli/console.py +52 -0
- nl2sql/cli/demo/__init__.py +1 -0
- nl2sql/cli/demo/data.py +87 -0
- nl2sql/cli/demo/defaults.py +122 -0
- nl2sql/cli/demo/factory.py +289 -0
- nl2sql/cli/demo/manager.py +230 -0
- nl2sql/cli/demo/schemas.py +336 -0
- nl2sql/cli/demo/writers/__init__.py +0 -0
- nl2sql/cli/demo/writers/docker.py +182 -0
- nl2sql/cli/demo/writers/sqlite.py +88 -0
- nl2sql/cli/generators/datasources/__init__.py +3 -0
- nl2sql/cli/generators/datasources/generator.py +24 -0
- nl2sql/cli/generators/datasources/templates.py +7 -0
- nl2sql/cli/generators/env/__init__.py +3 -0
- nl2sql/cli/generators/env/generator.py +46 -0
- nl2sql/cli/generators/env/templates.py +25 -0
- nl2sql/cli/generators/llm/__init__.py +3 -0
- nl2sql/cli/generators/llm/generator.py +24 -0
- nl2sql/cli/generators/llm/templates.py +4 -0
- nl2sql/cli/generators/policies/__init__.py +3 -0
- nl2sql/cli/generators/policies/generator.py +20 -0
- nl2sql/cli/generators/policies/templates.py +2 -0
- nl2sql/cli/main.py +195 -0
- nl2sql/cli/reporting.py +878 -0
- nl2sql/cli/types.py +13 -0
- nl2sql/common/__init__.py +1 -0
- nl2sql/common/cancellation.py +25 -0
- nl2sql/common/context.py +5 -0
- nl2sql/common/errors.py +109 -0
- nl2sql/common/event_logger.py +88 -0
- nl2sql/common/exceptions.py +3 -0
- nl2sql/common/logger.py +119 -0
- nl2sql/common/metrics.py +50 -0
- nl2sql/common/resilience.py +59 -0
- nl2sql/common/settings.py +195 -0
- nl2sql/configs/__init__.py +6 -0
- nl2sql/configs/datasources.py +10 -0
- nl2sql/configs/llm.py +36 -0
- nl2sql/configs/manager.py +176 -0
- nl2sql/configs/policies.py +14 -0
- nl2sql/configs/sample_questions.py +11 -0
- nl2sql/configs/secrets.py +11 -0
- nl2sql/context.py +106 -0
- nl2sql/datasources/__init__.py +21 -0
- nl2sql/datasources/discovery.py +28 -0
- nl2sql/datasources/models.py +21 -0
- nl2sql/datasources/protocols.py +3 -0
- nl2sql/datasources/registry.py +172 -0
- nl2sql/evaluation/__init__.py +6 -0
- nl2sql/evaluation/benchmark_runner.py +320 -0
- nl2sql/evaluation/evaluator.py +134 -0
- nl2sql/evaluation/types.py +22 -0
- nl2sql/execution/__init__.py +4 -0
- nl2sql/execution/artifacts/__init__.py +3 -0
- nl2sql/execution/artifacts/parquet.py +41 -0
- nl2sql/execution/artifacts/store.py +165 -0
- nl2sql/execution/contracts.py +57 -0
- nl2sql/execution/execution_store.py +25 -0
- nl2sql/execution/executor/__init__.py +3 -0
- nl2sql/execution/executor/sql_executor.py +116 -0
- nl2sql/indexing/__init__.py +7 -0
- nl2sql/indexing/chunk_builder.py +227 -0
- nl2sql/indexing/embeddings.py +180 -0
- nl2sql/indexing/enrichment_service.py +316 -0
- nl2sql/indexing/models.py +209 -0
- nl2sql/indexing/orchestrator.py +90 -0
- nl2sql/indexing/vector_store.py +422 -0
- nl2sql/llm/__init__.py +8 -0
- nl2sql/llm/models.py +10 -0
- nl2sql/llm/registry.py +214 -0
- nl2sql/pipeline/__init__.py +1 -0
- nl2sql/pipeline/graph.py +73 -0
- nl2sql/pipeline/graph_utils.py +141 -0
- nl2sql/pipeline/nodes/__init__.py +25 -0
- nl2sql/pipeline/nodes/aggregator/__init__.py +4 -0
- nl2sql/pipeline/nodes/aggregator/node.py +55 -0
- nl2sql/pipeline/nodes/aggregator/prompts.py +20 -0
- nl2sql/pipeline/nodes/aggregator/schemas.py +28 -0
- nl2sql/pipeline/nodes/answer_synthesizer/__init__.py +4 -0
- nl2sql/pipeline/nodes/answer_synthesizer/node.py +98 -0
- nl2sql/pipeline/nodes/answer_synthesizer/prompts.py +19 -0
- nl2sql/pipeline/nodes/answer_synthesizer/schemas.py +24 -0
- nl2sql/pipeline/nodes/ast_planner/__init__.py +4 -0
- nl2sql/pipeline/nodes/ast_planner/node.py +104 -0
- nl2sql/pipeline/nodes/ast_planner/prompts.py +138 -0
- nl2sql/pipeline/nodes/ast_planner/schemas.py +236 -0
- nl2sql/pipeline/nodes/datasource_resolver/__init__.py +4 -0
- nl2sql/pipeline/nodes/datasource_resolver/node.py +253 -0
- nl2sql/pipeline/nodes/datasource_resolver/schemas.py +21 -0
- nl2sql/pipeline/nodes/decomposer/__init__.py +3 -0
- nl2sql/pipeline/nodes/decomposer/node.py +219 -0
- nl2sql/pipeline/nodes/decomposer/prompts.py +96 -0
- nl2sql/pipeline/nodes/decomposer/schemas.py +143 -0
- nl2sql/pipeline/nodes/executor/__init__.py +3 -0
- nl2sql/pipeline/nodes/executor/node.py +107 -0
- nl2sql/pipeline/nodes/generator/__init__.py +4 -0
- nl2sql/pipeline/nodes/generator/node.py +267 -0
- nl2sql/pipeline/nodes/generator/schemas.py +13 -0
- nl2sql/pipeline/nodes/global_planner/__init__.py +4 -0
- nl2sql/pipeline/nodes/global_planner/node.py +186 -0
- nl2sql/pipeline/nodes/global_planner/schemas.py +101 -0
- nl2sql/pipeline/nodes/refiner/__init__.py +4 -0
- nl2sql/pipeline/nodes/refiner/node.py +132 -0
- nl2sql/pipeline/nodes/refiner/prompts.py +28 -0
- nl2sql/pipeline/nodes/refiner/schemas.py +13 -0
- nl2sql/pipeline/nodes/schema_retriever/__init__.py +3 -0
- nl2sql/pipeline/nodes/schema_retriever/node.py +252 -0
- nl2sql/pipeline/nodes/schema_retriever/schema.py +27 -0
- nl2sql/pipeline/nodes/validator/__init__.py +7 -0
- nl2sql/pipeline/nodes/validator/node.py +839 -0
- nl2sql/pipeline/nodes/validator/schemas.py +12 -0
- nl2sql/pipeline/pipeline_runner.py +72 -0
- nl2sql/pipeline/routes.py +72 -0
- nl2sql/pipeline/runtime.py +153 -0
- nl2sql/pipeline/state.py +92 -0
- nl2sql/pipeline/subgraphs/__init__.py +5 -0
- nl2sql/pipeline/subgraphs/schemas.py +23 -0
- nl2sql/pipeline/subgraphs/sql_agent.py +167 -0
- nl2sql/public_api.py +199 -0
- nl2sql/schema/__init__.py +37 -0
- nl2sql/schema/in_memory_store.py +173 -0
- nl2sql/schema/protocol.py +88 -0
- nl2sql/schema/sqlite_store.py +233 -0
- nl2sql/schema/store.py +29 -0
- nl2sql/secrets/__init__.py +14 -0
- nl2sql/secrets/factory.py +85 -0
- nl2sql/secrets/interfaces.py +16 -0
- nl2sql/secrets/manager.py +139 -0
- nl2sql/secrets/models.py +56 -0
- nl2sql/secrets/providers/aws.py +30 -0
- nl2sql/secrets/providers/azure.py +49 -0
- nl2sql/secrets/providers/env.py +8 -0
- nl2sql/secrets/providers/hashi.py +46 -0
- nl2sql/services/__init__.py +0 -0
- nl2sql/services/callbacks/__init__.py +0 -0
- nl2sql/services/callbacks/monitor.py +84 -0
- nl2sql/services/callbacks/node_context.py +7 -0
- nl2sql/services/callbacks/node_handlers.py +187 -0
- nl2sql/services/callbacks/node_metrics.py +14 -0
- nl2sql/services/callbacks/presenter.py +12 -0
- nl2sql/services/callbacks/token_handler.py +56 -0
- nl2sql_engine-0.1.0.dist-info/METADATA +295 -0
- nl2sql_engine-0.1.0.dist-info/RECORD +192 -0
- nl2sql_engine-0.1.0.dist-info/WHEEL +5 -0
- nl2sql_engine-0.1.0.dist-info/entry_points.txt +9 -0
- nl2sql_engine-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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()
|
nl2sql/pipeline/state.py
ADDED
|
@@ -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,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()
|