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
nl2sql/cli/types.py ADDED
@@ -0,0 +1,13 @@
1
+ from pydantic import BaseModel
2
+ from typing import Optional
3
+
4
+ class RunConfig(BaseModel):
5
+ """Configuration for running the pipeline."""
6
+ query: str
7
+ ds_id: Optional[str] = None
8
+ role: str = "admin"
9
+ no_exec: bool = False
10
+ verbose: bool = False
11
+ show_perf: bool = False
12
+
13
+
@@ -0,0 +1 @@
1
+ """Common utilities and shared models for the NL2SQL package."""
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from typing import Optional
5
+
6
+
7
+ class CancellationToken:
8
+ """Per-run cancellation flag.
9
+
10
+ Each run owns its own token, so cancelling one run never affects another.
11
+ Runs pass their token to the graph via
12
+ ``config={"configurable": {"cancellation_token": token}}``.
13
+ """
14
+
15
+ def __init__(self) -> None:
16
+ self._event = threading.Event()
17
+
18
+ def cancel(self) -> None:
19
+ self._event.set()
20
+
21
+ def is_cancelled(self) -> bool:
22
+ return self._event.is_set()
23
+
24
+ def wait(self, timeout: Optional[float] = None) -> bool:
25
+ return self._event.wait(timeout=timeout)
@@ -0,0 +1,5 @@
1
+ """Global context management for the NL2SQL pipeline."""
2
+ from contextvars import ContextVar
3
+ from typing import Optional
4
+
5
+ current_datasource_id: ContextVar[Optional[str]] = ContextVar("current_datasource_id", default=None)
@@ -0,0 +1,109 @@
1
+ from enum import Enum, auto
2
+ from typing import Optional, Any
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+
6
+ class ErrorSeverity(str, Enum):
7
+ """Severity levels for pipeline errors."""
8
+ INFO = "INFO"
9
+ WARNING = "WARNING"
10
+ ERROR = "ERROR"
11
+ CRITICAL = "CRITICAL"
12
+
13
+
14
+ class ErrorCode(str, Enum):
15
+ """Standardized error codes for the pipeline."""
16
+ MISSING_LLM = "MISSING_LLM"
17
+ MISSING_SQL = "MISSING_SQL"
18
+ MISSING_DATASOURCE_ID = "MISSING_DATASOURCE_ID"
19
+ MISSING_PLAN = "MISSING_PLAN"
20
+ INVALID_STATE = "INVALID_STATE"
21
+ INVALID_PLAN_STRUCTURE = "INVALID_PLAN_STRUCTURE"
22
+ SCHEMA_RETRIEVAL_FAILED = "SCHEMA_RETRIEVAL_FAILED"
23
+ SQL_GEN_FAILED = "SQL_GEN_FAILED"
24
+ TABLE_NOT_FOUND = "TABLE_NOT_FOUND"
25
+ COLUMN_NOT_FOUND = "COLUMN_NOT_FOUND"
26
+ INVALID_ALIAS_USAGE = "INVALID_ALIAS_USAGE"
27
+ MISSING_GROUP_BY = "MISSING_GROUP_BY"
28
+ INVALID_DATE_FORMAT = "INVALID_DATE_FORMAT"
29
+ INVALID_NUMERIC_VALUE = "INVALID_NUMERIC_VALUE"
30
+ JOIN_TABLE_NOT_IN_PLAN = "JOIN_TABLE_NOT_IN_PLAN"
31
+ JOIN_MISSING_ON_CLAUSE = "JOIN_MISSING_ON_CLAUSE"
32
+ SECURITY_VIOLATION = "SECURITY_VIOLATION"
33
+ SAFEGUARD_VIOLATION = "SAFEGUARD_VIOLATION"
34
+ DB_EXECUTION_ERROR = "DB_EXECUTION_ERROR"
35
+ EXECUTOR_CRASH = "EXECUTOR_CRASH"
36
+ PLANNING_FAILURE = "PLANNING_FAILURE"
37
+ VALIDATOR_CRASH = "VALIDATOR_CRASH"
38
+ REFINER_FAILED = "REFINER_FAILED"
39
+ PHYSICAL_VALIDATOR_FAILED = "PHYSICAL_VALIDATOR_FAILED"
40
+ PLAN_FEEDBACK = "PLAN_FEEDBACK"
41
+ UNKNOWN_ERROR = "UNKNOWN_ERROR"
42
+ AGGREGATOR_FAILED = "AGGREGATOR_FAILED"
43
+ PERFORMANCE_WARNING = "PERFORMANCE_WARNING"
44
+ EXECUTION_ERROR = "EXECUTION_ERROR"
45
+ ORCHESTRATOR_CRASH = "ORCHESTRATOR_CRASH"
46
+ INTENT_VIOLATION = "INTENT_VIOLATION"
47
+ PIPELINE_TIMEOUT = "PIPELINE_TIMEOUT"
48
+ SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE"
49
+ EXECUTION_TIMEOUT = "EXECUTION_TIMEOUT"
50
+ CANCELLED = "CANCELLED",
51
+ EXECUTION_FAILED = "EXECUTION_FAILED"
52
+
53
+
54
+
55
+ FATAL_ERRORS = {
56
+ ErrorCode.SECURITY_VIOLATION,
57
+ ErrorCode.INTENT_VIOLATION,
58
+ ErrorCode.SAFEGUARD_VIOLATION,
59
+ ErrorCode.MISSING_DATASOURCE_ID,
60
+ ErrorCode.MISSING_LLM,
61
+ ErrorCode.INVALID_STATE
62
+ }
63
+
64
+ SAFE_ERROR_MESSAGES = {
65
+ ErrorCode.DB_EXECUTION_ERROR: "An internal database error occurred while executing the query.",
66
+ ErrorCode.SAFEGUARD_VIOLATION: "The query result was blocked by data protection safeguards.",
67
+ ErrorCode.EXECUTOR_CRASH: "The query execution service encountered an unexpected error.",
68
+ ErrorCode.VALIDATOR_CRASH: "The validation service encountered an unexpected error.",
69
+ ErrorCode.MISSING_DATASOURCE_ID: "Datasource configuration error."
70
+ }
71
+
72
+ class PipelineError(BaseModel):
73
+ """Represents a structured error within the pipeline.
74
+
75
+ Attributes:
76
+ node (str): The node where the error occurred.
77
+ message (str): A human-readable error message.
78
+ severity (ErrorSeverity): The severity of the error.
79
+ error_code (ErrorCode): The standardized error code.
80
+ stack_trace (Optional[str]): Stack trace if applicable.
81
+ details (Optional[Any]): Additional context or metadata.
82
+ """
83
+ model_config = ConfigDict(extra="ignore")
84
+
85
+ node: str
86
+ message: str
87
+ severity: ErrorSeverity
88
+ error_code: ErrorCode
89
+ stack_trace: Optional[str] = None
90
+ details: Optional[Any] = None
91
+
92
+ @property
93
+ def is_retryable(self) -> bool:
94
+ """Determines if this error should trigger a retry/refinement loop."""
95
+ if self.severity == ErrorSeverity.CRITICAL:
96
+ return False
97
+ return self.error_code not in FATAL_ERRORS
98
+
99
+ def get_safe_message(self) -> str:
100
+ """Returns a sanitized error message safe for exposure to LLMs or users.
101
+
102
+ If a safe mapping exists for the error code, it is returned.
103
+ Otherwise, the original message is used (assuming it's safe).
104
+
105
+ Returns:
106
+ str: The sanitized error message.
107
+ """
108
+ return SAFE_ERROR_MESSAGES.get(self.error_code, self.message)
109
+
@@ -0,0 +1,88 @@
1
+ import logging
2
+ import json
3
+ import os
4
+ from logging.handlers import RotatingFileHandler
5
+ from typing import Any, Dict, Optional
6
+ from datetime import datetime
7
+
8
+ class EventLogger:
9
+ """Persistent audit logger for high-value AI events.
10
+
11
+ Writes structured JSON events to a dedicated log file, separate from
12
+ application debug logs. Used for forensic analysis and "Time Travel" debugging.
13
+ """
14
+
15
+ def __init__(self):
16
+ self.logger = logging.getLogger("nl2sql.audit")
17
+ self.logger.setLevel(logging.INFO)
18
+ self.logger.propagate = False # Do not bubble up to root logger (avoid stdout spam)
19
+
20
+ # Ensure handlers are set up (singleton-ish check)
21
+ if not self.logger.handlers:
22
+ log_path = "logs/audit_events.log"
23
+
24
+ # Ensure directory exists
25
+ os.makedirs(os.path.dirname(log_path), exist_ok=True)
26
+
27
+ # 10MB per file, max 5 backup files
28
+ handler = RotatingFileHandler(
29
+ log_path, maxBytes=10*1024*1024, backupCount=5, encoding="utf-8"
30
+ )
31
+
32
+ # Use specific JSON formatter for audit events
33
+ formatter = logging.Formatter("%(message)s")
34
+ handler.setFormatter(formatter)
35
+
36
+ self.logger.addHandler(handler)
37
+
38
+ def log_event(
39
+ self,
40
+ event_type: str,
41
+ payload: Dict[str, Any],
42
+ trace_id: Optional[str] = None,
43
+ tenant_id: Optional[str] = None
44
+ ):
45
+ """Logs a structured event to the audit log.
46
+
47
+ Args:
48
+ event_type: Category of event (e.g., 'llm_interaction', 'security_violation')
49
+ payload: The event data dictionary.
50
+ trace_id: Correlation ID.
51
+ tenant_id: Tenant/Customer ID.
52
+ """
53
+
54
+ sensitive_keys = {"api_key", "password", "secret", "authorization"}
55
+ cleaned_payload = self._redact(payload, sensitive_keys)
56
+
57
+ event = {
58
+ "timestamp": datetime.utcnow().isoformat(),
59
+ "event_type": event_type,
60
+ "trace_id": trace_id,
61
+ "tenant_id": tenant_id,
62
+ "data": cleaned_payload
63
+ }
64
+
65
+ self.logger.info(json.dumps(event))
66
+
67
+ def _redact(self, data: Any, keys_to_redact: set) -> Any:
68
+ """Recursively redact sensitive keys from dictionary.
69
+
70
+ Args:
71
+ data: Input data (dict, list, or primitive).
72
+ keys_to_redact: Set of lowercase keys to match and redact.
73
+
74
+ Returns:
75
+ The sanitized data structure with sensitive values replaced by '***REDACTED***'.
76
+ """
77
+ if isinstance(data, dict):
78
+ return {
79
+ k: ("***REDACTED***" if k.lower() in keys_to_redact else self._redact(v, keys_to_redact))
80
+ for k, v in data.items()
81
+ }
82
+ elif isinstance(data, list):
83
+ return [self._redact(item, keys_to_redact) for item in data]
84
+ else:
85
+ return data
86
+
87
+ # Global instance
88
+ event_logger = EventLogger()
@@ -0,0 +1,3 @@
1
+ class NL2SQLError(Exception):
2
+ """Base exception for all NL2SQL errors."""
3
+ pass
@@ -0,0 +1,119 @@
1
+ import logging
2
+ import json
3
+ import time
4
+ import contextvars
5
+ from contextlib import contextmanager
6
+ from typing import Any, Dict, Optional
7
+
8
+ _trace_id_ctx = contextvars.ContextVar("trace_id", default=None)
9
+ _tenant_id_ctx = contextvars.ContextVar("tenant_id", default=None)
10
+
11
+ class TraceContextFilter(logging.Filter):
12
+ """Injects trace_id and tenant_id from contextvars into the log record."""
13
+ def filter(self, record):
14
+ record.trace_id = _trace_id_ctx.get()
15
+ record.tenant_id = _tenant_id_ctx.get()
16
+ return True
17
+
18
+ @contextmanager
19
+ def trace_context(trace_id: str):
20
+ """Context manager to set the trace_id for the current context."""
21
+ token = _trace_id_ctx.set(trace_id)
22
+ try:
23
+ yield
24
+ finally:
25
+ _trace_id_ctx.reset(token)
26
+
27
+ @contextmanager
28
+ def tenant_context(tenant_id: Optional[str]):
29
+ """Context manager to set the tenant_id for the current context."""
30
+ token = _tenant_id_ctx.set(tenant_id)
31
+ try:
32
+ yield
33
+ finally:
34
+ _tenant_id_ctx.reset(token)
35
+
36
+ class JsonFormatter(logging.Formatter):
37
+ """Formatter that outputs JSON strings after parsing the LogRecord."""
38
+
39
+ def format(self, record: logging.LogRecord) -> str:
40
+ """Formats the log record as a JSON string.
41
+
42
+ Args:
43
+ record (logging.LogRecord): The log record to format.
44
+
45
+ Returns:
46
+ str: The JSON-formatted log string.
47
+ """
48
+ log_record = {
49
+ "timestamp": self.formatTime(record, self.datefmt),
50
+ "level": record.levelname,
51
+ "name": record.name,
52
+ "message": record.getMessage(),
53
+ }
54
+
55
+ if getattr(record, "trace_id", None):
56
+ log_record["trace_id"] = record.trace_id
57
+
58
+ if getattr(record, "tenant_id", None):
59
+ log_record["tenant_id"] = record.tenant_id
60
+
61
+ # Standard LogRecord attributes to ignore
62
+ standard_attrs = {
63
+ "args", "asctime", "created", "exc_info", "exc_text", "filename",
64
+ "funcName", "levelname", "levelno", "lineno", "module",
65
+ "msecs", "message", "msg", "name", "pathname", "process",
66
+ "processName", "relativeCreated", "stack_info", "thread", "threadName",
67
+ "taskName", "trace_id", "tenant_id"
68
+ }
69
+
70
+ for key, value in record.__dict__.items():
71
+ if key not in standard_attrs and not key.startswith("_"):
72
+ log_record[key] = value
73
+
74
+ return json.dumps(log_record)
75
+
76
+
77
+ def configure_logging(level: str = "INFO", json_format: bool = False):
78
+ """Configures the root logger.
79
+
80
+ Args:
81
+ level (str): The logging level (default: INFO).
82
+ json_format (bool): Whether to use JSON formatting (default: False).
83
+ """
84
+ root_logger = logging.getLogger()
85
+ root_logger.setLevel(level)
86
+
87
+ # Remove existing handlers
88
+ for handler in root_logger.handlers[:]:
89
+ root_logger.removeHandler(handler)
90
+
91
+ handler = logging.StreamHandler()
92
+ handler.addFilter(TraceContextFilter())
93
+
94
+ if json_format:
95
+ handler.setFormatter(JsonFormatter())
96
+ else:
97
+ # Standard text format
98
+ formatter = logging.Formatter(
99
+ "%(asctime)s - [%(trace_id)s] - %(name)s - %(levelname)s - %(message)s"
100
+ )
101
+ handler.setFormatter(formatter)
102
+
103
+ root_logger.addHandler(handler)
104
+
105
+ # Silence noisy libraries
106
+ logging.getLogger("httpx").setLevel(logging.WARNING)
107
+ logging.getLogger("httpcore").setLevel(logging.WARNING)
108
+
109
+
110
+ def get_logger(name: str) -> logging.Logger:
111
+ """Gets a named logger.
112
+
113
+ Args:
114
+ name (str): The name of the logger.
115
+
116
+ Returns:
117
+ logging.Logger: The logger instance.
118
+ """
119
+ return logging.getLogger(name)
@@ -0,0 +1,50 @@
1
+ """Performance metrics tracking with OpenTelemetry support."""
2
+ from typing import List, Dict, Any, Optional
3
+ from opentelemetry import metrics
4
+ from opentelemetry.sdk.metrics import MeterProvider
5
+ from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader
6
+ from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
7
+
8
+ # Legacy lists for CLI compatibility
9
+ TOKEN_LOG: List[Dict[str, Any]] = []
10
+ LATENCY_LOG: List[Dict[str, Any]] = []
11
+
12
+ _meter = metrics.get_meter("nl2sql.core")
13
+ node_duration_histogram = _meter.create_histogram(
14
+ name="nl2sql.node.duration",
15
+ description="Duration of node execution in seconds",
16
+ unit="s",
17
+ )
18
+ token_usage_counter = _meter.create_counter(
19
+ name="nl2sql.token.usage",
20
+ description="Number of tokens used by LLM interactions",
21
+ unit="1",
22
+ )
23
+
24
+
25
+ def configure_metrics(exporter_type: str = "none", otlp_endpoint: Optional[str] = None):
26
+ """Configures the OpenTelemetry Metric Provider.
27
+
28
+ Args:
29
+ exporter_type: 'none', 'console', or 'otlp'
30
+ otlp_endpoint: Optional endpoint for OTLP exporter
31
+ """
32
+ if exporter_type == "none":
33
+ return
34
+
35
+ reader = None
36
+ if exporter_type == "console":
37
+ reader = PeriodicExportingMetricReader(ConsoleMetricExporter())
38
+ elif exporter_type == "otlp":
39
+ endpoint = otlp_endpoint or "http://localhost:4317"
40
+ reader = PeriodicExportingMetricReader(OTLPMetricExporter(endpoint=endpoint))
41
+
42
+ if reader:
43
+ provider = MeterProvider(metric_readers=[reader])
44
+ metrics.set_meter_provider(provider)
45
+
46
+
47
+ def reset_usage():
48
+ """Resets the token and latency logs."""
49
+ TOKEN_LOG.clear()
50
+ LATENCY_LOG.clear()
@@ -0,0 +1,59 @@
1
+ """
2
+ Resilience Module: Circuit Breakers and Fault Tolerance.
3
+
4
+ This module centralizes the configuration of Circuit Breakers using `pybreaker`.
5
+ It allows the system to Fail Fast when the vector store is unavailable.
6
+
7
+ Features:
8
+ - Global Breaker Instance (VECTOR)
9
+ - Observability via CircuitBreakerListener
10
+ - Safe Exclusion of "Soft Failures" (e.g., Rate Limits)
11
+ """
12
+ import pybreaker
13
+ from typing import Any, Optional, List, Type
14
+ from nl2sql.common.logger import get_logger
15
+
16
+ logger = get_logger("resilience")
17
+
18
+ class ObservabilityListener(pybreaker.CircuitBreakerListener):
19
+ """Listener to export circuit breaker state changes and failures to logs/metrics."""
20
+
21
+ def state_change(self, cb, old_state, new_state):
22
+ logger.warning(
23
+ f"Circuit Breaker '{cb.name}' changed state: {old_state.name} -> {new_state.name}"
24
+ )
25
+ # TODO: Emit metric: breaker_state_change{name=cb.name, state=new_state.name}
26
+
27
+ def failure(self, cb, exc):
28
+ logger.error(
29
+ f"Circuit Breaker '{cb.name}' recorded failure: {type(exc).__name__}: {exc}"
30
+ )
31
+ # TODO: Emit metric: breaker_failure{name=cb.name, error=type(exc).__name__}
32
+
33
+ def success(self, cb):
34
+ pass
35
+ # TODO: Emit metric: breaker_success{name=cb.name}
36
+
37
+
38
+ def create_breaker(
39
+ name: str,
40
+ fail_max: int = 5,
41
+ reset_timeout: int = 60,
42
+ exclude: Optional[List[Type[Exception]]] = None
43
+ ) -> pybreaker.CircuitBreaker:
44
+ """Factory to create a configured Circuit Breaker."""
45
+ return pybreaker.CircuitBreaker(
46
+ fail_max=fail_max,
47
+ reset_timeout=reset_timeout,
48
+ name=name,
49
+ listeners=[ObservabilityListener()],
50
+ exclude=exclude or []
51
+ )
52
+
53
+
54
+ # Vector Breaker: Retrieval Layer
55
+ VECTOR_BREAKER = create_breaker(
56
+ name="VECTOR_BREAKER",
57
+ fail_max=5,
58
+ reset_timeout=30 # Faster recovery for infra blips
59
+ )
@@ -0,0 +1,195 @@
1
+ from typing import Optional
2
+ import os
3
+ from pydantic import Field
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
+ from nl2sql.common.logger import get_logger
6
+
7
+ logger = get_logger(__name__)
8
+
9
+ class Settings(BaseSettings):
10
+ """Application configuration settings backed by environment variables."""
11
+
12
+ openai_api_key: Optional[str] = Field(default=None, validation_alias="OPENAI_API_KEY")
13
+ vector_store_path: Optional[str] = Field(
14
+ default="./chroma_db",
15
+ validation_alias="VECTOR_STORE",
16
+ description="Persist directory for the vector store."
17
+ )
18
+ vector_store_collection_name: str = Field(
19
+ default="nl2sql_store",
20
+ validation_alias="VECTOR_STORE_COLLECTION",
21
+ description="Chroma collection name for schema embeddings."
22
+ )
23
+ llm_config_path: str = Field(default="configs/llm.yaml", validation_alias="LLM_CONFIG")
24
+ datasource_config_path: str = Field(default="configs/datasources.yaml", validation_alias="DATASOURCE_CONFIG")
25
+ secrets_config_path: str = Field(default="configs/secrets.yaml", validation_alias="SECRETS_CONFIG")
26
+ embedding_model: str = Field(default="text-embedding-3-small", validation_alias="EMBEDDING_MODEL")
27
+ embedding_provider: str = Field(
28
+ default="openai",
29
+ validation_alias="EMBEDDING_PROVIDER",
30
+ description="Embedding backend: 'openai' (API key required) or 'local' (key-free ONNX model)."
31
+ )
32
+ tenant_id: str = Field(default="default_tenant", validation_alias="TENANT_ID")
33
+ sample_questions_path: str = Field(
34
+ default="configs/sample_questions.yaml",
35
+ validation_alias="SAMPLE_QUESTIONS",
36
+ description="Path to the YAML file containing sample questions for routing."
37
+ )
38
+ policies_config_path: str = Field(
39
+ default="configs/policies.json",
40
+ validation_alias="POLICIES_CONFIG",
41
+ description="Path to the JSON file containing RBAC policies and permissions."
42
+ )
43
+
44
+ global_timeout_sec: int = Field(
45
+ default=60,
46
+ validation_alias="GLOBAL_TIMEOUT_SEC",
47
+ description="Global timeout in seconds for pipeline execution."
48
+ )
49
+
50
+ sandbox_exec_workers: int = Field(
51
+ default=4,
52
+ validation_alias="SANDBOX_EXEC_WORKERS",
53
+ description="Max workers for latency-sensitive execution pool."
54
+ )
55
+
56
+ result_artifact_backend: str = Field(
57
+ default="local",
58
+ validation_alias="RESULT_ARTIFACT_BACKEND",
59
+ description="Artifact backend to store executor results: local, s3, adls."
60
+ )
61
+ result_artifact_base_uri: str = Field(
62
+ default="./artifacts",
63
+ validation_alias="RESULT_ARTIFACT_BASE_URI",
64
+ description="Base URI or path for artifact storage."
65
+ )
66
+ result_artifact_path_template: str = Field(
67
+ default="<tenant_id>/<request_id>.parquet",
68
+ validation_alias="RESULT_ARTIFACT_PATH_TEMPLATE",
69
+ description=(
70
+ "Template for artifact paths, relative to the backend root. "
71
+ "Placeholders are <key> names resolved from executor metadata; "
72
+ "the executor supplies tenant_id, request_id and schema_version."
73
+ )
74
+ )
75
+ result_artifact_s3_bucket: Optional[str] = Field(
76
+ default=None,
77
+ validation_alias="RESULT_ARTIFACT_S3_BUCKET",
78
+ description="S3 bucket for artifact storage."
79
+ )
80
+ result_artifact_s3_prefix: Optional[str] = Field(
81
+ default=None,
82
+ validation_alias="RESULT_ARTIFACT_S3_PREFIX",
83
+ description="S3 prefix for artifact storage."
84
+ )
85
+ result_artifact_adls_account: Optional[str] = Field(
86
+ default=None,
87
+ validation_alias="RESULT_ARTIFACT_ADLS_ACCOUNT",
88
+ description="ADLS storage account name."
89
+ )
90
+ result_artifact_adls_container: Optional[str] = Field(
91
+ default=None,
92
+ validation_alias="RESULT_ARTIFACT_ADLS_CONTAINER",
93
+ description="ADLS container name."
94
+ )
95
+ result_artifact_adls_connection_string: Optional[str] = Field(
96
+ default=None,
97
+ validation_alias="RESULT_ARTIFACT_ADLS_CONNECTION_STRING",
98
+ description="ADLS connection string, if using key-based auth."
99
+ )
100
+
101
+ schema_store_backend: str = Field(
102
+ default="sqlite",
103
+ validation_alias="SCHEMA_STORE_BACKEND",
104
+ description="Schema store backend identifier (e.g., 'sqlite', 'memory')."
105
+ )
106
+ schema_store_path: str = Field(
107
+ default="data/schema_store.db",
108
+ validation_alias="SCHEMA_STORE_PATH",
109
+ description="SQLite database path for schema store persistence."
110
+ )
111
+ schema_store_max_versions: int = Field(
112
+ default=3,
113
+ validation_alias="SCHEMA_STORE_MAX_VERSIONS",
114
+ description="Max versions to retain per datasource in schema store."
115
+ )
116
+ schema_version_mismatch_policy: str = Field(
117
+ default="warn",
118
+ validation_alias="SCHEMA_VERSION_MISMATCH_POLICY",
119
+ description="Action when chunk schema_version differs from SchemaStore: warn, fail, or ignore."
120
+ )
121
+
122
+ logical_validator_strict_columns: bool = Field(
123
+ default=False,
124
+ validation_alias="LOGICAL_VALIDATOR_STRICT_COLUMNS",
125
+ description="Treat missing columns as errors in logical validation."
126
+ )
127
+
128
+ sql_agent_max_retries: int = Field(
129
+ default=3,
130
+ validation_alias="SQL_AGENT_MAX_RETRIES",
131
+ description="Max retry attempts for SQL agent refinement loop."
132
+ )
133
+ sql_agent_retry_base_delay_sec: float = Field(
134
+ default=1.0,
135
+ validation_alias="SQL_AGENT_RETRY_BASE_DELAY_SEC",
136
+ description="Base delay for SQL agent retries (seconds)."
137
+ )
138
+ sql_agent_retry_max_delay_sec: float = Field(
139
+ default=10.0,
140
+ validation_alias="SQL_AGENT_RETRY_MAX_DELAY_SEC",
141
+ description="Max delay for SQL agent retries (seconds)."
142
+ )
143
+ sql_agent_retry_jitter_sec: float = Field(
144
+ default=0.5,
145
+ validation_alias="SQL_AGENT_RETRY_JITTER_SEC",
146
+ description="Max jitter added to SQL agent retry delays (seconds)."
147
+ )
148
+
149
+ observability_exporter: str = Field(
150
+ default="none",
151
+ validation_alias="OBSERVABILITY_EXPORTER",
152
+ description="Exporter for metrics/traces: 'none', 'console', 'otlp'."
153
+ )
154
+
155
+ otlp_endpoint: Optional[str] = Field(
156
+ default=None,
157
+ validation_alias="OTEL_EXPORTER_OTLP_ENDPOINT",
158
+ description="Endpoint for OTLP exporter (e.g. http://localhost:4317)."
159
+ )
160
+
161
+ model_config = SettingsConfigDict(
162
+ env_file=".env",
163
+ env_file_encoding="utf-8",
164
+ extra="ignore"
165
+ )
166
+
167
+
168
+ def load_settings() -> Settings:
169
+ """Load settings using ENV_FILE_PATH or ENV/APP_ENV when provided."""
170
+ env_file_path = os.getenv("ENV_FILE_PATH")
171
+ env_name = os.getenv("ENV") or os.getenv("APP_ENV")
172
+
173
+ if env_file_path:
174
+ logger.info(f"Loading settings from ENV_FILE_PATH={env_file_path}")
175
+ return Settings(_env_file=env_file_path)
176
+ if env_name:
177
+ env_file = f".env.{env_name}"
178
+ logger.info(f"Loading settings from {env_file}")
179
+ return Settings(_env_file=env_file)
180
+
181
+ logger.info("Loading settings from default .env")
182
+ return Settings()
183
+
184
+
185
+ settings = load_settings()
186
+
187
+ def reload_settings() -> Settings:
188
+ """Re-read settings from the environment and refresh the module singleton.
189
+
190
+ The ``settings`` object is created at import time, so switching environments
191
+ at runtime requires updating it in place; existing
192
+ ``from nl2sql.common.settings import settings`` references stay valid.
193
+ """
194
+ settings.__dict__.update(load_settings().__dict__)
195
+ return settings
@@ -0,0 +1,6 @@
1
+
2
+ from .datasources import DatasourceConfig, ConnectionConfig, DatasourceFileConfig
3
+ from .llm import LLMFileConfig, AgentConfig
4
+ from .policies import PolicyFileConfig, RolePolicy
5
+ from .secrets import SecretProviderConfig
6
+ from .manager import ConfigManager