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,46 @@
1
+ from typing import Optional
2
+ import os
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ class HashiCorpSecretProvider:
8
+ """Fetches secrets from HashiCorp Vault."""
9
+
10
+ def __init__(self):
11
+ try:
12
+ import hvac
13
+ url = os.environ.get("VAULT_ADDR", "http://localhost:8200")
14
+ token = os.environ.get("VAULT_TOKEN")
15
+
16
+ # Simple Token Auth for now, can extend to AppRole later
17
+ self.client = hvac.Client(url=url, token=token)
18
+ self._available = True
19
+ except ImportError:
20
+ self.client = None
21
+ self._available = False
22
+ logger.warning("HashiCorp Secret Provider initialized but 'hvac' missing. Install 'nl2sql-engine[hashicorp]'.")
23
+
24
+ def get_secret(self, key: str) -> Optional[str]:
25
+ if not self._available or not self.client:
26
+ raise ImportError("HashiCorp Secret Provider is not available.")
27
+
28
+ try:
29
+ # key is assumed to be path/to/secret:key or just path (if single value?)
30
+ # Let's assume standard kv v2: "secret/data/my-app:password"
31
+ # Format: "mount/path:key"
32
+
33
+ if ":" in key:
34
+ path, field = key.split(":", 1)
35
+ else:
36
+ path = key
37
+ field = "value" # Default field?
38
+
39
+ # Using KV V2
40
+ response = self.client.secrets.kv.v2.read_secret_version(path=path)
41
+ data = response['data']['data']
42
+ return data.get(field)
43
+
44
+ except Exception as e:
45
+ logger.error(f"Failed to fetch secret '{key}' from Vault: {e}")
46
+ return None
File without changes
File without changes
@@ -0,0 +1,84 @@
1
+ from nl2sql.services.callbacks.node_handlers import NodeHandler
2
+ from nl2sql.services.callbacks.token_handler import TokenHandler
3
+ from langchain_core.callbacks import BaseCallbackHandler
4
+ from nl2sql.services.callbacks.presenter import PresenterProtocol
5
+ from typing import Dict, Any
6
+ from langchain_core.outputs import LLMResult
7
+
8
+ class PipelineMonitorCallback(BaseCallbackHandler):
9
+ """Callback handler for monitoring pipeline execution and auditing events.
10
+
11
+ Integrates with OpenTelemetry for metrics and EventLogger for audit trails.
12
+ """
13
+
14
+ def __init__(self, presenter: PresenterProtocol):
15
+ from nl2sql.common.settings import settings
16
+ from nl2sql.common.metrics import configure_metrics
17
+
18
+ configure_metrics(
19
+ exporter_type=settings.observability_exporter,
20
+ otlp_endpoint=settings.otlp_endpoint
21
+ )
22
+
23
+ self.node_handler = NodeHandler(presenter)
24
+ self.tokens = TokenHandler(self.node_handler.node_metrics)
25
+
26
+ def on_chain_start(self, serialized: Dict[str, Any], inputs: Dict[str, Any], **kwargs: Any) -> Any:
27
+ """Called when a chain starts."""
28
+ node_name = kwargs.get("metadata", {}).get("langgraph_node")
29
+ run_id = str(kwargs.get("run_id"))
30
+ parent_run_id = kwargs.get("parent_run_id")
31
+ parent_run_id = str(parent_run_id) if parent_run_id else None
32
+ self.node_handler.on_chain_start(run_id, parent_run_id, node_name, inputs)
33
+
34
+ def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> Any:
35
+ """Called when a chain ends."""
36
+ run_id = str(kwargs.get("run_id"))
37
+ self.node_handler.on_chain_end(run_id)
38
+
39
+ def on_chain_error(self, error: BaseException, **kwargs: Any) -> Any:
40
+ """Called when a chain errors."""
41
+ run_id = str(kwargs.get("run_id"))
42
+ self.node_handler.on_chain_error(run_id, error)
43
+
44
+ def on_llm_end(self, response: LLMResult, **kwargs: Any) -> Any:
45
+ """Called when an LLM ends."""
46
+ from nl2sql.common.event_logger import event_logger
47
+ from nl2sql.common.logger import _trace_id_ctx, _tenant_id_ctx
48
+
49
+ tags = kwargs.get("tags", [])
50
+ agent_name = next((t for t in tags if not t.startswith("seq:") and not t.startswith("langsmith:")), "unknown")
51
+
52
+ text_output = ""
53
+ model_name = "unknown"
54
+ token_usage = {}
55
+
56
+ if response.generations:
57
+ gen = response.generations[0][0]
58
+ text_output = gen.text
59
+
60
+ if response.llm_output:
61
+ model_name = response.llm_output.get("model_name", "unknown")
62
+ token_usage = response.llm_output.get("token_usage", {})
63
+
64
+ audit_payload = {
65
+ "agent": agent_name,
66
+ "model": model_name,
67
+ "response_snippet": text_output[:1000],
68
+ "token_usage": token_usage
69
+ }
70
+
71
+ event_logger.log_event(
72
+ event_type="llm_interaction",
73
+ payload=audit_payload,
74
+ trace_id=_trace_id_ctx.get(),
75
+ tenant_id=_tenant_id_ctx.get()
76
+ )
77
+
78
+ self.tokens.on_llm_end(response, agent_name=agent_name)
79
+
80
+ def get_status_tree(self):
81
+ self.node_handler.print_tree()
82
+
83
+ def get_performance_tree(self):
84
+ return self.node_handler.get_performance_tree()
@@ -0,0 +1,7 @@
1
+ import contextvars
2
+ from typing import Optional
3
+
4
+ current_node_run_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
5
+ "current_node_run_id", default=None
6
+ )
7
+
@@ -0,0 +1,187 @@
1
+ import time
2
+ from typing import Dict, Any, List, Optional
3
+
4
+ from nl2sql.common.context import current_datasource_id
5
+ from nl2sql.common.metrics import LATENCY_LOG, node_duration_histogram
6
+ from nl2sql.services.callbacks.presenter import PresenterProtocol
7
+ from nl2sql.services.callbacks.node_context import current_node_run_id
8
+ from nl2sql.services.callbacks.node_metrics import NodeMetrics
9
+
10
+
11
+ class NodeHandler:
12
+ """Handles node execution lifecycle events and metrics recording."""
13
+
14
+ def __init__(self, presenter: PresenterProtocol):
15
+ self.presenter = presenter
16
+
17
+ self.run_start: Dict[str, float] = {}
18
+ self.run_to_node: Dict[str, str] = {}
19
+ self.run_parent: Dict[str, Optional[str]] = {}
20
+
21
+ self.primary_run_id: Dict[str, str] = {}
22
+
23
+ self.tree: Dict[str, List[str]] = {"root": []}
24
+ self.node_metrics: Dict[str, NodeMetrics] = {}
25
+
26
+ self.node_active_count: Dict[str, int] = {}
27
+ self.run_ctx_tokens: Dict[str, Any] = {}
28
+ self.ds_ctx_tokens: Dict[str, Any] = {}
29
+
30
+ def on_chain_start(
31
+ self,
32
+ run_id: str,
33
+ parent_run_id: Optional[str],
34
+ node_name: Optional[str],
35
+ inputs: Dict[str, Any],
36
+ ):
37
+ if not node_name:
38
+ return
39
+
40
+ now = time.perf_counter()
41
+
42
+ raw_node = node_name
43
+ display_node = raw_node
44
+
45
+ if raw_node == "execution_branch":
46
+ if isinstance(inputs, dict):
47
+ q = inputs.get("user_query")
48
+ if not q:
49
+ sub_query = inputs.get("sub_query")
50
+ if isinstance(sub_query, dict):
51
+ q = sub_query.get("intent")
52
+ elif hasattr(sub_query, "intent"):
53
+ q = sub_query.intent
54
+ if q:
55
+ display_node = f"execution_branch ({q})"
56
+
57
+ self.run_start[run_id] = now
58
+ self.run_to_node[run_id] = display_node
59
+ self.run_parent[run_id] = parent_run_id
60
+
61
+ if display_node not in self.primary_run_id:
62
+ self.primary_run_id[display_node] = run_id
63
+
64
+ parent_primary = "root"
65
+ if parent_run_id and parent_run_id in self.run_to_node:
66
+ parent_node = self.run_to_node[parent_run_id]
67
+ parent_primary = self.primary_run_id.get(parent_node, "root")
68
+
69
+ self.tree.setdefault(parent_primary, []).append(run_id)
70
+ self.tree.setdefault(run_id, [])
71
+
72
+ self.node_metrics[run_id] = NodeMetrics(
73
+ start_time=now,
74
+ end_time=now,
75
+ datasource_id=current_datasource_id.get(),
76
+ )
77
+
78
+ primary_id = self.primary_run_id[display_node]
79
+ metrics = self.node_metrics[primary_id]
80
+ metrics.start_time = min(metrics.start_time, now)
81
+
82
+ ds_id = None
83
+ if isinstance(inputs, dict):
84
+ ds_id = inputs.get("selected_datasource_id") or inputs.get("datasource_id")
85
+ if not ds_id:
86
+ sub_query = inputs.get("sub_query")
87
+ if isinstance(sub_query, dict):
88
+ ds_id = sub_query.get("datasource_id")
89
+ elif hasattr(sub_query, "datasource_id"):
90
+ ds_id = sub_query.datasource_id
91
+
92
+ if ds_id:
93
+ tok = current_datasource_id.set(ds_id)
94
+ self.ds_ctx_tokens[run_id] = tok
95
+
96
+ tok = current_node_run_id.set(primary_id)
97
+ self.run_ctx_tokens[run_id] = tok
98
+
99
+ self.node_active_count[display_node] = (
100
+ self.node_active_count.get(display_node, 0) + 1
101
+ )
102
+
103
+ if self.node_active_count[display_node] == 1:
104
+ self.presenter.update_interactive_status(
105
+ f"[bold blue]{display_node}[/bold blue] Working..."
106
+ )
107
+
108
+ def on_chain_end(self, run_id: str):
109
+ node = self.run_to_node.get(run_id)
110
+ start = self.run_start.get(run_id)
111
+
112
+ if not node or start is None:
113
+ return
114
+
115
+ end = time.perf_counter()
116
+ primary_id = self.primary_run_id[node]
117
+ metrics = self.node_metrics[primary_id]
118
+
119
+ metrics.end_time = max(metrics.end_time, end)
120
+ metrics.duration = metrics.end_time - metrics.start_time
121
+
122
+
123
+ LATENCY_LOG.append(
124
+ {
125
+ "node": node,
126
+ "duration": end - start,
127
+ "datasource_id": current_datasource_id.get(),
128
+ "run_id": run_id,
129
+ "parent_run_id": self.run_parent.get(run_id),
130
+ }
131
+ )
132
+
133
+ node_duration_histogram.record(
134
+ end - start,
135
+ attributes={
136
+ "node": node,
137
+ "datasource_id": str(current_datasource_id.get() or "none"),
138
+ }
139
+ )
140
+
141
+ self.node_active_count[node] -= 1
142
+ if self.node_active_count[node] == 0:
143
+ tok_str = f" | {metrics.total_tokens} tok" if metrics.total_tokens else ""
144
+ self.presenter.print_success(
145
+ f"{node} Completed ({metrics.duration:.2f}s{tok_str})"
146
+ )
147
+ self.presenter.update_interactive_status("Thinking...")
148
+
149
+ tok = self.run_ctx_tokens.pop(run_id, None)
150
+ if tok:
151
+ current_node_run_id.reset(tok)
152
+
153
+ ds_tok = self.ds_ctx_tokens.pop(run_id, None)
154
+ if ds_tok:
155
+ current_datasource_id.reset(ds_tok)
156
+
157
+ def on_chain_error(self, run_id: str, error: BaseException):
158
+ node = self.run_to_node.get(run_id)
159
+ start = self.run_start.get(run_id)
160
+ end = time.perf_counter()
161
+
162
+ if node:
163
+ primary_id = self.primary_run_id[node]
164
+ metrics = self.node_metrics[primary_id]
165
+ metrics.end_time = max(metrics.end_time, end)
166
+ metrics.duration = metrics.end_time - metrics.start_time
167
+ metrics.error = str(error)
168
+
169
+ self.presenter.print_error(
170
+ f"{node} Failed: {error}"
171
+ )
172
+ self.presenter.update_interactive_status("Error encountered...")
173
+
174
+ tok = self.run_ctx_tokens.pop(run_id, None)
175
+ if tok:
176
+ current_node_run_id.reset(tok)
177
+
178
+ ds_tok = self.ds_ctx_tokens.pop(run_id, None)
179
+ if ds_tok:
180
+ current_datasource_id.reset(ds_tok)
181
+
182
+ def get_performance_tree(self):
183
+ return (
184
+ self.tree,
185
+ self.node_metrics,
186
+ {rid: self.run_to_node[rid] for rid in self.node_metrics},
187
+ )
@@ -0,0 +1,14 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+
5
+ @dataclass
6
+ class NodeMetrics:
7
+ start_time: float = float("inf")
8
+ end_time: float = 0.0
9
+ duration: float = 0.0
10
+ total_tokens: int = 0
11
+ prompt_tokens: int = 0
12
+ completion_tokens: int = 0
13
+ error: Optional[str] = None
14
+ datasource_id: Optional[str] = None
@@ -0,0 +1,12 @@
1
+ from typing import Protocol
2
+
3
+
4
+ class PresenterProtocol(Protocol):
5
+ def update_interactive_status(self, message: str) -> None:
6
+ ...
7
+
8
+ def print_success(self, message: str) -> None:
9
+ ...
10
+
11
+ def print_error(self, message: str) -> None:
12
+ ...
@@ -0,0 +1,56 @@
1
+ from langchain_core.outputs import LLMResult
2
+ from nl2sql.common.metrics import TOKEN_LOG, token_usage_counter
3
+ from nl2sql.common.context import current_datasource_id
4
+ from nl2sql.services.callbacks.node_context import current_node_run_id
5
+ from nl2sql.services.callbacks.node_metrics import NodeMetrics
6
+
7
+
8
+ class TokenHandler:
9
+ """Handles token usage tracking and metrics."""
10
+
11
+ def __init__(self, node_metrics: dict[str, NodeMetrics]):
12
+ self.node_metrics = node_metrics
13
+
14
+ def on_llm_end(self, response: LLMResult, agent_name: str = "unknown", model_name: str = "unknown"):
15
+ """Records token usage from LLM response."""
16
+ usage = None
17
+ if response and response.llm_output:
18
+ usage = response.llm_output.get("token_usage") or response.llm_output.get("usage")
19
+
20
+ if not usage:
21
+ return
22
+
23
+ p = usage.get("prompt_tokens") or usage.get("input_tokens", 0)
24
+ c = usage.get("completion_tokens") or usage.get("output_tokens", 0)
25
+ t = usage.get("total_tokens") or (p + c)
26
+
27
+ run_id = current_node_run_id.get()
28
+
29
+
30
+ TOKEN_LOG.append(
31
+ {
32
+ "agent": agent_name,
33
+ "model": model_name,
34
+ "datasource_id": current_datasource_id.get(),
35
+ "prompt_tokens": p,
36
+ "completion_tokens": c,
37
+ "total_tokens": t,
38
+ "run_id": run_id,
39
+ }
40
+ )
41
+
42
+ token_usage_counter.add(
43
+ t,
44
+ attributes={
45
+ "agent": agent_name,
46
+ "model": model_name,
47
+ "datasource_id": str(current_datasource_id.get() or "none"),
48
+ "type": "total"
49
+ }
50
+ )
51
+
52
+ if run_id and run_id in self.node_metrics:
53
+ m = self.node_metrics[run_id]
54
+ m.prompt_tokens += int(p)
55
+ m.completion_tokens += int(c)
56
+ m.total_tokens += int(t)