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,415 @@
1
+
2
+ import os
3
+ import pathlib
4
+ from rich.markup import escape
5
+ from rich.panel import Panel
6
+ from InquirerPy import inquirer
7
+ from InquirerPy.validator import NumberValidator
8
+
9
+ from nl2sql.cli.common.decorators import handle_cli_errors
10
+ from nl2sql.cli.console import console, print_success, print_step
11
+ from nl2sql.cli.config import ADAPTER_DRIVERS, KNOWN_ADAPTERS
12
+ from nl2sql.cli.commands.install import install_package
13
+ from nl2sql.cli.checks import check_package, verify_connectivity
14
+
15
+ from nl2sql.common.logger import get_logger
16
+ from nl2sql.configs import ConfigManager
17
+ from nl2sql.configs import (
18
+ DatasourceConfig,
19
+ DatasourceFileConfig,
20
+ ConnectionConfig,
21
+ LLMFileConfig,
22
+ AgentConfig,
23
+ PolicyFileConfig,
24
+ RolePolicy
25
+ )
26
+ from nl2sql.cli.demo import DemoManager
27
+
28
+ logger = get_logger(__name__)
29
+
30
+ # The CLI writes where it is invoked. Do not resolve this from __file__:
31
+ # that walks up out of the installed module and, in a source checkout,
32
+ # lands on the repo root instead of the user's working directory.
33
+ PROJECT_ROOT = pathlib.Path.cwd()
34
+
35
+ CONFIG_DIR = PROJECT_ROOT / "configs"
36
+ DATASOURCE_CONFIG = CONFIG_DIR / "datasources.yaml"
37
+ LLM_CONFIG = CONFIG_DIR / "llm.yaml"
38
+ POLICIES_CONFIG = CONFIG_DIR / "policies.json"
39
+
40
+
41
+ from typing import Optional
42
+ from nl2sql.configs import DatasourceConfig, ConnectionConfig
43
+
44
+ def _configure_datasource(config_manager: ConfigManager):
45
+ """Interactively configures datasources."""
46
+
47
+ if DATASOURCE_CONFIG.exists():
48
+ console.print(Panel("[bold]1. Datasource Configuration[/bold]", border_style="cyan"))
49
+ console.print("[dim]Existing configuration found.[/dim]")
50
+ return
51
+
52
+ console.print(Panel("[bold]1. Datasource Configuration[/bold]", border_style="cyan"))
53
+ console.print("No datasource configuration found. Let's create one.")
54
+
55
+ db_type = inquirer.select(
56
+ message="Select Database Type:",
57
+ choices=["postgres", "mysql", "mssql", "sqlite"],
58
+ default="postgres"
59
+ ).execute()
60
+
61
+ ds_config = None
62
+
63
+ if db_type == "sqlite":
64
+ db_path = inquirer.text(message="Database Path:", default="./my_database.db").execute()
65
+ conn = ConnectionConfig(type="sqlite", database=db_path)
66
+ ds_config = DatasourceConfig(
67
+ id="my_sqlite_db",
68
+ description="Main application database",
69
+ connection=conn
70
+ )
71
+ else:
72
+ host = inquirer.text(message="Host:", default="localhost").execute()
73
+ default_ports = {"postgres": "5432", "mysql": "3306", "mssql": "1433"}
74
+ port = inquirer.text(
75
+ message="Port:",
76
+ default=default_ports.get(db_type, "5432"),
77
+ validate=NumberValidator()
78
+ ).execute()
79
+
80
+ user = inquirer.text(
81
+ message="Username:",
82
+ default="postgres" if db_type == "postgres" else "root"
83
+ ).execute()
84
+
85
+ dbname = inquirer.text(message="Database Name:").execute()
86
+
87
+ # Password & Secrets
88
+ password = inquirer.secret(message="Password:").execute()
89
+ final_password = password
90
+
91
+ if inquirer.confirm(message="Secure this password with an Environment Variable?", default=True).execute():
92
+ env_var = inquirer.text(message="Environment Variable Name:", default="DB_PASSWORD").execute()
93
+ final_password = f"${{env:{env_var}}}"
94
+ console.print(f"[dim]Will save as: {escape(str(final_password))}[/dim]")
95
+ os.environ[env_var] = password # Set it for current session so validation passes
96
+
97
+ conn_args = {
98
+ "type": db_type,
99
+ "host": host,
100
+ "port": int(port),
101
+ "user": user,
102
+ "password": final_password,
103
+ "database": dbname
104
+ }
105
+
106
+ if db_type == "mssql":
107
+ conn_args["driver"] = "ODBC Driver 17 for SQL Server"
108
+
109
+ conn = ConnectionConfig(**conn_args)
110
+
111
+ ds_config = DatasourceConfig(
112
+ id="main_db",
113
+ connection=conn,
114
+ options={}
115
+ )
116
+
117
+ # Write using Manager
118
+ # Write using Generator
119
+ ds_configs = [ds_config]
120
+ if ds_configs:
121
+ console.print(f"[green]Generated configuration for {len(ds_configs)} datasources.[/green]")
122
+ file_config = DatasourceFileConfig(datasources=ds_configs)
123
+ content = DatasourceGenerator.generate(file_config)
124
+ _write_config_file(DATASOURCE_CONFIG, content)
125
+
126
+
127
+ from nl2sql.configs import LLMFileConfig, AgentConfig
128
+ from nl2sql.cli.generators.llm import LLMGenerator
129
+
130
+
131
+ def _configure_llm(config_manager: ConfigManager, api_key: Optional[str] = None):
132
+ """Interactively configures LLM."""
133
+ if LLM_CONFIG.exists():
134
+ console.print(Panel("[bold]2. LLM Configuration[/bold]", border_style="magenta"))
135
+ console.print("[dim]Existing configuration found.[/dim]")
136
+ return
137
+
138
+ console.print(Panel("[bold]2. LLM Configuration[/bold]", border_style="magenta"))
139
+
140
+ if api_key:
141
+ console.print("[green]API Key provided via CLI. Creating default OpenAI configuration.[/green]")
142
+ default_agent = AgentConfig(
143
+ provider="openai",
144
+ model="gpt-4o",
145
+ api_key="${env:OPENAI_API_KEY}"
146
+ )
147
+ llm_config = LLMFileConfig(default=default_agent)
148
+ content = LLMGenerator.generate(llm_config)
149
+ _write_config_file(LLM_CONFIG, content)
150
+ return
151
+
152
+ console.print("No LLM configuration found. Let's configure one.")
153
+
154
+ # Only providers the engine can actually serve are offered here; anything
155
+ # else lets setup succeed and then fails on the first query.
156
+ provider = inquirer.select(
157
+ message="Select Provider:",
158
+ choices=["openai", "openrouter"],
159
+ default="openai"
160
+ ).execute()
161
+
162
+ default_agent = None
163
+
164
+ if provider == "openai":
165
+ api_key = inquirer.secret(message="OpenAI API Key:").execute()
166
+ default_agent = AgentConfig(
167
+ provider="openai",
168
+ model="gpt-4o",
169
+ api_key=api_key
170
+ )
171
+ elif provider == "openrouter":
172
+ console.print(
173
+ "[dim]OpenRouter is an OpenAI-compatible gateway: one key reaches "
174
+ "Anthropic, Google, Meta and hundreds of other models.[/dim]"
175
+ )
176
+ api_key = inquirer.secret(message="OpenRouter API Key:").execute()
177
+ model = inquirer.text(
178
+ message="OpenRouter model identifier (e.g. anthropic/claude-sonnet-4.5):",
179
+ default="anthropic/claude-sonnet-4.5"
180
+ ).execute()
181
+ env_var = "OPENROUTER_API_KEY"
182
+ os.environ[env_var] = api_key # available to the rest of this session
183
+ default_agent = AgentConfig(
184
+ provider="openrouter",
185
+ model=model,
186
+ api_key=f"${{env:{env_var}}}"
187
+ )
188
+ console.print(f"[dim]Will save the key as: ${{env:{env_var}}}[/dim]")
189
+ console.print(
190
+ "[yellow]Note:[/yellow] embeddings still go through OpenAI, so "
191
+ "[cyan]nl2sql index[/cyan] needs OPENAI_API_KEY as well."
192
+ )
193
+
194
+ llm_config = LLMFileConfig(default=default_agent)
195
+ content = LLMGenerator.generate(llm_config)
196
+ _write_config_file(LLM_CONFIG, content)
197
+
198
+ from nl2sql.configs import PolicyFileConfig, RolePolicy
199
+
200
+ def _configure_policies(config_manager: ConfigManager):
201
+ """Generates default policies."""
202
+ if POLICIES_CONFIG.exists():
203
+ console.print(Panel("[bold]3. Policy Configuration[/bold]", border_style="yellow"))
204
+ console.print("[dim]Existing policies configuration found.[/dim]")
205
+ return
206
+
207
+ console.print(Panel("[bold]3. Policy Configuration[/bold]", border_style="yellow"))
208
+ console.print("Generating default RBAC policies...")
209
+
210
+ admin_policy = RolePolicy(
211
+ description="System Administrator",
212
+ role="admin",
213
+ allowed_datasources=["*"],
214
+ allowed_tables=["*"]
215
+ )
216
+
217
+ policy_config = PolicyFileConfig(roles={"admin": admin_policy})
218
+
219
+ content = PolicyGenerator.generate(policy_config)
220
+ _write_config_file(POLICIES_CONFIG, content)
221
+
222
+
223
+
224
+ from nl2sql.cli.generators.env import EnvFileGenerator
225
+ from nl2sql.cli.generators.datasources import DatasourceGenerator
226
+ from nl2sql.cli.generators.llm import LLMGenerator
227
+ from nl2sql.cli.generators.policies import PolicyGenerator
228
+
229
+ def _write_config_file(path: pathlib.Path, content: str):
230
+ """Helper to write generator output to file."""
231
+ try:
232
+ path.parent.mkdir(parents=True, exist_ok=True)
233
+ with open(path, "w", encoding="utf-8") as f:
234
+ f.write(content)
235
+ print_success(f"Created {path}")
236
+ except Exception as e:
237
+ console.print(f"[red]Failed to write {escape(str(path.name))}: {escape(str(e))}[/red]")
238
+
239
+
240
+ def _configure_env_file(env: str, api_key: Optional[str] = None):
241
+ """Creates the .env.{env} file using the Universal Environment Protocol."""
242
+ target_file = PROJECT_ROOT / f".env.{env}"
243
+
244
+ if target_file.exists():
245
+ console.print(Panel(f"[bold]Environment Configuration ({env})[/bold]", border_style="blue"))
246
+ console.print(f"[dim]Existing {target_file.name} found.[/dim]")
247
+ return
248
+
249
+ console.print(Panel(f"[bold]Environment Configuration ({env})[/bold]", border_style="blue"))
250
+ console.print(f"Creating explicit configuration file: {target_file.name}")
251
+
252
+ # Generate Content
253
+ secrets = {}
254
+ if api_key:
255
+ secrets["OPENAI_API_KEY"] = api_key
256
+
257
+ content = EnvFileGenerator.generate(env, secrets=secrets)
258
+
259
+ try:
260
+ with open(target_file, "w", encoding="utf-8") as f:
261
+ f.write(content)
262
+ print_success(f"Created {target_file}")
263
+ except Exception as e:
264
+ console.print(f"[red]Failed to write env file: {escape(str(e))}[/red]")
265
+
266
+ def _install_required_adapters(config_manager: ConfigManager):
267
+ """Reads config using ConfigManager and installs necessary adapters."""
268
+ if not DATASOURCE_CONFIG.exists():
269
+ return
270
+
271
+ try:
272
+ # Use ConfigManager to load standardized objects
273
+ # load_datasources returns List[Dict]
274
+ configs = config_manager.load_datasources()
275
+
276
+ required = set()
277
+ for config in configs:
278
+ connection = config.get("connection", {})
279
+ engine = connection.get("type", "").lower() or config.get("type", "").lower()
280
+
281
+ for name, pkg in KNOWN_ADAPTERS.items():
282
+ if name in engine:
283
+ required.add((name, pkg))
284
+ break
285
+
286
+ if required:
287
+ print_step("Checking Adapters...")
288
+ for name, pkg in sorted(required):
289
+ # The adapter ships with nl2sql; the extra supplies the driver.
290
+ if not check_package(ADAPTER_DRIVERS[name]):
291
+ if inquirer.confirm(message=f"Required adapter {pkg} is missing. Install now?", default=True).execute():
292
+ console.print(f"[yellow]Installing {escape(pkg)}...[/yellow]")
293
+ install_package(pkg)
294
+ else:
295
+ console.print(f"[dim]Adapter {escape(pkg)} is installed.[/dim]")
296
+
297
+ except Exception as e:
298
+ console.print(f"[red]Failed to check adapters: {escape(str(e))}[/red]")
299
+
300
+
301
+ def _run_indexing_step():
302
+ """Offers schema indexing and runs it through a context built from config paths."""
303
+ from nl2sql.common.settings import settings
304
+ from nl2sql.context import NL2SQLContext
305
+ from nl2sql.indexing.vector_store import VectorStore
306
+ from nl2sql.cli.commands.indexing import run_indexing
307
+
308
+ try:
309
+ v_store = VectorStore(
310
+ collection_name=settings.vector_store_collection_name,
311
+ persist_directory=str(PROJECT_ROOT / settings.vector_store_path),
312
+ )
313
+ should_index = False
314
+
315
+ if not v_store.is_empty():
316
+ console.print("[yellow]Vector Store already contains data.[/yellow]")
317
+ if inquirer.confirm(message="Do you want to clear and re-index?", default=False).execute():
318
+ should_index = True
319
+ else:
320
+ if inquirer.confirm(message="Vector Store is empty. Run Schema Indexing now?", default=True).execute():
321
+ should_index = True
322
+
323
+ if not should_index:
324
+ return
325
+
326
+ print_step("Starting Indexer...")
327
+
328
+ # NL2SQLContext builds the datasource, LLM and vector store registries
329
+ # itself; it takes config *paths*, not pre-built registries.
330
+ ctx = NL2SQLContext(
331
+ ds_config_path=PROJECT_ROOT / settings.datasource_config_path,
332
+ secrets_config_path=PROJECT_ROOT / settings.secrets_config_path,
333
+ llm_config_path=PROJECT_ROOT / settings.llm_config_path,
334
+ vector_store_path=PROJECT_ROOT / settings.vector_store_path,
335
+ policies_config_path=PROJECT_ROOT / settings.policies_config_path,
336
+ )
337
+
338
+ run_indexing(ctx)
339
+ print_success("Indexing process finished.")
340
+
341
+ except Exception as e:
342
+ logger.debug("Indexing setup failed", exc_info=True)
343
+ console.print(f"[red]Indexing setup failed: {escape(str(e))}[/red]")
344
+
345
+
346
+ @handle_cli_errors
347
+ def setup_command(demo: bool = False, lite: bool = True, docker: bool = False, api_key: Optional[str] = None):
348
+
349
+ # Instantiate Managers
350
+ config_manager = ConfigManager(PROJECT_ROOT)
351
+ demo_manager = DemoManager(console, PROJECT_ROOT)
352
+
353
+ if demo:
354
+ console.print(Panel("[bold green]Setting up Demo Environment...[/bold green]", border_style="green"))
355
+
356
+ if lite:
357
+ demo_manager.setup_lite(api_key=api_key)
358
+ elif docker:
359
+ docker_dir = demo_manager.setup_docker(api_key=api_key)
360
+ if inquirer.confirm(message="Start Docker containers now?", default=True).execute():
361
+ demo_manager.start_docker_containers(docker_dir)
362
+
363
+ console.print(Panel(f"""[bold yellow]Next Steps:[/bold yellow]
364
+ 1. [bold]Verify & Index[/bold]:
365
+ Once database containers are healthy (~30s), run:
366
+ [cyan]nl2sql --env demo index[/cyan]
367
+
368
+ 2. [bold]API[/bold]: the 'app' container serves the REST API on
369
+ [cyan]http://localhost:8000[/cyan].
370
+
371
+ 3. [bold]MSSQL[/bold] is opt-in:
372
+ [cyan]docker compose -f docker-compose.demo.yml --profile mssql up -d[/cyan]
373
+ """, title="Docker Instructions", border_style="yellow")
374
+ )
375
+
376
+ if not docker:
377
+ print_step("Indexing Demo Environment...")
378
+ demo_manager.index_demo_data()
379
+ console.print("Run: [cyan]nl2sql --env demo run \"Show me broken machines in Austin\"[/cyan]")
380
+
381
+ return
382
+
383
+ # --- Standard Setup Wizard ---
384
+ console.print("[bold cyan]NL2SQL Setup Wizard[/bold cyan]\n")
385
+
386
+ config_manager.ensure_config_dirs()
387
+
388
+ # 1. Environment File
389
+ _configure_env_file("dev", api_key=api_key)
390
+
391
+ # 2. Datasource
392
+ _configure_datasource(config_manager)
393
+
394
+ # 2. LLM
395
+ _configure_llm(config_manager, api_key=api_key)
396
+
397
+ # 3. Policies
398
+ _configure_policies(config_manager)
399
+
400
+ # 4. Adapters
401
+ _install_required_adapters(config_manager)
402
+
403
+ # 5. Connectivity Check
404
+ print_step("Checking Database Connectivity...")
405
+ if not verify_connectivity(print_table=True):
406
+ console.print("[yellow]Warning: Some datasources are failing validation.[/yellow]")
407
+ if not inquirer.confirm(message="Continue anyway?", default=False).execute():
408
+ return
409
+
410
+ # 6. Indexing Prompt
411
+ console.print("")
412
+ _run_indexing_step()
413
+
414
+ console.print("\n[bold green]Setup Complete![/bold green]")
415
+ console.print("Try running a query: [cyan]nl2sql run \"Show me all tables\"[/cyan]")
@@ -0,0 +1,34 @@
1
+ from typing import List, Dict, Any
2
+ from langgraph.graph.state import CompiledStateGraph
3
+ from nl2sql.cli.reporting import ConsolePresenter
4
+
5
+ from nl2sql.cli.common.decorators import handle_cli_errors
6
+
7
+ @handle_cli_errors
8
+ def draw_execution_trace(
9
+ trace: List[Dict[str, Any]],
10
+ graph: CompiledStateGraph,
11
+ execution_subgraph: CompiledStateGraph,
12
+ agentic_execution_loop: CompiledStateGraph
13
+ ):
14
+ """Visualizes the execution trace in the CLI and saves the graph structure.
15
+
16
+ Args:
17
+ trace (List[Dict[str, Any]]): The execution trace.
18
+ graph (CompiledStateGraph): The main graph.
19
+ execution_subgraph (CompiledStateGraph): The execution subgraph.
20
+ agentic_execution_loop (CompiledStateGraph): The agentic loop subgraph.
21
+ """
22
+ presenter = ConsolePresenter()
23
+
24
+ try:
25
+ png_bytes = graph.get_graph(xray=True).draw_mermaid_png()
26
+ import os
27
+ output_path = "graph_trace.png"
28
+ abs_path = os.path.abspath(output_path)
29
+ with open(output_path, "wb") as f:
30
+ f.write(png_bytes)
31
+
32
+ presenter.print_graph_saved(abs_path)
33
+ except Exception as e:
34
+ presenter.print_graph_save_error(str(e))
@@ -0,0 +1,34 @@
1
+ from functools import wraps
2
+ import sys
3
+ import traceback
4
+ from rich.console import Console
5
+ from rich.markup import escape
6
+ from nl2sql.common.exceptions import NL2SQLError
7
+
8
+ console = Console()
9
+
10
+ def handle_cli_errors(func):
11
+ """
12
+ Decorator to wrap CLI commands with unified error handling.
13
+
14
+ - NL2SQLError: Prints a clean red error message.
15
+ - KeyboardInterrupt: Exits gracefully.
16
+ - Unexpected Exception: Prints stack trace and error.
17
+ """
18
+ @wraps(func)
19
+ def wrapper(*args, **kwargs):
20
+ try:
21
+ return func(*args, **kwargs)
22
+ except NL2SQLError as e:
23
+ console.print(f"[bold red]Error:[/bold red] {escape(str(e))}")
24
+ sys.exit(1)
25
+ except KeyboardInterrupt:
26
+ console.print("\n[yellow]Operation cancelled by user.[/yellow]")
27
+ sys.exit(130) # Standard SIGINT exit code
28
+ except Exception as e:
29
+ console.print(f"[bold red]Unexpected Error:[/bold red] {escape(str(e))}")
30
+ console.print(traceback.format_exc(), markup=False)
31
+ console.print("[dim]Please report this bug to the nl2sql team.[/dim]")
32
+ sys.exit(1)
33
+
34
+ return wrapper
nl2sql/cli/config.py ADDED
@@ -0,0 +1,24 @@
1
+
2
+ CORE_PACKAGE = "nl2sql"
3
+ CORE_MODULE = "nl2sql.cli"
4
+
5
+ # Every dialect adapter now ships inside the single `nl2sql-engine`
6
+ # distribution, so the adapter module is always importable. What is optional
7
+ # is the DB driver each one needs to actually connect, carried by an extra.
8
+ KNOWN_ADAPTERS = {
9
+ "sqlite": "nl2sql-engine",
10
+ "duckdb": "nl2sql-engine[duckdb]",
11
+ "postgresql": "nl2sql-engine[postgres]",
12
+ "mysql": "nl2sql-engine[mysql]",
13
+ "mssql": "nl2sql-engine[mssql]",
14
+ }
15
+
16
+ # The driver module that must import for the adapter above to be usable.
17
+ # sqlite needs none beyond the standard library.
18
+ ADAPTER_DRIVERS = {
19
+ "sqlite": "sqlite3",
20
+ "duckdb": "duckdb_engine",
21
+ "postgresql": "psycopg2",
22
+ "mysql": "pymysql",
23
+ "mssql": "pyodbc",
24
+ }
nl2sql/cli/console.py ADDED
@@ -0,0 +1,52 @@
1
+ import sys
2
+
3
+ from rich.console import Console
4
+ from rich.markup import escape
5
+ from rich.theme import Theme
6
+
7
+
8
+ def configure_output_encoding() -> None:
9
+ """Make CLI output safe on a console whose code page is not UTF-8.
10
+
11
+ Rich renders symbols such as U+2713 that the default Windows code page
12
+ (cp1252) cannot encode, so writing them raised UnicodeEncodeError and
13
+ aborted the command -- `nl2sql setup --demo --lite` died this way.
14
+ Reconfiguring the streams once, at the entry point, is what
15
+ PYTHONIOENCODING=utf-8 did for users who knew to set it; rich reads
16
+ ``sys.stdout`` and its encoding lazily, so consoles built at import time
17
+ pick this up too.
18
+
19
+ ``errors="replace"`` is the backstop: if a stream cannot become UTF-8, an
20
+ unencodable symbol degrades to a placeholder instead of killing the
21
+ process. Streams that are not text wrappers -- pytest capture, some
22
+ redirections -- have no ``reconfigure`` and are left alone.
23
+ """
24
+ for stream in (sys.stdout, sys.stderr):
25
+ reconfigure = getattr(stream, "reconfigure", None)
26
+ if reconfigure is None:
27
+ continue
28
+ try:
29
+ reconfigure(encoding="utf-8", errors="replace")
30
+ except (OSError, ValueError):
31
+ # A detached or already-closed stream. Nothing to configure.
32
+ pass
33
+
34
+
35
+ custom_theme = Theme({
36
+ "info": "cyan",
37
+ "warning": "yellow",
38
+ "error": "bold red",
39
+ "success": "bold green",
40
+ "command": "bold white on blue",
41
+ })
42
+
43
+ console = Console(theme=custom_theme)
44
+
45
+ def print_step(message: str) -> None:
46
+ console.print(f"[bold blue]Step:[/bold blue] {escape(str(message))}")
47
+
48
+ def print_success(message: str) -> None:
49
+ console.print(f"[success][OK] {escape(str(message))}[/success]")
50
+
51
+ def print_error(message: str) -> None:
52
+ console.print(f"[error][ERROR] {escape(str(message))}[/error]")
@@ -0,0 +1 @@
1
+ from .manager import DemoManager
@@ -0,0 +1,87 @@
1
+ from typing import List, Dict, Any
2
+
3
+ # Shared Constants (The "Glue")
4
+
5
+ FACTORIES: List[Dict[str, Any]] = [
6
+ {"id": 1, "name": "Austin Gigafactory", "region": "US", "capacity": 5000},
7
+ {"id": 2, "name": "Berlin Plant", "region": "EU", "capacity": 3500},
8
+ {"id": 3, "name": "Shanghai Facility", "region": "CN", "capacity": 8000},
9
+ {"id": 4, "name": "Tokyo Hub", "region": "JP", "capacity": 4200},
10
+ {"id": 5, "name": "Mumbai Plant", "region": "IN", "capacity": 3000},
11
+ ]
12
+
13
+ SHIFTS: List[Dict[str, Any]] = [
14
+ {"id": 1, "name": "Morning", "start_time": "06:00", "end_time": "14:00"},
15
+ {"id": 2, "name": "Evening", "start_time": "14:00", "end_time": "22:00"},
16
+ {"id": 3, "name": "Night", "start_time": "22:00", "end_time": "06:00"},
17
+ ]
18
+
19
+ MACHINE_TYPES: List[Dict[str, Any]] = [
20
+ {"id": 1, "model": "Robotic Arm v1", "producer": "TechCorp", "maintenance_interval_days": 30},
21
+ {"id": 2, "model": "Conveyor Belt Gen3", "producer": "MoveIt", "maintenance_interval_days": 180},
22
+ {"id": 3, "model": "Stamping Press 500T", "producer": "HeavyMetal", "maintenance_interval_days": 90},
23
+ {"id": 4, "model": "Painting Station", "producer": "ColorsInc", "maintenance_interval_days": 14},
24
+ {"id": 5, "model": "Quality Scanner", "producer": "VisionAI", "maintenance_interval_days": 60},
25
+ {"id": 6, "model": "Laser Cutter L2", "producer": "PhotonWorks", "maintenance_interval_days": 45},
26
+ {"id": 7, "model": "CNC Mill Pro", "producer": "Machina", "maintenance_interval_days": 120},
27
+ ]
28
+
29
+ PRODUCTS: List[Dict[str, Any]] = [
30
+ # High Value
31
+ {"id": 1, "sku": "IC-5000", "name": "Industrial Controller", "base_cost": 4500.00, "category": "Electronics"},
32
+ {"id": 2, "sku": "BAT-EV-X", "name": "EV Battery Pack Long Range", "base_cost": 8000.00, "category": "Components"},
33
+ {"id": 3, "sku": "MOT-HI-T", "name": "High Torque Motor", "base_cost": 1200.00, "category": "Components"},
34
+ # High Volume
35
+ {"id": 4, "sku": "BOLT-M5", "name": "Bolt M5 Stainless", "base_cost": 0.50, "category": "Hardware"},
36
+ {"id": 5, "sku": "WASH-M5", "name": "Washer M5", "base_cost": 0.10, "category": "Hardware"},
37
+ {"id": 6, "sku": "CAB-USB-C", "name": "USB-C Data Cable", "base_cost": 2.50, "category": "Electronics"},
38
+ {"id": 7, "sku": "BRK-SET", "name": "Brake Assembly Set", "base_cost": 350.00, "category": "Components"},
39
+ {"id": 8, "sku": "SNS-TEMP", "name": "Temperature Sensor Pack", "base_cost": 85.00, "category": "Electronics"},
40
+ {"id": 9, "sku": "PNL-AL", "name": "Aluminum Panel Sheet", "base_cost": 40.00, "category": "Materials"},
41
+ ]
42
+
43
+ SUPPLIERS: List[Dict[str, Any]] = [
44
+ {"id": 1, "name": "Global Tech Components", "country": "Taiwan"},
45
+ {"id": 2, "name": "SteelCorp International", "country": "Germany"},
46
+ {"id": 3, "name": "ChemSafe Solutions", "country": "USA"},
47
+ {"id": 4, "name": "Nippon Metals", "country": "Japan"},
48
+ {"id": 5, "name": "Indus Industrial", "country": "India"},
49
+ ]
50
+
51
+ DEPARTMENTS: List[Dict[str, Any]] = [
52
+ {"id": 1, "name": "Assembly"},
53
+ {"id": 2, "name": "Quality Assurance"},
54
+ {"id": 3, "name": "Logistics"},
55
+ {"id": 4, "name": "Maintenance"},
56
+ {"id": 5, "name": "Engineering"},
57
+ ]
58
+
59
+ CUSTOMER_SEGMENTS: List[Dict[str, Any]] = [
60
+ {"id": 1, "name": "Enterprise"},
61
+ {"id": 2, "name": "SMB"},
62
+ {"id": 3, "name": "Retail"},
63
+ ]
64
+
65
+ EMPLOYEE_ROLES: List[Dict[str, Any]] = [
66
+ {"id": 1, "title": "Operator", "department_id": 1},
67
+ {"id": 2, "title": "Line Supervisor", "department_id": 1},
68
+ {"id": 3, "title": "QA Analyst", "department_id": 2},
69
+ {"id": 4, "title": "Logistics Coordinator", "department_id": 3},
70
+ {"id": 5, "title": "Maintenance Technician", "department_id": 4},
71
+ {"id": 6, "title": "Process Engineer", "department_id": 5},
72
+ ]
73
+
74
+ SUPPLIER_PRODUCTS: List[Dict[str, Any]] = [
75
+ {"supplier_id": 1, "product_id": 1},
76
+ {"supplier_id": 1, "product_id": 2},
77
+ {"supplier_id": 1, "product_id": 8},
78
+ {"supplier_id": 2, "product_id": 4},
79
+ {"supplier_id": 2, "product_id": 5},
80
+ {"supplier_id": 2, "product_id": 9},
81
+ {"supplier_id": 3, "product_id": 6},
82
+ {"supplier_id": 3, "product_id": 7},
83
+ {"supplier_id": 4, "product_id": 3},
84
+ {"supplier_id": 4, "product_id": 9},
85
+ {"supplier_id": 5, "product_id": 7},
86
+ {"supplier_id": 5, "product_id": 4},
87
+ ]