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,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,3 @@
1
+ from .node import SchemaRetrieverNode
2
+
3
+ __all__ = ["SchemaRetrieverNode"]
@@ -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")
@@ -0,0 +1,7 @@
1
+ from .node import LogicalValidatorNode
2
+ from .schemas import LogicalValidatorResponse
3
+
4
+ __all__ = [
5
+ "LogicalValidatorNode",
6
+ "LogicalValidatorResponse",
7
+ ]