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.
- nl2sql/__init__.py +38 -0
- nl2sql/adapters/__init__.py +0 -0
- nl2sql/adapters/duckdb/__init__.py +0 -0
- nl2sql/adapters/duckdb/adapter.py +71 -0
- nl2sql/adapters/mssql/__init__.py +0 -0
- nl2sql/adapters/mssql/adapter.py +122 -0
- nl2sql/adapters/mysql/__init__.py +0 -0
- nl2sql/adapters/mysql/adapter.py +123 -0
- nl2sql/adapters/postgres/__init__.py +0 -0
- nl2sql/adapters/postgres/adapter.py +115 -0
- nl2sql/adapters/sqlalchemy_base/__init__.py +17 -0
- nl2sql/adapters/sqlalchemy_base/adapter.py +476 -0
- nl2sql/adapters/sqlalchemy_base/models.py +36 -0
- nl2sql/adapters/sqlite/__init__.py +0 -0
- nl2sql/adapters/sqlite/adapter.py +88 -0
- nl2sql/aggregation/__init__.py +3 -0
- nl2sql/aggregation/aggregator.py +98 -0
- nl2sql/aggregation/engines/__init__.py +3 -0
- nl2sql/aggregation/engines/polars_duckdb.py +125 -0
- nl2sql/api/__init__.py +0 -0
- nl2sql/api/auth_api.py +60 -0
- nl2sql/api/benchmark_api.py +114 -0
- nl2sql/api/datasource_api.py +132 -0
- nl2sql/api/indexing_api.py +59 -0
- nl2sql/api/llm_api.py +82 -0
- nl2sql/api/policy_api.py +135 -0
- nl2sql/api/query_api.py +138 -0
- nl2sql/api/result_api.py +24 -0
- nl2sql/api/settings_api.py +65 -0
- nl2sql/auth/__init__.py +8 -0
- nl2sql/auth/models.py +36 -0
- nl2sql/auth/rbac.py +25 -0
- nl2sql/cli/__init__.py +0 -0
- nl2sql/cli/checks.py +53 -0
- nl2sql/cli/commands/__init__.py +0 -0
- nl2sql/cli/commands/benchmark.py +34 -0
- nl2sql/cli/commands/doctor.py +49 -0
- nl2sql/cli/commands/indexing.py +126 -0
- nl2sql/cli/commands/info.py +25 -0
- nl2sql/cli/commands/install.py +27 -0
- nl2sql/cli/commands/policy.py +57 -0
- nl2sql/cli/commands/run.py +166 -0
- nl2sql/cli/commands/setup.py +415 -0
- nl2sql/cli/commands/visualize.py +34 -0
- nl2sql/cli/common/decorators.py +34 -0
- nl2sql/cli/config.py +24 -0
- nl2sql/cli/console.py +52 -0
- nl2sql/cli/demo/__init__.py +1 -0
- nl2sql/cli/demo/data.py +87 -0
- nl2sql/cli/demo/defaults.py +122 -0
- nl2sql/cli/demo/factory.py +289 -0
- nl2sql/cli/demo/manager.py +230 -0
- nl2sql/cli/demo/schemas.py +336 -0
- nl2sql/cli/demo/writers/__init__.py +0 -0
- nl2sql/cli/demo/writers/docker.py +182 -0
- nl2sql/cli/demo/writers/sqlite.py +88 -0
- nl2sql/cli/generators/datasources/__init__.py +3 -0
- nl2sql/cli/generators/datasources/generator.py +24 -0
- nl2sql/cli/generators/datasources/templates.py +7 -0
- nl2sql/cli/generators/env/__init__.py +3 -0
- nl2sql/cli/generators/env/generator.py +46 -0
- nl2sql/cli/generators/env/templates.py +25 -0
- nl2sql/cli/generators/llm/__init__.py +3 -0
- nl2sql/cli/generators/llm/generator.py +24 -0
- nl2sql/cli/generators/llm/templates.py +4 -0
- nl2sql/cli/generators/policies/__init__.py +3 -0
- nl2sql/cli/generators/policies/generator.py +20 -0
- nl2sql/cli/generators/policies/templates.py +2 -0
- nl2sql/cli/main.py +195 -0
- nl2sql/cli/reporting.py +878 -0
- nl2sql/cli/types.py +13 -0
- nl2sql/common/__init__.py +1 -0
- nl2sql/common/cancellation.py +25 -0
- nl2sql/common/context.py +5 -0
- nl2sql/common/errors.py +109 -0
- nl2sql/common/event_logger.py +88 -0
- nl2sql/common/exceptions.py +3 -0
- nl2sql/common/logger.py +119 -0
- nl2sql/common/metrics.py +50 -0
- nl2sql/common/resilience.py +59 -0
- nl2sql/common/settings.py +195 -0
- nl2sql/configs/__init__.py +6 -0
- nl2sql/configs/datasources.py +10 -0
- nl2sql/configs/llm.py +36 -0
- nl2sql/configs/manager.py +176 -0
- nl2sql/configs/policies.py +14 -0
- nl2sql/configs/sample_questions.py +11 -0
- nl2sql/configs/secrets.py +11 -0
- nl2sql/context.py +106 -0
- nl2sql/datasources/__init__.py +21 -0
- nl2sql/datasources/discovery.py +28 -0
- nl2sql/datasources/models.py +21 -0
- nl2sql/datasources/protocols.py +3 -0
- nl2sql/datasources/registry.py +172 -0
- nl2sql/evaluation/__init__.py +6 -0
- nl2sql/evaluation/benchmark_runner.py +320 -0
- nl2sql/evaluation/evaluator.py +134 -0
- nl2sql/evaluation/types.py +22 -0
- nl2sql/execution/__init__.py +4 -0
- nl2sql/execution/artifacts/__init__.py +3 -0
- nl2sql/execution/artifacts/parquet.py +41 -0
- nl2sql/execution/artifacts/store.py +165 -0
- nl2sql/execution/contracts.py +57 -0
- nl2sql/execution/execution_store.py +25 -0
- nl2sql/execution/executor/__init__.py +3 -0
- nl2sql/execution/executor/sql_executor.py +116 -0
- nl2sql/indexing/__init__.py +7 -0
- nl2sql/indexing/chunk_builder.py +227 -0
- nl2sql/indexing/embeddings.py +180 -0
- nl2sql/indexing/enrichment_service.py +316 -0
- nl2sql/indexing/models.py +209 -0
- nl2sql/indexing/orchestrator.py +90 -0
- nl2sql/indexing/vector_store.py +422 -0
- nl2sql/llm/__init__.py +8 -0
- nl2sql/llm/models.py +10 -0
- nl2sql/llm/registry.py +214 -0
- nl2sql/pipeline/__init__.py +1 -0
- nl2sql/pipeline/graph.py +73 -0
- nl2sql/pipeline/graph_utils.py +141 -0
- nl2sql/pipeline/nodes/__init__.py +25 -0
- nl2sql/pipeline/nodes/aggregator/__init__.py +4 -0
- nl2sql/pipeline/nodes/aggregator/node.py +55 -0
- nl2sql/pipeline/nodes/aggregator/prompts.py +20 -0
- nl2sql/pipeline/nodes/aggregator/schemas.py +28 -0
- nl2sql/pipeline/nodes/answer_synthesizer/__init__.py +4 -0
- nl2sql/pipeline/nodes/answer_synthesizer/node.py +98 -0
- nl2sql/pipeline/nodes/answer_synthesizer/prompts.py +19 -0
- nl2sql/pipeline/nodes/answer_synthesizer/schemas.py +24 -0
- nl2sql/pipeline/nodes/ast_planner/__init__.py +4 -0
- nl2sql/pipeline/nodes/ast_planner/node.py +104 -0
- nl2sql/pipeline/nodes/ast_planner/prompts.py +138 -0
- nl2sql/pipeline/nodes/ast_planner/schemas.py +236 -0
- nl2sql/pipeline/nodes/datasource_resolver/__init__.py +4 -0
- nl2sql/pipeline/nodes/datasource_resolver/node.py +253 -0
- nl2sql/pipeline/nodes/datasource_resolver/schemas.py +21 -0
- nl2sql/pipeline/nodes/decomposer/__init__.py +3 -0
- nl2sql/pipeline/nodes/decomposer/node.py +219 -0
- nl2sql/pipeline/nodes/decomposer/prompts.py +96 -0
- nl2sql/pipeline/nodes/decomposer/schemas.py +143 -0
- nl2sql/pipeline/nodes/executor/__init__.py +3 -0
- nl2sql/pipeline/nodes/executor/node.py +107 -0
- nl2sql/pipeline/nodes/generator/__init__.py +4 -0
- nl2sql/pipeline/nodes/generator/node.py +267 -0
- nl2sql/pipeline/nodes/generator/schemas.py +13 -0
- nl2sql/pipeline/nodes/global_planner/__init__.py +4 -0
- nl2sql/pipeline/nodes/global_planner/node.py +186 -0
- nl2sql/pipeline/nodes/global_planner/schemas.py +101 -0
- nl2sql/pipeline/nodes/refiner/__init__.py +4 -0
- nl2sql/pipeline/nodes/refiner/node.py +132 -0
- nl2sql/pipeline/nodes/refiner/prompts.py +28 -0
- nl2sql/pipeline/nodes/refiner/schemas.py +13 -0
- nl2sql/pipeline/nodes/schema_retriever/__init__.py +3 -0
- nl2sql/pipeline/nodes/schema_retriever/node.py +252 -0
- nl2sql/pipeline/nodes/schema_retriever/schema.py +27 -0
- nl2sql/pipeline/nodes/validator/__init__.py +7 -0
- nl2sql/pipeline/nodes/validator/node.py +839 -0
- nl2sql/pipeline/nodes/validator/schemas.py +12 -0
- nl2sql/pipeline/pipeline_runner.py +72 -0
- nl2sql/pipeline/routes.py +72 -0
- nl2sql/pipeline/runtime.py +153 -0
- nl2sql/pipeline/state.py +92 -0
- nl2sql/pipeline/subgraphs/__init__.py +5 -0
- nl2sql/pipeline/subgraphs/schemas.py +23 -0
- nl2sql/pipeline/subgraphs/sql_agent.py +167 -0
- nl2sql/public_api.py +199 -0
- nl2sql/schema/__init__.py +37 -0
- nl2sql/schema/in_memory_store.py +173 -0
- nl2sql/schema/protocol.py +88 -0
- nl2sql/schema/sqlite_store.py +233 -0
- nl2sql/schema/store.py +29 -0
- nl2sql/secrets/__init__.py +14 -0
- nl2sql/secrets/factory.py +85 -0
- nl2sql/secrets/interfaces.py +16 -0
- nl2sql/secrets/manager.py +139 -0
- nl2sql/secrets/models.py +56 -0
- nl2sql/secrets/providers/aws.py +30 -0
- nl2sql/secrets/providers/azure.py +49 -0
- nl2sql/secrets/providers/env.py +8 -0
- nl2sql/secrets/providers/hashi.py +46 -0
- nl2sql/services/__init__.py +0 -0
- nl2sql/services/callbacks/__init__.py +0 -0
- nl2sql/services/callbacks/monitor.py +84 -0
- nl2sql/services/callbacks/node_context.py +7 -0
- nl2sql/services/callbacks/node_handlers.py +187 -0
- nl2sql/services/callbacks/node_metrics.py +14 -0
- nl2sql/services/callbacks/presenter.py +12 -0
- nl2sql/services/callbacks/token_handler.py +56 -0
- nl2sql_engine-0.1.0.dist-info/METADATA +295 -0
- nl2sql_engine-0.1.0.dist-info/RECORD +192 -0
- nl2sql_engine-0.1.0.dist-info/WHEEL +5 -0
- nl2sql_engine-0.1.0.dist-info/entry_points.txt +9 -0
- 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,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,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)
|