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,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Callable, Optional, Union, Dict, Any, TYPE_CHECKING
|
|
5
|
+
from langchain_core.runnables import Runnable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from nl2sql.pipeline.state import SubgraphExecutionState
|
|
9
|
+
from .prompts import REFINER_PROMPT
|
|
10
|
+
from nl2sql.common.errors import PipelineError, ErrorSeverity, ErrorCode
|
|
11
|
+
from nl2sql.pipeline.nodes.refiner.schemas import RefinerResponse
|
|
12
|
+
|
|
13
|
+
from nl2sql.common.logger import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger("refiner")
|
|
16
|
+
|
|
17
|
+
LLMCallable = Union[Callable[[str], str], Runnable]
|
|
18
|
+
|
|
19
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
20
|
+
|
|
21
|
+
from langchain_core.output_parsers import StrOutputParser
|
|
22
|
+
from nl2sql.context import NL2SQLContext
|
|
23
|
+
|
|
24
|
+
class RefinerNode:
|
|
25
|
+
"""
|
|
26
|
+
Analyzes validation errors and generates constructive feedback for the Planner.
|
|
27
|
+
|
|
28
|
+
Uses an LLM to look at the failed plan, the schema, and the errors to suggest fixes.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, ctx: NL2SQLContext):
|
|
32
|
+
"""
|
|
33
|
+
Initializes the RefinerNode.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
llm: The language model to use for refinement.
|
|
37
|
+
"""
|
|
38
|
+
self.node_name = self.__class__.__name__.lower().replace('node', '')
|
|
39
|
+
self.llm = ctx.llm_registry.get_llm(self.node_name)
|
|
40
|
+
self.prompt = ChatPromptTemplate.from_template(REFINER_PROMPT)
|
|
41
|
+
self.chain = None
|
|
42
|
+
if self.llm is not None:
|
|
43
|
+
self.chain = self.prompt | self.llm | StrOutputParser()
|
|
44
|
+
|
|
45
|
+
def __call__(self, state: SubgraphExecutionState) -> Dict[str, Any]:
|
|
46
|
+
"""
|
|
47
|
+
Executes the summarization step.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
state: The current graph state.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Dictionary updates for the graph state with refined error messages (feedback).
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
if not self.chain:
|
|
57
|
+
error = PipelineError(
|
|
58
|
+
node=self.node_name,
|
|
59
|
+
message="No LLM configured for refiner.",
|
|
60
|
+
severity=ErrorSeverity.ERROR,
|
|
61
|
+
error_code=ErrorCode.MISSING_LLM,
|
|
62
|
+
)
|
|
63
|
+
return {
|
|
64
|
+
"refiner_response": RefinerResponse(errors=[error]),
|
|
65
|
+
"errors": [error],
|
|
66
|
+
}
|
|
67
|
+
relevant_tables = ""
|
|
68
|
+
if state.relevant_tables:
|
|
69
|
+
lines = []
|
|
70
|
+
for tbl in state.relevant_tables:
|
|
71
|
+
lines.append(tbl.model_dump_json(indent=2))
|
|
72
|
+
lines.append("---")
|
|
73
|
+
relevant_tables = "\n".join(lines)
|
|
74
|
+
|
|
75
|
+
failed_plan_str = "No plan generated."
|
|
76
|
+
if state.ast_planner_response and state.ast_planner_response.plan:
|
|
77
|
+
try:
|
|
78
|
+
failed_plan_str = json.dumps(state.ast_planner_response.plan, indent=2)
|
|
79
|
+
except:
|
|
80
|
+
failed_plan_str = str(state.ast_planner_response.plan)
|
|
81
|
+
|
|
82
|
+
# Extract messages from PipelineError objects
|
|
83
|
+
errors_str = "\n".join(f"- {e.message}" for e in state.errors)
|
|
84
|
+
|
|
85
|
+
reasoning_str = "No reasoning history."
|
|
86
|
+
if state.reasoning:
|
|
87
|
+
reasoning_str = "\n".join(
|
|
88
|
+
f"[{r.get('node', 'unknown')}]: {r.get('content')}" for r in state.reasoning
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
feedback = self.chain.invoke({
|
|
93
|
+
"user_query": state.sub_query.intent if state.sub_query else "",
|
|
94
|
+
"relevant_tables": relevant_tables,
|
|
95
|
+
"failed_plan": failed_plan_str,
|
|
96
|
+
"errors": errors_str,
|
|
97
|
+
"reasoning": reasoning_str
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
warning = PipelineError(
|
|
101
|
+
node=self.node_name,
|
|
102
|
+
message=feedback,
|
|
103
|
+
severity=ErrorSeverity.WARNING, # Feedback for retry
|
|
104
|
+
error_code=ErrorCode.PLAN_FEEDBACK,
|
|
105
|
+
)
|
|
106
|
+
response = RefinerResponse(
|
|
107
|
+
feedback=feedback,
|
|
108
|
+
errors=[warning],
|
|
109
|
+
reasoning=[{"node": self.node_name, "content": feedback}],
|
|
110
|
+
)
|
|
111
|
+
return {
|
|
112
|
+
"refiner_response": response,
|
|
113
|
+
"errors": [warning],
|
|
114
|
+
"reasoning": response.reasoning,
|
|
115
|
+
}
|
|
116
|
+
except Exception as e:
|
|
117
|
+
raise e
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.error(f"Node {self.node_name} failed: {e}")
|
|
121
|
+
error = PipelineError(
|
|
122
|
+
node=self.node_name,
|
|
123
|
+
message=f"Refiner failed: {e}",
|
|
124
|
+
severity=ErrorSeverity.ERROR,
|
|
125
|
+
error_code=ErrorCode.REFINER_FAILED,
|
|
126
|
+
stack_trace=str(e),
|
|
127
|
+
)
|
|
128
|
+
return {
|
|
129
|
+
"refiner_response": RefinerResponse(errors=[error]),
|
|
130
|
+
"reasoning": [{"node": self.node_name, "content": f"Refiner failed: {e}", "type": "error"}],
|
|
131
|
+
"errors": [error],
|
|
132
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
REFINER_PROMPT = """You are an expert SQL debugger and schema analyst.
|
|
2
|
+
Your goal is to analyze a failed SQL generation attempt and provide actionable, schema-aware feedback to the Planner.
|
|
3
|
+
|
|
4
|
+
### Context
|
|
5
|
+
User Query: "{user_query}"
|
|
6
|
+
|
|
7
|
+
### Database Schema
|
|
8
|
+
{relevant_tables}
|
|
9
|
+
|
|
10
|
+
### Failed Plan
|
|
11
|
+
{failed_plan}
|
|
12
|
+
|
|
13
|
+
### Errors (Validation or Execution)
|
|
14
|
+
{errors}
|
|
15
|
+
|
|
16
|
+
### Previous Reasoning (Trace)
|
|
17
|
+
{reasoning}
|
|
18
|
+
|
|
19
|
+
### Instructions
|
|
20
|
+
1. Analyze the Errors in the context of the Schema and User Query.
|
|
21
|
+
2. If the error is an Execution Error (e.g., syntax error, runtime error), analyze why the SQL failed and suggest a fix in the Plan.
|
|
22
|
+
2. If the error is about a missing table or column, check the Schema for the correct name.
|
|
23
|
+
- Example: Error "Column 'revenue' not found", Schema has "total_revenue". -> Suggest "Use 'total_revenue' instead of 'revenue'".
|
|
24
|
+
3. If the plan is empty or missing, analyze the User Query and suggest which tables/columns to use.
|
|
25
|
+
4. Provide a concise, numbered list of specific fixes. Do not generate SQL. Focus on correcting the Plan.
|
|
26
|
+
|
|
27
|
+
### Feedback
|
|
28
|
+
"""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from nl2sql.common.errors import PipelineError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RefinerResponse(BaseModel):
|
|
11
|
+
feedback: Optional[str] = None
|
|
12
|
+
errors: List[PipelineError] = Field(default_factory=list)
|
|
13
|
+
reasoning: List[Dict[str, Any]] = Field(default_factory=list)
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import Dict, Any, List, Optional, TYPE_CHECKING, Set
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from nl2sql.pipeline.state import SubgraphExecutionState
|
|
8
|
+
from nl2sql.pipeline.nodes.decomposer.schemas import SubQuery
|
|
9
|
+
|
|
10
|
+
from nl2sql.common.logger import get_logger
|
|
11
|
+
from nl2sql.context import NL2SQLContext
|
|
12
|
+
from .schema import Table, Column
|
|
13
|
+
from nl2sql_adapter_sdk.schema import SchemaSnapshot
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = get_logger("schema_retriever")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SchemaRetrieverNode:
|
|
20
|
+
"""Retrieves relevant schema chunks for planning context."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, ctx: NL2SQLContext):
|
|
23
|
+
self.node_name = self.__class__.__name__.lower().replace("node", "")
|
|
24
|
+
self.vector_store = ctx.vector_store
|
|
25
|
+
self.schema_store = ctx.schema_store
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _build_semantic_query(self, sub_query: SubQuery) -> str:
|
|
29
|
+
parts: List[str] = []
|
|
30
|
+
if sub_query and sub_query.intent:
|
|
31
|
+
parts.append(sub_query.intent)
|
|
32
|
+
|
|
33
|
+
if sub_query and sub_query.filters:
|
|
34
|
+
filters_text = []
|
|
35
|
+
for f in sub_query.filters:
|
|
36
|
+
value = f.value
|
|
37
|
+
if isinstance(value, list):
|
|
38
|
+
value = ", ".join(str(v) for v in value)
|
|
39
|
+
filters_text.append(f"{f.attribute}={value}")
|
|
40
|
+
if filters_text:
|
|
41
|
+
parts.append("filters: " + "; ".join(filters_text))
|
|
42
|
+
|
|
43
|
+
if sub_query and sub_query.group_by:
|
|
44
|
+
group_by = ", ".join(g.attribute for g in sub_query.group_by)
|
|
45
|
+
if group_by:
|
|
46
|
+
parts.append("group_by: " + group_by)
|
|
47
|
+
|
|
48
|
+
if sub_query and sub_query.expected_schema:
|
|
49
|
+
expected = ", ".join(c.name for c in sub_query.expected_schema)
|
|
50
|
+
if expected:
|
|
51
|
+
parts.append("expected_schema: " + expected)
|
|
52
|
+
|
|
53
|
+
if sub_query and sub_query.metrics:
|
|
54
|
+
metrics = ", ".join(m.name for m in sub_query.metrics)
|
|
55
|
+
if metrics:
|
|
56
|
+
parts.append("metrics: " + metrics)
|
|
57
|
+
|
|
58
|
+
return "\n".join(parts).strip()
|
|
59
|
+
|
|
60
|
+
def _resolve_snapshot(self, datasource_id: str, schema_version: Optional[str]) -> Optional[SchemaSnapshot]:
|
|
61
|
+
if not self.schema_store:
|
|
62
|
+
return None
|
|
63
|
+
if schema_version:
|
|
64
|
+
return self.schema_store.get_snapshot(datasource_id, schema_version)
|
|
65
|
+
return self.schema_store.get_latest_snapshot(datasource_id)
|
|
66
|
+
|
|
67
|
+
def _build_tables_from_snapshot(
|
|
68
|
+
self,
|
|
69
|
+
snapshot: SchemaSnapshot,
|
|
70
|
+
resolved_tables: Optional[Dict[str, Set[str]]] = None,
|
|
71
|
+
schema_version: Optional[str] = None,
|
|
72
|
+
) -> List[Table]:
|
|
73
|
+
if not snapshot:
|
|
74
|
+
return []
|
|
75
|
+
|
|
76
|
+
tables_out: List[Table] = []
|
|
77
|
+
table_keys = (
|
|
78
|
+
list(snapshot.contract.tables.keys())
|
|
79
|
+
if not resolved_tables
|
|
80
|
+
else list(resolved_tables.keys())
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
for table_key in table_keys:
|
|
84
|
+
table_contract = snapshot.contract.tables.get(table_key)
|
|
85
|
+
table_metadata = snapshot.metadata.tables.get(table_key)
|
|
86
|
+
if not table_contract:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
resolved_columns = resolved_tables[table_key] if resolved_tables else set()
|
|
90
|
+
if not resolved_columns:
|
|
91
|
+
resolved_columns = set(table_contract.columns.keys())
|
|
92
|
+
|
|
93
|
+
columns: List[Column] = []
|
|
94
|
+
for col_key, col_contract in table_contract.columns.items():
|
|
95
|
+
if col_key not in resolved_columns:
|
|
96
|
+
continue
|
|
97
|
+
col_metadata = table_metadata.columns.get(col_key) if table_metadata else None
|
|
98
|
+
|
|
99
|
+
columns.append(
|
|
100
|
+
Column(
|
|
101
|
+
name=col_key,
|
|
102
|
+
type=col_contract.data_type,
|
|
103
|
+
stats=col_metadata.statistics.model_dump() if col_metadata and col_metadata.statistics else {},
|
|
104
|
+
description=col_metadata.description if col_metadata else ""
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
relationships = []
|
|
109
|
+
for fk in table_contract.foreign_keys:
|
|
110
|
+
relationships.append(
|
|
111
|
+
{
|
|
112
|
+
"from_table": table_contract.table.full_name,
|
|
113
|
+
"to_table": fk.referred_table.full_name,
|
|
114
|
+
"from_columns": fk.constrained_columns,
|
|
115
|
+
"to_columns": fk.referred_columns,
|
|
116
|
+
"cardinality": fk.cardinality,
|
|
117
|
+
"business_meaning": fk.business_meaning,
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
primary_keys = [
|
|
122
|
+
col.name for col in table_contract.columns.values() if col.is_primary_key
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
table = Table(
|
|
126
|
+
name=table_contract.table.table_name,
|
|
127
|
+
columns=columns,
|
|
128
|
+
description=table_metadata.description if table_metadata else "",
|
|
129
|
+
primary_key=primary_keys,
|
|
130
|
+
schema_version=schema_version,
|
|
131
|
+
relationships=relationships,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
tables_out.append( table )
|
|
135
|
+
|
|
136
|
+
return tables_out
|
|
137
|
+
|
|
138
|
+
def __call__(self, state: SubgraphExecutionState) -> Dict[str, Any]:
|
|
139
|
+
try:
|
|
140
|
+
sub_query = state.sub_query
|
|
141
|
+
if not sub_query:
|
|
142
|
+
return {"relevant_tables": []}
|
|
143
|
+
|
|
144
|
+
datasource_id = sub_query.datasource_id
|
|
145
|
+
schema_version = sub_query.schema_version
|
|
146
|
+
query = self._build_semantic_query(sub_query)
|
|
147
|
+
|
|
148
|
+
tables: Dict[str, Set[str]] = defaultdict(set)
|
|
149
|
+
schema_docs = []
|
|
150
|
+
column_docs = []
|
|
151
|
+
|
|
152
|
+
if self.vector_store:
|
|
153
|
+
schema_docs = self.vector_store.retrieve_schema_context(
|
|
154
|
+
query, datasource_id, k=8
|
|
155
|
+
)
|
|
156
|
+
if schema_docs:
|
|
157
|
+
for doc in schema_docs:
|
|
158
|
+
table = doc.metadata.get("table")
|
|
159
|
+
if table:
|
|
160
|
+
tables[table].update([])
|
|
161
|
+
else:
|
|
162
|
+
column_docs = self.vector_store.retrieve_column_candidates(
|
|
163
|
+
query, datasource_id, k=8
|
|
164
|
+
)
|
|
165
|
+
for doc in column_docs:
|
|
166
|
+
table = doc.metadata.get("table")
|
|
167
|
+
column = doc.metadata.get("column")
|
|
168
|
+
if not table:
|
|
169
|
+
continue
|
|
170
|
+
tables[table].update([])
|
|
171
|
+
if column:
|
|
172
|
+
tables[table].add(column)
|
|
173
|
+
|
|
174
|
+
planning_docs = []
|
|
175
|
+
if self.vector_store and tables:
|
|
176
|
+
planning_docs = self.vector_store.retrieve_planning_context(
|
|
177
|
+
query, datasource_id, list(tables.keys()), k=12
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
for doc in planning_docs:
|
|
181
|
+
doc_type = doc.metadata.get("type")
|
|
182
|
+
if doc_type == "schema.column":
|
|
183
|
+
table = doc.metadata.get("table")
|
|
184
|
+
column = doc.metadata.get("column")
|
|
185
|
+
if table and column:
|
|
186
|
+
tables[table].add(column)
|
|
187
|
+
|
|
188
|
+
if doc_type == "schema.relationship":
|
|
189
|
+
from_table = doc.metadata.get("from_table")
|
|
190
|
+
to_table = doc.metadata.get("to_table")
|
|
191
|
+
if from_table:
|
|
192
|
+
tables[from_table].update(doc.metadata.get("from_columns"))
|
|
193
|
+
if to_table:
|
|
194
|
+
tables[to_table].update(doc.metadata.get("to_columns"))
|
|
195
|
+
|
|
196
|
+
if not tables:
|
|
197
|
+
snapshot = self._resolve_snapshot(datasource_id, schema_version)
|
|
198
|
+
relevant_tables = self._build_tables_from_snapshot(
|
|
199
|
+
snapshot,
|
|
200
|
+
resolved_tables=None,
|
|
201
|
+
schema_version=schema_version,
|
|
202
|
+
)
|
|
203
|
+
return {
|
|
204
|
+
"relevant_tables": relevant_tables,
|
|
205
|
+
"reasoning": [
|
|
206
|
+
{
|
|
207
|
+
"node": self.node_name,
|
|
208
|
+
"content": "Vector retrieval produced no candidates. Using full schema snapshot.",
|
|
209
|
+
"type": "warning",
|
|
210
|
+
}
|
|
211
|
+
],
|
|
212
|
+
"warnings": [
|
|
213
|
+
{
|
|
214
|
+
"node": self.node_name,
|
|
215
|
+
"content": "Vector retrieval produced no candidates. Using full schema snapshot.",
|
|
216
|
+
}
|
|
217
|
+
],
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
snapshot = self._resolve_snapshot(datasource_id, schema_version)
|
|
221
|
+
relevant_tables = self._build_tables_from_snapshot(
|
|
222
|
+
snapshot,
|
|
223
|
+
resolved_tables=tables,
|
|
224
|
+
schema_version=schema_version,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
"relevant_tables": relevant_tables,
|
|
231
|
+
"reasoning": [
|
|
232
|
+
{
|
|
233
|
+
"node": self.node_name,
|
|
234
|
+
"content": (
|
|
235
|
+
f"Retrieved {len(relevant_tables)} tables "
|
|
236
|
+
f"with {sum(len(t.columns) for t in relevant_tables)} columns."
|
|
237
|
+
),
|
|
238
|
+
}
|
|
239
|
+
],
|
|
240
|
+
}
|
|
241
|
+
except Exception as exc:
|
|
242
|
+
logger.error(f"Schema retrieval failed: {exc}")
|
|
243
|
+
return {
|
|
244
|
+
"relevant_tables": [],
|
|
245
|
+
"reasoning": [
|
|
246
|
+
{
|
|
247
|
+
"node": self.node_name,
|
|
248
|
+
"content": f"Schema retrieval failed: {exc}",
|
|
249
|
+
"type": "error",
|
|
250
|
+
}
|
|
251
|
+
],
|
|
252
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Column(BaseModel):
|
|
8
|
+
"""Lightweight column schema for routing/planning."""
|
|
9
|
+
|
|
10
|
+
name: str
|
|
11
|
+
type: Optional[str] = None
|
|
12
|
+
stats: Optional[Dict[str, Any]] = None
|
|
13
|
+
description: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(extra="allow")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Table(BaseModel):
|
|
19
|
+
"""Lightweight table schema for routing/planning."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
columns: List[Column] = Field(default_factory=list)
|
|
23
|
+
description: Optional[str] = None
|
|
24
|
+
primary_key: Optional[List[str]] = None
|
|
25
|
+
foreign_keys: Optional[Dict[str, List[str]]] = None
|
|
26
|
+
|
|
27
|
+
model_config = ConfigDict(extra="allow")
|