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,126 @@
1
+ import json
2
+ import sys
3
+ from typing import Any, Dict
4
+ from nl2sql.cli.reporting import ConsolePresenter
5
+ from nl2sql.indexing.vector_store import VectorStore
6
+ from nl2sql.datasources import DatasourceRegistry
7
+ from nl2sql.context import NL2SQLContext
8
+ from nl2sql.cli.common.decorators import handle_cli_errors
9
+ from nl2sql.indexing.orchestrator import IndexingOrchestrator
10
+
11
+ @handle_cli_errors
12
+ def run_indexing(
13
+ ctx: NL2SQLContext,
14
+ ) -> None:
15
+ """
16
+ Runs schema indexing for all registered datasources.
17
+
18
+ This command clears the existing vector store and indexes
19
+ schema chunks for each configured datasource using the
20
+ indexing orchestrator.
21
+
22
+ Args:
23
+ ctx: The initialized NL2SQLContext.
24
+
25
+ Raises:
26
+ SystemExit: With code 1 if the store could not be cleared or if any
27
+ datasource failed to index.
28
+ """
29
+ presenter = ConsolePresenter()
30
+ presenter.print_info(f"Indexing schema to: {ctx.vector_store.persist_directory}")
31
+
32
+ adapters = ctx.ds_registry.list_adapters()
33
+ orchestrator = IndexingOrchestrator(ctx)
34
+ stats = []
35
+ errors = []
36
+ empty_stats = []
37
+
38
+ presenter.start_interactive_status("Clearing existing data...")
39
+ try:
40
+ orchestrator.clear_store()
41
+ except Exception as e:
42
+ presenter.stop_interactive_status()
43
+ presenter.print_error(f"Failed to clear existing data: {e}")
44
+ sys.exit(1)
45
+ presenter.stop_interactive_status()
46
+ presenter.print_success("Cleared existing data.")
47
+
48
+ for adapter in adapters:
49
+ ds_id = adapter.datasource_id
50
+
51
+ try:
52
+ task = presenter.start_task_line(f"Indexing {ds_id}...")
53
+ schema_stats = orchestrator.index_datasource(adapter)
54
+ if schema_stats:
55
+ stats.append(schema_stats)
56
+ else:
57
+ empty_stats.append(ds_id)
58
+ presenter.finish_task_line(task, f"{ds_id} indexed", success=True)
59
+ except Exception as e:
60
+ if "task" in locals():
61
+ presenter.finish_task_line(task, f"{ds_id} failed", success=False)
62
+ presenter.print_error(f"Failed to index {ds_id}: {e}")
63
+ errors.append(
64
+ {"datasource_id": ds_id, "error": str(e)}
65
+ )
66
+
67
+ if errors:
68
+ presenter.print_table(errors, "Indexing Errors", columns=["datasource_id", "error"])
69
+
70
+ if stats:
71
+ summary_rows = []
72
+ total_chunks = 0
73
+ totals_by_type: Dict[str, int] = {}
74
+
75
+ for s in stats:
76
+ ds_id = s.get("datasource_id", "unknown")
77
+ schema_version = s.get("schema_version", "-")
78
+ chunk_stats = {
79
+ k: v for k, v in s.items()
80
+ if k not in ("datasource_id", "schema_version")
81
+ }
82
+ ds_total = sum(v for v in chunk_stats.values() if isinstance(v, int))
83
+ total_chunks += ds_total
84
+ for key, val in chunk_stats.items():
85
+ if isinstance(val, int):
86
+ totals_by_type[key] = totals_by_type.get(key, 0) + val
87
+
88
+ summary_rows.append(
89
+ {
90
+ "datasource_id": ds_id,
91
+ "schema_version": schema_version,
92
+ "total_chunks": ds_total,
93
+ "chunks": json.dumps(chunk_stats, separators=(",", ":")),
94
+ }
95
+ )
96
+
97
+ presenter.print_table(
98
+ summary_rows,
99
+ "Indexing Summary",
100
+ columns=["datasource_id", "schema_version", "total_chunks", "chunks"],
101
+ )
102
+
103
+ if totals_by_type:
104
+ totals_str = ", ".join([f"{k}={v}" for k, v in sorted(totals_by_type.items())])
105
+ presenter.print_info(f"Total chunks indexed: {total_chunks} ({totals_str})")
106
+
107
+ total_adapters = len(adapters)
108
+ succeeded = len(stats)
109
+ failed = len(errors)
110
+ skipped = len(empty_stats)
111
+ presenter.print_info(
112
+ f"Datasources: total={total_adapters}, succeeded={succeeded}, failed={failed}, empty={skipped}"
113
+ )
114
+
115
+ if empty_stats:
116
+ presenter.print_warning(f"Datasources with empty stats: {', '.join(empty_stats)}")
117
+
118
+ if errors:
119
+ # Any failure is fatal, not just a total one: a partially populated
120
+ # index answers later queries from an incomplete schema, which is worse
121
+ # than a loud stop because it looks like it worked. Scripts chaining
122
+ # `nl2sql index && nl2sql run ...` need that signal.
123
+ presenter.print_warning("Indexing completed with errors.")
124
+ sys.exit(1)
125
+
126
+ presenter.print_success("Indexing complete.")
@@ -0,0 +1,25 @@
1
+ from rich.console import Console
2
+ from rich.table import Table
3
+ from nl2sql.datasources.discovery import discover_adapters
4
+
5
+ from nl2sql.cli.common.decorators import handle_cli_errors
6
+
7
+ @handle_cli_errors
8
+ def list_available_adapters() -> None:
9
+ """Discovers and displays all installed Datasource Adapters."""
10
+ console = Console()
11
+ adapters = discover_adapters()
12
+
13
+ if not adapters:
14
+ console.print("[yellow]No adapters found. Please install a driver extra (e.g., nl2sql\[postgres]).[/yellow]")
15
+ return
16
+
17
+ table = Table(title="Installed Datasource Adapters")
18
+ table.add_column("Adapter ID", style="cyan", no_wrap=True)
19
+ table.add_column("Class", style="magenta")
20
+ table.add_column("Status", style="green")
21
+
22
+ for name, cls in adapters.items():
23
+ table.add_row(name, f"{cls.__module__}.{cls.__name__}", "Active")
24
+
25
+ console.print(table)
@@ -0,0 +1,27 @@
1
+ import sys
2
+ import subprocess
3
+ from rich.markup import escape
4
+ from rich.prompt import Confirm
5
+ from nl2sql.cli.config import KNOWN_ADAPTERS
6
+ from nl2sql.cli.console import console, print_success, print_error, print_step
7
+
8
+ def install_package(package_name: str) -> bool:
9
+ print_step(f"Installing {package_name}...")
10
+ try:
11
+ subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])
12
+ print_success(f"Installed {package_name}")
13
+ return True
14
+ except subprocess.CalledProcessError:
15
+ print_error(f"Failed to install {package_name}")
16
+ return False
17
+
18
+ from nl2sql.cli.common.decorators import handle_cli_errors
19
+
20
+ @handle_cli_errors
21
+ def install_command(adapter_name: str):
22
+ target_pkg = adapter_name
23
+ if adapter_name in KNOWN_ADAPTERS:
24
+ target_pkg = KNOWN_ADAPTERS[adapter_name]
25
+
26
+ if Confirm.ask(f"Install [cyan]{escape(str(target_pkg))}[/cyan]?"):
27
+ install_package(target_pkg)
@@ -0,0 +1,57 @@
1
+ import typer
2
+ import pathlib
3
+ import sys
4
+ from typing import Optional
5
+ from typing_extensions import Annotated
6
+ from rich.console import Console
7
+ from rich.markup import escape
8
+ from rich.text import Text
9
+ from rich.table import Table
10
+
11
+ from nl2sql import PolicyAPI
12
+
13
+ app = typer.Typer(help="Manage RBAC policies and security.")
14
+ console = Console()
15
+
16
+ from nl2sql.cli.common.decorators import handle_cli_errors
17
+
18
+ @app.command("validate")
19
+ @handle_cli_errors
20
+ def validate(
21
+ config: Annotated[Optional[pathlib.Path], typer.Option("--config", help="Path to datasource config")] = None,
22
+ policies: Annotated[Optional[pathlib.Path], typer.Option("--policies", help="Path to policies.json")] = None,
23
+ secrets: Annotated[Optional[pathlib.Path], typer.Option("--secrets", help="Path to secrets config")] = None,
24
+ ):
25
+ """
26
+ Validate policy syntax and integrity against defined datasources.
27
+ """
28
+ console.print(f"[bold blue]Validating Policies from:[/bold blue] {escape(str(policies or 'default'))}")
29
+
30
+ api = PolicyAPI()
31
+ report = api.validate_policies(policies_path=policies, datasources_path=config, secrets_path=secrets)
32
+
33
+ if report.errors:
34
+ console.print(f"[bold red]Schema Validation Failed:[/bold red]\\n{escape(str(report.errors[0]))}")
35
+ sys.exit(1)
36
+
37
+ console.print(f"[bold blue]Checking Integrity against Datasources:[/bold blue] {escape(str(config or 'default'))}")
38
+ if report.available_datasources:
39
+ console.print(f"[dim]Available Datasources: {report.available_datasources}[/dim]")
40
+
41
+ table = Table(title="Policy Integrity Report")
42
+ table.add_column("Role", style="cyan")
43
+ table.add_column("Target", style="magenta")
44
+ table.add_column("Status", style="green")
45
+ table.add_column("Details", style="white")
46
+
47
+ for entry in report.entries:
48
+ status = "[green]OK[/green]" if entry.status == "OK" else f"[red]{entry.status}[/red]"
49
+ table.add_row(Text(str(entry.role)), Text(str(entry.target)), status, Text(str(entry.details)))
50
+
51
+ console.print(table)
52
+
53
+ if not report.ok:
54
+ console.print("\n[bold red]Integrity Check Failed: Policies reference missing resources.[/bold red]")
55
+ sys.exit(1)
56
+ else:
57
+ console.print("\n[bold green]✓ Policy Integrity Verified[/bold green]")
@@ -0,0 +1,166 @@
1
+ import sys
2
+ import json
3
+
4
+ from rich.text import Text
5
+
6
+ from nl2sql.datasources import DatasourceRegistry
7
+ from nl2sql.llm import LLMRegistry
8
+ from nl2sql.indexing.vector_store import VectorStore
9
+ from nl2sql.cli.reporting import ConsolePresenter
10
+ from nl2sql.pipeline.pipeline_runner import PipelineRunner
11
+ from nl2sql.common.settings import settings
12
+ from nl2sql.cli.types import RunConfig
13
+ from nl2sql.cli.common.decorators import handle_cli_errors
14
+ from nl2sql.context import NL2SQLContext
15
+
16
+ @handle_cli_errors
17
+ def run_pipeline(
18
+ config: RunConfig,
19
+ ctx: NL2SQLContext
20
+ ) -> None:
21
+ """Executes the NL2SQL pipeline."""
22
+ if not config.query:
23
+ return
24
+
25
+ presenter = ConsolePresenter()
26
+ presenter.print_info(f"Query: {config.query}")
27
+ if config.no_exec:
28
+ presenter.print_warning("Execution disabled (no_exec). Only SQL/plan output will be shown.")
29
+
30
+ # Instantiate Runner
31
+ runner = PipelineRunner(ctx)
32
+
33
+ # Setup Monitoring
34
+ from nl2sql.services.callbacks.monitor import PipelineMonitorCallback
35
+ monitor = PipelineMonitorCallback(presenter)
36
+
37
+ presenter.start_interactive_status("Thinking...")
38
+
39
+ # Execution
40
+ result = runner.run(
41
+ query=config.query,
42
+ role=config.role,
43
+ datasource_id=config.ds_id,
44
+ execute=not config.no_exec,
45
+ callbacks=[monitor]
46
+ )
47
+
48
+ presenter.stop_interactive_status()
49
+
50
+ # Handle Result
51
+ if not result.success:
52
+ if result.traceback:
53
+ presenter.print_error(result.traceback)
54
+ presenter.print_error(result.error or "Unknown Pipeline Error")
55
+ sys.exit(1)
56
+
57
+ final_state = result.final_state
58
+
59
+
60
+ query_history = []
61
+ subgraph_outputs = final_state.get("subgraph_outputs") or {}
62
+ for _subgraph_id, output in subgraph_outputs.items():
63
+ if isinstance(output, dict):
64
+ sub_query = output.get("sub_query")
65
+ sql_draft = output.get("sql_draft")
66
+ else:
67
+ sub_query = getattr(output, "sub_query", None)
68
+ sql_draft = getattr(output, "sql_draft", None)
69
+
70
+ if not sub_query:
71
+ continue
72
+
73
+ if isinstance(sub_query, dict):
74
+ ds_id = sub_query.get("datasource_id")
75
+ intent = sub_query.get("intent")
76
+ else:
77
+ ds_id = getattr(sub_query, "datasource_id", None)
78
+ intent = getattr(sub_query, "intent", None)
79
+
80
+ query_history.append({
81
+ "sub_query": intent,
82
+ "datasource_id": ds_id,
83
+ "sql": sql_draft,
84
+ })
85
+
86
+ if config.verbose:
87
+ reasoning = final_state.get("reasoning", [])
88
+ presenter.print_execution_tree(config.query, query_history, top_level_reasoning=reasoning)
89
+
90
+ try:
91
+ with open("last_reasoning.json", "w") as f:
92
+ dump_data = {
93
+ "global_reasoning": reasoning,
94
+ "execution_history": query_history
95
+ }
96
+ json.dump(dump_data, f, indent=2, default=str)
97
+ presenter.print_info("Detailed reasoning trace saved to last_reasoning.json")
98
+ except Exception:
99
+ pass
100
+
101
+ if query_history:
102
+ datasources_used = sorted({item.get("datasource_id") for item in query_history if item.get("datasource_id")})
103
+ if datasources_used:
104
+ presenter.print_info(f"Datasources used: {', '.join(datasources_used)}")
105
+
106
+ for item in query_history:
107
+ ds = item.get("datasource_id", "Unknown")
108
+ sub_query = item.get("sub_query")
109
+ sql = item.get("sql")
110
+
111
+ if sql:
112
+ body = Text()
113
+ if sub_query:
114
+ body.append(f"Sub-Query: {sub_query}\n", style="bold")
115
+ body.append(f"Datasource: {ds}\n\n", style="bold")
116
+ body.append(str(sql))
117
+ presenter.print_sql(body)
118
+
119
+ elif final_state.get("sql_draft"):
120
+ sql_draft_data = final_state.get("sql_draft")
121
+ sql_draft = sql_draft_data.get("sql") if isinstance(sql_draft_data, dict) else getattr(sql_draft_data, "sql", None)
122
+ if sql_draft:
123
+ presenter.print_sql(str(sql_draft))
124
+
125
+ errors = final_state.get("errors")
126
+ if errors:
127
+ presenter.print_pipeline_errors(errors)
128
+
129
+ warnings = final_state.get("warnings") or []
130
+ for warning in warnings:
131
+ if isinstance(warning, dict):
132
+ presenter.print_warning(json.dumps(warning, default=str))
133
+ else:
134
+ presenter.print_warning(str(warning))
135
+
136
+ answer_payload = None
137
+ answer_synth = final_state.get("answer_synthesizer_response")
138
+ if answer_synth:
139
+ if isinstance(answer_synth, dict):
140
+ answer_payload = answer_synth.get("final_answer")
141
+ else:
142
+ answer_payload = getattr(answer_synth, "final_answer", None)
143
+ if isinstance(answer_payload, dict) and answer_payload:
144
+ presenter.print_answer_synthesizer_output(answer_payload)
145
+
146
+ final_answer = final_state.get("final_answer")
147
+ if isinstance(final_answer, dict) and final_answer:
148
+ presenter.print_answer_synthesizer_output(final_answer)
149
+ elif type(final_answer) == str and final_answer:
150
+ presenter.print_final_answer(final_answer)
151
+ elif type(final_answer) == list and final_answer:
152
+ presenter.print_execution_result(final_answer)
153
+
154
+ execution = final_state.get("execution")
155
+ if execution:
156
+ row_count = execution.get("row_count", 0) if isinstance(execution, dict) else getattr(execution, "row_count", 0)
157
+ presenter.print_rows_returned(row_count)
158
+
159
+
160
+ if config.show_perf:
161
+ tree, metrics, node_map = monitor.get_performance_tree()
162
+ presenter.print_performance_tree(tree, metrics, node_map)
163
+
164
+
165
+ from nl2sql.common.metrics import TOKEN_LOG
166
+ presenter.print_cost_summary(result.duration, TOKEN_LOG)