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,878 @@
1
+ import csv
2
+ import json
3
+ import statistics
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Iterable, List, Optional, Sequence, Union
6
+
7
+ from rich.columns import Columns
8
+ from rich.console import Console, Group
9
+ from rich.markdown import Markdown
10
+ from rich.markup import escape
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+ from rich.tree import Tree
14
+ from rich.live import Live
15
+ from rich.spinner import Spinner
16
+ from rich.style import Style
17
+ from rich.text import Text
18
+
19
+
20
+ class ConsolePresenter:
21
+ """
22
+ Console presentation utilities for the CLI.
23
+ """
24
+
25
+ def __init__(self, console: Optional[Console] = None):
26
+ self.console = console or Console()
27
+ self._status = None
28
+
29
+ # ------------------------------------------------------------------
30
+ # Status helpers
31
+ # ------------------------------------------------------------------
32
+ def status_context(self, message: str, spinner: str = "dots"):
33
+ return self.console.status(message, spinner=spinner)
34
+
35
+ def start_interactive_status(self, message: str, spinner: str = "dots") -> None:
36
+ if self._status:
37
+ self._status.stop()
38
+ self._status = self.console.status(message, spinner=spinner)
39
+ self._status.start()
40
+
41
+ def update_interactive_status(self, message: str) -> None:
42
+ if self._status:
43
+ self._status.update(message)
44
+ else:
45
+ self.start_interactive_status(message)
46
+
47
+ def stop_interactive_status(self) -> None:
48
+ if self._status:
49
+ self._status.stop()
50
+ self._status = None
51
+
52
+ def start_task_line(self, message: str, spinner: str = "dots") -> Live:
53
+ live = Live(Spinner(spinner, text=message), console=self.console, refresh_per_second=8, transient=False)
54
+ live.start()
55
+ return live
56
+
57
+ def finish_task_line(self, live: Live, message: str, success: bool = True) -> None:
58
+ label = "✓" if success else "✗"
59
+ style = "green" if success else "red"
60
+ live.update(Text(f"{label} {message}", style=style))
61
+ live.stop()
62
+
63
+ # ------------------------------------------------------------------
64
+ # Generic helpers
65
+ # ------------------------------------------------------------------
66
+ def _print_labeled(self, label: str, style: str, message: str) -> None:
67
+ self.console.print(f"[{style}]{escape(label)}[/{style}] {escape(str(message))}")
68
+
69
+ def print_success(self, message: str) -> None:
70
+ self._print_labeled("✓", "green", message)
71
+
72
+ def print_error(self, message: str) -> None:
73
+ self._print_labeled("[ERROR]", "red", message)
74
+
75
+ def print_warning(self, message: str) -> None:
76
+ self._print_labeled("[WARN]", "yellow", message)
77
+
78
+ def print_info(self, message: str) -> None:
79
+ self._print_labeled("[INFO]", "blue", message)
80
+
81
+ def print_header(self, message: str) -> None:
82
+ self.console.print(f"\n[bold magenta]--- {escape(str(message))} ---[/bold magenta]")
83
+
84
+ def print_panel(self, content: Any, title: str, style: str = "green") -> None:
85
+ if isinstance(content, (dict, list)):
86
+ content = json.dumps(content, indent=2, default=str)
87
+ if isinstance(content, str):
88
+ content = Text(content)
89
+ self.console.print(Panel(content, title=escape(str(title)), border_style=style))
90
+
91
+ def print_table(
92
+ self,
93
+ data: Union[Sequence[Dict[str, Any]], Sequence[Sequence[Any]]],
94
+ title: str = "",
95
+ columns: Optional[Sequence[str]] = None,
96
+ ) -> None:
97
+ if not data:
98
+ self.console.print("[dim]No data to display.[/dim]")
99
+ return
100
+
101
+ table = Table(title=title, show_header=True, header_style="bold cyan", expand=True)
102
+
103
+ first = data[0]
104
+ if isinstance(first, dict):
105
+ keys = list(columns) if columns else list(first.keys())
106
+ for key in keys:
107
+ table.add_column(str(key))
108
+ for row in data:
109
+ table.add_row(*[Text(str(row.get(k, ""))) for k in keys])
110
+ else:
111
+ if columns:
112
+ for col in columns:
113
+ table.add_column(str(col))
114
+ else:
115
+ for idx in range(len(first)):
116
+ table.add_column(f"Col {idx + 1}")
117
+ for row in data:
118
+ table.add_row(*[Text(str(val)) for val in row])
119
+
120
+ self.console.print(table)
121
+
122
+ def print_tree(self, tree: Tree, title: Optional[str] = None, style: str = "cyan") -> None:
123
+ if title:
124
+ self.console.print(Panel(tree, title=escape(str(title)), border_style=style))
125
+ else:
126
+ self.console.print(tree)
127
+
128
+ # ------------------------------------------------------------------
129
+ # Pipeline execution (run.py)
130
+ # ------------------------------------------------------------------
131
+ def print_pipeline_errors(self, errors: List[Any]) -> None:
132
+ if not errors:
133
+ return
134
+
135
+ table = Table(title="Pipeline Errors", show_header=True, header_style="bold red", expand=True)
136
+ table.add_column("Node", style="cyan")
137
+ table.add_column("Severity", justify="center")
138
+ table.add_column("Code", justify="center")
139
+ table.add_column("Message", style="white")
140
+
141
+ for e in errors:
142
+ if isinstance(e, dict):
143
+ severity = e.get("severity", "ERROR")
144
+ node = e.get("node", "unknown")
145
+ code = e.get("error_code", "-")
146
+ msg = e.get("message", "-")
147
+ else:
148
+ severity = e.severity.name if hasattr(e.severity, "name") else str(e.severity)
149
+ node = e.node
150
+ code = e.error_code
151
+ msg = e.message
152
+
153
+ sev_style = "red"
154
+ if "WARNING" in severity:
155
+ sev_style = "yellow"
156
+ if "CRITICAL" in severity:
157
+ sev_style = "bold red"
158
+
159
+ table.add_row(
160
+ Text(str(node).upper()),
161
+ f"[{sev_style}]{escape(str(severity))}[/{sev_style}]",
162
+ Text(str(code)),
163
+ Text(str(msg)),
164
+ )
165
+
166
+ self.console.print("\n")
167
+ self.console.print(table)
168
+ self.console.print("\n")
169
+
170
+ def print_query(self, query: str) -> None:
171
+ self.console.print(f"[bold blue]Query:[/bold blue] {escape(str(query))}")
172
+
173
+ def print_node_output(self, node_name: str, output: Any) -> None:
174
+ title = f"{node_name.capitalize()} Output"
175
+ self.print_panel(output, title=title, style="green")
176
+
177
+ def print_sql(self, sql: Any, title: str = "SQL Generated") -> None:
178
+ """Render SQL in a panel.
179
+
180
+ A plain string is shown verbatim: SQL is not markup, and T-SQL
181
+ bracket-quoted identifiers such as ``[dbo].[orders]`` would otherwise be
182
+ parsed as style tags and vanish from the output. Callers that want
183
+ styling pass a pre-built renderable (e.g. ``Text``).
184
+ """
185
+ body = Text(sql) if isinstance(sql, str) else sql
186
+ self.console.print(Panel(body, title=escape(str(title)), border_style="cyan", expand=False))
187
+
188
+ def print_final_answer(self, answer: str) -> None:
189
+ self.console.print(Panel(Markdown(answer), title="[bold green]Final Answer[/bold green]", expand=False))
190
+
191
+ def print_answer_synthesizer_output(self, answer: Dict[str, Any]) -> None:
192
+ summary = answer.get("summary")
193
+ format_type = answer.get("format_type")
194
+ content = answer.get("content")
195
+ warnings = answer.get("warnings") or []
196
+
197
+ if summary:
198
+ self.print_panel(Markdown(summary), title="Answer Summary", style="green")
199
+
200
+ if content:
201
+ title = "Answer Content"
202
+ if format_type:
203
+ title = f"Answer Content ({format_type})"
204
+ self.print_panel(Markdown(content), title=title, style="cyan")
205
+
206
+ if warnings:
207
+ for warning in warnings:
208
+ self.print_warning(str(warning))
209
+
210
+ def print_rows_returned(self, count: int) -> None:
211
+ self.console.print(f"[dim]Rows returned: {count}[/dim]")
212
+
213
+ def print_execution_result(self, execution: Any) -> None:
214
+ if not execution:
215
+ return
216
+
217
+ if isinstance(execution, list):
218
+ rows = execution
219
+ columns = list(rows[0].keys()) if rows and isinstance(rows[0], dict) else []
220
+ else:
221
+ rows = execution.get("rows", []) if isinstance(execution, dict) else getattr(execution, "rows", [])
222
+ columns = execution.get("columns", []) if isinstance(execution, dict) else getattr(execution, "columns", [])
223
+
224
+ if not rows:
225
+ self.console.print("[dim]No rows returned.[/dim]")
226
+ return
227
+
228
+ table = Table(title="Result Data", show_header=True, header_style="bold cyan", border_style="blue")
229
+ for col in columns:
230
+ table.add_column(Text(str(col)))
231
+
232
+ for row in rows:
233
+ row_vals = []
234
+ for col in columns:
235
+ val = row.get(col, "") if isinstance(row, dict) else getattr(row, col, "")
236
+ row_vals.append(str(val))
237
+ table.add_row(*[Text(v) for v in row_vals])
238
+
239
+ self.console.print("\n")
240
+ self.console.print(table)
241
+ self.console.print("\n")
242
+
243
+ def print_datasource_used(self, ds_id: str) -> None:
244
+ self.console.print(f"[bold blue]Datasource Used:[/bold blue] {escape(str(ds_id))}")
245
+
246
+ def print_performance_report(self, latency: Dict[str, Any], token_log: List[Dict[str, Any]]) -> None:
247
+ renderables = []
248
+
249
+ top_table = Table(title="Top Level Performance", show_header=True, header_style="bold magenta", expand=True)
250
+ top_table.add_column("Metric", style="dim")
251
+ top_table.add_column("Decomposer", justify="right")
252
+ top_table.add_column("Aggregator", justify="right")
253
+
254
+ datasources = set()
255
+ for key in latency.keys():
256
+ if ":" in key:
257
+ datasources.add(key.split(":")[0])
258
+ sorted_ds = sorted(list(datasources))
259
+
260
+ for ds in sorted_ds:
261
+ top_table.add_column(f"Exec ({ds})", justify="right")
262
+ top_table.add_column("Total", justify="right", style="bold")
263
+
264
+ lat_decomp = latency.get("decomposer", 0.0)
265
+ lat_agg = latency.get("aggregator", 0.0)
266
+
267
+ lat_row = ["Latency (s)", f"{lat_decomp:.4f}", f"{lat_agg:.4f}"]
268
+
269
+ max_branch_latency = 0.0
270
+ for ds in sorted_ds:
271
+ val = latency.get(f"{ds}:total", 0.0)
272
+ lat_row.append(f"{val:.4f}")
273
+ if val > max_branch_latency:
274
+ max_branch_latency = val
275
+
276
+ total_latency = lat_decomp + lat_agg + max_branch_latency
277
+ lat_row.append(f"{total_latency:.4f}")
278
+ top_table.add_row(*lat_row)
279
+
280
+ def sum_tokens(agent_prefix=None, ds_id=None):
281
+ total = 0
282
+ for entry in token_log:
283
+ if agent_prefix and entry["agent"].startswith(agent_prefix):
284
+ total += entry["total_tokens"]
285
+ elif ds_id and entry.get("datasource_id") == ds_id:
286
+ if not (
287
+ entry["agent"].startswith("decomposer")
288
+ or entry["agent"].startswith("aggregator")
289
+ ):
290
+ total += entry["total_tokens"]
291
+ return total
292
+
293
+ tok_decomp = sum_tokens(agent_prefix="decomposer")
294
+ tok_agg = sum_tokens(agent_prefix="aggregator")
295
+
296
+ tok_row = ["Token Usage", str(tok_decomp), str(tok_agg)]
297
+ total_tokens = tok_decomp + tok_agg
298
+ for ds in sorted_ds:
299
+ val = sum_tokens(ds_id=ds)
300
+ tok_row.append(str(val))
301
+ total_tokens += val
302
+ tok_row.append(str(total_tokens))
303
+ top_table.add_row(*tok_row)
304
+
305
+ renderables.append(top_table)
306
+ renderables.append("\n")
307
+
308
+ ai_nodes = {"planner", "intent", "router", "summarizer", "generator", "decomposer", "aggregator"}
309
+
310
+ ds_metrics = {}
311
+ for key, val in latency.items():
312
+ if ":" in key:
313
+ parts = key.split(":", 1)
314
+ ds_id = parts[0]
315
+ node = parts[1]
316
+ if node == "total":
317
+ continue
318
+ if ds_id not in ds_metrics:
319
+ ds_metrics[ds_id] = {}
320
+ ds_metrics[ds_id][node] = val
321
+
322
+ ds_tables = []
323
+ for ds_id in sorted_ds:
324
+ ds_table = Table(title=f"Performance: {ds_id}", show_header=True, header_style="bold cyan", expand=True)
325
+ ds_table.add_column("Node", style="dim")
326
+ ds_table.add_column("Type", justify="center")
327
+ ds_table.add_column("Model", justify="center")
328
+ ds_table.add_column("Latency (s)", justify="right")
329
+ ds_table.add_column("Tokens", justify="right")
330
+
331
+ metrics = ds_metrics.get(ds_id, {})
332
+ node_order = ["intent", "planner", "generator", "executor"]
333
+ other_nodes = sorted([n for n in metrics.keys() if n not in node_order])
334
+ sorted_nodes = [n for n in node_order if n in metrics] + other_nodes
335
+
336
+ for node in sorted_nodes:
337
+ duration = metrics[node]
338
+ is_ai = node in ai_nodes
339
+ node_type = "AI" if is_ai else "Non-AI"
340
+
341
+ model_name = "-"
342
+ tokens = 0
343
+ if is_ai:
344
+ for entry in token_log:
345
+ if entry.get("datasource_id") == ds_id and entry["agent"] == node:
346
+ model_name = entry["model"]
347
+ tokens += entry["total_tokens"]
348
+
349
+ ds_table.add_row(
350
+ node.capitalize(),
351
+ node_type,
352
+ model_name,
353
+ f"{duration:.4f}",
354
+ str(tokens) if is_ai else "-",
355
+ )
356
+ ds_tables.append(ds_table)
357
+
358
+ if ds_tables:
359
+ renderables.append(Columns(ds_tables))
360
+
361
+ if renderables:
362
+ self.print_panel(Group(*renderables), title="Performance & Metrics", style="magenta")
363
+
364
+ def print_execution_tree(
365
+ self,
366
+ user_query: str,
367
+ query_history: List[Dict[str, Any]],
368
+ top_level_reasoning: Optional[List[Dict[str, Any]]] = None,
369
+ ) -> None:
370
+ if top_level_reasoning is None:
371
+ top_level_reasoning = []
372
+ tree = Tree(f"[bold blue]Root Query: {escape(str(user_query))}[/bold blue]")
373
+
374
+ node_styles = {
375
+ "decomposer": "bold magenta",
376
+ "router": "bold cyan",
377
+ "intent": "bold magenta",
378
+ "schema": "bold yellow",
379
+ "planner": "bold blue",
380
+ "validator": "bold red",
381
+ "summarizer": "bold orange1",
382
+ "generator": "bold green",
383
+ "executor": "bold white",
384
+ "aggregator": "bold magenta",
385
+ }
386
+
387
+ def add_reasoning_steps(parent_tree: Tree, reasoning_list: List[Dict[str, Any]]) -> None:
388
+ if not isinstance(reasoning_list, list):
389
+ return
390
+
391
+ for step in reasoning_list:
392
+ node = step.get("node", "unknown")
393
+ content = step.get("content")
394
+ msg_type = step.get("type", "info")
395
+
396
+ style = node_styles.get(node, "bold")
397
+
398
+ node_label = f"[{style}]{node.capitalize()}[/{style}]"
399
+ if msg_type == "error":
400
+ node_label += " [bold red](Error)[/bold red]"
401
+
402
+ step_branch = parent_tree.add(node_label)
403
+
404
+ if isinstance(content, list):
405
+ for line in content:
406
+ step_branch.add(Text(str(line)))
407
+ else:
408
+ step_branch.add(Text(str(content)))
409
+
410
+ if top_level_reasoning:
411
+ decomp_steps = [r for r in top_level_reasoning if r.get("node") == "decomposer"]
412
+ if decomp_steps:
413
+ add_reasoning_steps(tree, decomp_steps)
414
+
415
+ for i, item in enumerate(query_history):
416
+ sub_query = item.get("sub_query") or "Main Branch"
417
+ ds_id = item.get("datasource_id") or "Unknown"
418
+ reasoning = item.get("reasoning", [])
419
+
420
+ branch = tree.add(
421
+ f"[bold green]Branch {i+1}:[/bold green] {escape(str(sub_query))} "
422
+ f"[dim]({escape(str(ds_id))})[/dim]"
423
+ )
424
+
425
+ add_reasoning_steps(branch, reasoning)
426
+
427
+ sql = item.get("sql_draft")
428
+ if not sql:
429
+ sql = item.get("sql")
430
+
431
+ if sql:
432
+ branch.add(f"[bold]SQL:[/bold] {escape(str(sql))}")
433
+
434
+ if top_level_reasoning:
435
+ agg_steps = [r for r in top_level_reasoning if r.get("node") == "aggregator"]
436
+ if agg_steps:
437
+ add_reasoning_steps(tree, agg_steps)
438
+
439
+ self.console.print("\n")
440
+ self.console.print(tree)
441
+
442
+ def print_status_tree(
443
+ self,
444
+ tree_data: Dict[str, List[str]],
445
+ durations: Dict[str, float],
446
+ node_order: Optional[List[str]] = None,
447
+ ) -> None:
448
+ if not tree_data:
449
+ return
450
+
451
+ root_tree = Tree("Graph")
452
+
453
+ def build_branch(parent_name: str, tree_node: Tree):
454
+ children = tree_data.get(parent_name, [])
455
+ for child in children:
456
+ duration = durations.get(child, 0.0)
457
+ label = f"{child} [dim]({duration:.2f}s)[/dim]"
458
+ branch = tree_node.add(label)
459
+ build_branch(child, branch)
460
+
461
+ all_children = set()
462
+ for kids in tree_data.values():
463
+ all_children.update(kids)
464
+
465
+ all_nodes = set(durations.keys())
466
+ roots = list(all_nodes - all_children)
467
+
468
+ if node_order:
469
+ roots.sort(key=lambda x: node_order.index(x) if x in node_order else 9999)
470
+ else:
471
+ roots.sort()
472
+
473
+ for r in roots:
474
+ duration = durations.get(r, 0.0)
475
+ label = f"{r} [dim]({duration:.2f}s)[/dim]"
476
+ branch = root_tree.add(label)
477
+ build_branch(r, branch)
478
+
479
+ self.console.print("\n")
480
+ self.console.print(root_tree)
481
+
482
+ def print_performance_tree(
483
+ self,
484
+ tree_data: Dict[str, List[str]],
485
+ metrics_data: Dict[str, Any],
486
+ node_map: Optional[Dict[str, str]] = None,
487
+ ) -> None:
488
+ if not tree_data:
489
+ return
490
+ if node_map is None:
491
+ node_map = {}
492
+
493
+ root_tree = Tree("[bold magenta]Performance Execution Tree[/bold magenta]")
494
+
495
+ def get_dur_style(dur):
496
+ if dur > 5.0:
497
+ return "bold red"
498
+ if dur > 2.0:
499
+ return "yellow"
500
+ return "green"
501
+
502
+ def build_branch(parent_id: str, tree_node: Tree, path: set):
503
+ for child_id in tree_data.get(parent_id, []):
504
+ if child_id in path:
505
+ name = node_map.get(child_id, child_id)
506
+ tree_node.add(f"[dim]{name} (recursive)[/dim]")
507
+ continue
508
+
509
+ meta = metrics_data.get(child_id)
510
+ name = node_map.get(child_id, child_id)
511
+
512
+ label = f"[bold]{name}[/bold]"
513
+
514
+ if meta:
515
+ dur = meta.duration
516
+ dur_style = get_dur_style(dur)
517
+ label += f" [dim]in[/dim] [{dur_style}]{dur:.2f}s[/{dur_style}]"
518
+
519
+ if meta.total_tokens > 0:
520
+ label += f" | [cyan]{meta.total_tokens} tok[/cyan]"
521
+
522
+ if meta.error:
523
+ label += f" [bold red]FAILED: {escape(str(meta.error))}[/bold red]"
524
+
525
+ branch = tree_node.add(label)
526
+
527
+ new_path = set(path)
528
+ new_path.add(child_id)
529
+ build_branch(child_id, branch, new_path)
530
+
531
+ all_children = set()
532
+ for parent, kids in tree_data.items():
533
+ for k in kids:
534
+ if k != parent:
535
+ all_children.add(k)
536
+
537
+ all_nodes = set(tree_data.keys())
538
+ roots = [n for n in all_nodes if n not in all_children]
539
+
540
+ roots.sort(
541
+ key=lambda r: metrics_data[r].duration if r in metrics_data else 0,
542
+ reverse=True,
543
+ )
544
+
545
+ for r_id in roots:
546
+ meta = metrics_data.get(r_id)
547
+ name = node_map.get(r_id, r_id)
548
+
549
+ label = f"[bold]{name}[/bold]"
550
+ if meta:
551
+ dur = meta.duration
552
+ dur_style = get_dur_style(dur)
553
+ label += f" [dim]in[/dim] [{dur_style}]{dur:.2f}s[/{dur_style}]"
554
+ if meta.total_tokens > 0:
555
+ label += f" | [cyan]{meta.total_tokens} tok[/cyan]"
556
+
557
+ branch = root_tree.add(label)
558
+ build_branch(r_id, branch, {r_id})
559
+
560
+ self.console.print("\n")
561
+ self.console.print(Panel(root_tree, title="Trace Metrics", border_style="magenta"))
562
+ self.console.print("\n")
563
+
564
+ def print_cost_summary(self, total_duration: float, token_log: List[Dict[str, Any]]) -> None:
565
+ total_tokens = sum(entry["total_tokens"] for entry in token_log)
566
+ self.console.print(f"[dim]Total Duration: {total_duration:.2f}s | Total Tokens: {total_tokens}[/dim]")
567
+
568
+ # ------------------------------------------------------------------
569
+ # Benchmarking (benchmark.py)
570
+ # ------------------------------------------------------------------
571
+ def print_config_benchmark_results(self, results: List[Dict[str, Any]]) -> None:
572
+ table = Table(title="Benchmark Results", show_header=True, header_style="bold magenta")
573
+ table.add_column("Config", style="cyan")
574
+ table.add_column("Success Rate", justify="right")
575
+ table.add_column("Avg Latency", justify="right")
576
+ table.add_column("Avg Tokens", justify="right")
577
+
578
+ for res in results:
579
+ sr = res["success_rate"]
580
+ sr_style = "green" if sr == 100 else "yellow" if sr >= 50 else "red"
581
+
582
+ table.add_row(
583
+ res["config"],
584
+ f"[{sr_style}]{sr:.1f}%[/{sr_style}]",
585
+ f"{res['avg_latency']:.2f}s",
586
+ f"{res['avg_tokens']:.1f}",
587
+ )
588
+
589
+ self.console.print("\n")
590
+ self.console.print(table)
591
+
592
+ def print_dataset_benchmark_results(
593
+ self,
594
+ results: List[Dict[str, Any]],
595
+ iterations: int = 1,
596
+ routing_only: bool = False,
597
+ ) -> None:
598
+ if iterations > 1:
599
+ self._print_pass_k_table(results, iterations)
600
+ else:
601
+ self._print_standard_table(results, routing_only)
602
+
603
+ def _print_pass_k_table(self, results: List[Dict[str, Any]], iterations: int) -> None:
604
+ grouped = {}
605
+ for r in results:
606
+ qid = r["id"]
607
+ if qid not in grouped:
608
+ grouped[qid] = []
609
+ grouped[qid].append(r)
610
+
611
+ table = Table(
612
+ title=f"Evaluation Results (Pass@{iterations})",
613
+ show_header=True,
614
+ header_style="bold magenta",
615
+ expand=True,
616
+ )
617
+ table.add_column("ID", style="cyan", no_wrap=True)
618
+ table.add_column("Success Rate", justify="right")
619
+ table.add_column("Route Stab.", justify="right")
620
+ table.add_column("Exec Acc.", justify="right")
621
+ table.add_column("Sem Acc.", justify="right")
622
+ table.add_column("Avg Latency", justify="right")
623
+ table.add_column("Errors", justify="left", overflow="ellipsis")
624
+
625
+ for qid, runs in grouped.items():
626
+ n = len(runs)
627
+ success_count = sum(1 for r in runs if r["status"] == "PASS")
628
+ success_rate = (success_count / n) * 100
629
+
630
+ ds_counts = {}
631
+ for r in runs:
632
+ ds = r.get("actual_ds", "None")
633
+ ds_counts[ds] = ds_counts.get(ds, 0) + 1
634
+ most_common_ds = max(ds_counts, key=ds_counts.get)
635
+ stability_rate = (ds_counts[most_common_ds] / n) * 100
636
+
637
+ sql_match_count = sum(1 for r in runs if r.get("sql_match") is True)
638
+ exec_acc = (sql_match_count / n) * 100
639
+
640
+ sem_match_count = sum(1 for r in runs if r.get("semantic_sql_match") is True)
641
+ sem_acc = (sem_match_count / n) * 100
642
+
643
+ avg_latency = statistics.mean([r.get("routing_latency", 0) for r in runs])
644
+
645
+ errors_set = {r["error"] for r in runs if r.get("error")}
646
+ error_str = str(list(errors_set)[0]) if errors_set else "-"
647
+ if len(error_str) > 30:
648
+ error_str = error_str[:27] + "..."
649
+
650
+ sr_style = "green" if success_rate == 100 else "yellow" if success_rate >= 50 else "red"
651
+
652
+ table.add_row(
653
+ Text(str(qid)),
654
+ f"[{sr_style}]{success_rate:.0f}%[/{sr_style}]",
655
+ f"{stability_rate:.0f}%",
656
+ f"{exec_acc:.0f}%",
657
+ f"{sem_acc:.0f}%",
658
+ f"{avg_latency:.2f}s",
659
+ Text(error_str),
660
+ )
661
+
662
+ self.console.print(table)
663
+
664
+ def _print_standard_table(self, results: List[Dict[str, Any]], routing_only: bool) -> None:
665
+ table = Table(title="Evaluation Results", show_header=True, header_style="bold magenta", expand=True)
666
+ table.add_column("ID", style="cyan", no_wrap=True)
667
+ table.add_column("Status", justify="center")
668
+ table.add_column("Route", justify="center")
669
+ table.add_column("Layer", justify="center")
670
+
671
+ if not routing_only:
672
+ table.add_column("SQL Match", justify="center")
673
+ table.add_column("Sem Match", justify="center")
674
+ table.add_column("Rows", justify="right")
675
+ else:
676
+ table.add_column("Got/Exp DS", justify="left")
677
+
678
+ table.add_column("Reasoning", justify="left", max_width=40, overflow="ellipsis")
679
+ table.add_column("L1 Score", justify="right")
680
+ table.add_column("Tokens", justify="right")
681
+ table.add_column("Latency", justify="right")
682
+ table.add_column("Candidates", justify="left")
683
+
684
+ for r in results:
685
+ status_style = "green" if r["status"] == "PASS" else "red"
686
+ route_icon = "YES" if r["routing_match"] else "NO"
687
+
688
+ layer_raw = r.get("routing_layer", "unknown")
689
+ layer_map = {"layer_1": "L1", "layer_2": "L2", "layer_3": "L3", "fallback": "FB"}
690
+ layer_str = layer_map.get(layer_raw, layer_raw)
691
+
692
+ cols = [r["id"], f"[{status_style}]{r['status']}[/{status_style}]", route_icon, layer_str]
693
+
694
+ if not routing_only:
695
+ sql_icon = "YES" if r.get("sql_match") else "NO" if r.get("sql_match") is not None else "-"
696
+ sem_icon = (
697
+ "YES"
698
+ if r.get("semantic_sql_match")
699
+ else "NO"
700
+ if r.get("semantic_sql_match") is not None
701
+ else "-"
702
+ )
703
+
704
+ rows_info = f"{r.get('gen_rows', '-')} / {r.get('exp_rows', '-')}"
705
+ cols.extend([sql_icon, sem_icon, rows_info])
706
+ else:
707
+ ds_info = f"{r.get('actual_ds')} / {r.get('expected_ds')}"
708
+ cols.append(ds_info)
709
+
710
+ reasoning = r.get("routing_reasoning", "-")
711
+ tokens = str(r.get("routing_tokens", "-"))
712
+ latency_val = r.get("routing_latency", 0)
713
+ latency_str = f"{latency_val:.2f}s" if isinstance(latency_val, (int, float)) else "-"
714
+ score_val = r.get("l1_score", 0.0)
715
+ score_str = f"{score_val:.3f}" if isinstance(score_val, (int, float)) else "-"
716
+
717
+ candidates = r.get("candidates", [])
718
+ cand_str = ""
719
+ if candidates:
720
+ cand_str = ", ".join([f"{c['id']}({c['score']:.2f})" for c in candidates[:3]])
721
+ if len(candidates) > 3:
722
+ cand_str += "..."
723
+
724
+ cols.extend([Text(str(reasoning)), score_str, tokens, latency_str, cand_str])
725
+
726
+ table.add_row(*cols)
727
+
728
+ self.console.print(table)
729
+
730
+ def print_metrics_summary(
731
+ self,
732
+ metrics: Dict[str, Any],
733
+ results: List[Dict[str, Any]],
734
+ routing_only: bool = False,
735
+ ) -> None:
736
+ routing_acc = metrics.get("routing_accuracy", 0.0)
737
+ self.console.print(f"\n[bold]Routing Accuracy:[/bold] {routing_acc:.1f}%")
738
+
739
+ self.console.print("\n[bold]Routing Layer Breakdown:[/bold]")
740
+ layer_counts = metrics.get("layer_distribution", {})
741
+ layer_pcts = metrics.get("layer_percentages", {})
742
+
743
+ for layer, count in layer_counts.items():
744
+ pct = layer_pcts.get(layer, 0.0)
745
+ self.console.print(f" - {layer.replace('_', ' ').title()}: {count} ({pct:.1f}%)")
746
+
747
+ if not routing_only:
748
+ sql_acc = metrics.get("execution_accuracy", 0.0)
749
+ sem_acc = metrics.get("semantic_sql_accuracy", 0.0)
750
+ valid_sql_rate = metrics.get("valid_sql_rate", 0.0)
751
+ self.console.print(f"\n[bold]Execution Accuracy:[/bold] {sql_acc:.1f}%")
752
+ self.console.print(f"[bold]Semantic SQL Accuracy:[/bold] {sem_acc:.1f}%")
753
+ self.console.print(f"[bold]Valid SQL Rate:[/bold] {valid_sql_rate:.1f}%")
754
+
755
+ errors = [r for r in results if r["status"] in ["ERROR", "GT_FAIL", "EXEC_FAIL", "INVALID_GT", "INVALID_SQL"]]
756
+ if errors:
757
+ unique_errors = {}
758
+ for e in errors:
759
+ unique_errors[f"{e['id']}: {e.get('error')}"] = e
760
+
761
+ self.console.print("\n[bold red]Top Errors:[/bold red]")
762
+ for k in list(unique_errors.keys())[:5]:
763
+ self.console.print(Text(k))
764
+
765
+ def export_results(self, results: List[Dict[str, Any]], path: Path) -> None:
766
+ export_data = []
767
+ for r in results:
768
+ item = {
769
+ "id": r.get("id"),
770
+ "question": r.get("question", ""),
771
+ "status": r.get("status"),
772
+ "generated_sql": r.get("gen_sql"),
773
+ "expected_sql": r.get("exp_sql"),
774
+ "sql_match": r.get("sql_match"),
775
+ "semantic_match": r.get("semantic_sql_match"),
776
+ "routing_match": r.get("routing_match"),
777
+ "datasource": r.get("actual_ds"),
778
+ "expected_datasource": r.get("expected_ds"),
779
+ "error": r.get("error"),
780
+ }
781
+ export_data.append(item)
782
+
783
+ if path.suffix.lower() == ".json":
784
+ with open(path, "w", encoding="utf-8") as f:
785
+ json.dump(export_data, f, indent=2, default=str)
786
+ self.console.print(f"\n[bold green]Results exported to {escape(str(path))}[/bold green]")
787
+ elif path.suffix.lower() == ".csv":
788
+ if not export_data:
789
+ self.console.print(f"\n[yellow]No results to export.[/yellow]")
790
+ else:
791
+ keys = export_data[0].keys()
792
+ with open(path, "w", newline="", encoding="utf-8") as f:
793
+ writer = csv.DictWriter(f, fieldnames=keys)
794
+ writer.writeheader()
795
+ writer.writerows(export_data)
796
+ self.console.print(f"\n[bold green]Results exported to {escape(str(path))}[/bold green]")
797
+ else:
798
+ self.console.print(
799
+ f"\n[bold red]Unsupported export format: {escape(str(path.suffix))}. Use .json or .csv[/bold red]"
800
+ )
801
+
802
+ # ------------------------------------------------------------------
803
+ # Indexing (indexing.py)
804
+ # ------------------------------------------------------------------
805
+ def print_indexing_start(self, path: str) -> None:
806
+ self.console.print(f"[bold blue]Indexing schema to:[/bold blue] {escape(str(path))}")
807
+
808
+ def print_indexing_error(self, ds_id: str, error: str) -> None:
809
+ self.console.print(f"[red]Failed to index {escape(str(ds_id))}: {escape(str(error))}[/red]")
810
+
811
+ def print_indexing_complete(self) -> None:
812
+ self.console.print("\n[bold green]Indexing complete![/bold green]")
813
+
814
+ def print_indexing_summary(self, stats: List[Dict[str, Any]]) -> None:
815
+ if not stats:
816
+ return
817
+
818
+ table = Table(title="Indexing Summary", show_header=True, header_style="bold magenta", expand=True)
819
+ table.add_column("Datasource", style="cyan")
820
+ table.add_column("Tables", justify="right")
821
+ table.add_column("Columns", justify="right")
822
+ table.add_column("Examples", justify="right")
823
+
824
+ total_tables = 0
825
+ total_cols = 0
826
+ total_examples = 0
827
+
828
+ for s in stats:
829
+ t = s.get("tables", 0)
830
+ c = s.get("columns", 0)
831
+ e = s.get("examples", 0)
832
+
833
+ total_tables += t
834
+ total_cols += c
835
+ total_examples += e
836
+
837
+ table.add_row(Text(str(s.get("id", "Unknown"))), str(t), str(c), str(e))
838
+
839
+ table.add_row(
840
+ "[bold]TOTAL[/bold]",
841
+ f"[bold]{total_tables}[/bold]",
842
+ f"[bold]{total_cols}[/bold]",
843
+ f"[bold]{total_examples}[/bold]",
844
+ style="green",
845
+ )
846
+
847
+ self.console.print("\n")
848
+ self.console.print(table)
849
+
850
+ def create_progress(self):
851
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn
852
+
853
+ return Progress(
854
+ SpinnerColumn(),
855
+ TextColumn("[progress.description]{task.description}"),
856
+ BarColumn(),
857
+ TaskProgressColumn(),
858
+ console=self.console,
859
+ )
860
+
861
+ # ------------------------------------------------------------------
862
+ # Visualization (visualize.py)
863
+ # ------------------------------------------------------------------
864
+ def print_graph_saved(self, path: str) -> None:
865
+ import os
866
+
867
+ abs_path = os.path.abspath(path)
868
+ link = Text(abs_path, style=Style(bold=True, underline=True, link=f"file:///{abs_path}"))
869
+ self.console.print("Graph visualization saved to: ", link)
870
+
871
+ def print_graph_save_error(self, error: str) -> None:
872
+ self.console.print(f"[bold red]Failed to save graph image:[/bold red] {escape(str(error))}")
873
+
874
+ def track(self, sequence: Iterable[Any], description: str = "Working...", total: Optional[float] = None):
875
+ from rich.progress import track
876
+
877
+ return track(sequence, description=description, total=total, console=self.console)
878
+