nlp2sql 0.2.0rc6__tar.gz → 0.2.0rc7__tar.gz

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 (114) hide show
  1. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/CLAUDE.md +4 -1
  2. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/PKG-INFO +28 -2
  3. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/README.md +27 -1
  4. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/docs/API.md +38 -0
  5. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/docs/ARCHITECTURE.md +4 -2
  6. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/pyproject.toml +1 -1
  7. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/__init__.py +11 -4
  8. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/adapters/anthropic_adapter.py +7 -9
  9. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/adapters/gemini_adapter.py +7 -9
  10. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/adapters/openai_adapter.py +7 -9
  11. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/adapters/postgres_repository.py +34 -28
  12. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/adapters/redshift_adapter.py +32 -26
  13. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/cli.py +2 -0
  14. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/config/settings.py +1 -1
  15. nlp2sql-0.2.0rc7/src/nlp2sql/core/database_prompts.py +48 -0
  16. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/schema/analyzer.py +42 -17
  17. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/schema/manager.py +4 -4
  18. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/services/query_service.py +112 -1
  19. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/tests/test_batch_scoring.py +20 -41
  20. nlp2sql-0.2.0rc7/tests/test_column_validation.py +180 -0
  21. nlp2sql-0.2.0rc7/tests/test_database_prompts.py +64 -0
  22. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/.claude/commands/db.md +0 -0
  23. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/.claude/commands/quality.md +0 -0
  24. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/.claude/commands/test.md +0 -0
  25. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/.claude/rules/adapters.md +0 -0
  26. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/.claude/rules/architecture.md +0 -0
  27. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/.claude/rules/python-style.md +0 -0
  28. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/.claude/rules/testing.md +0 -0
  29. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/.claude/settings.json +0 -0
  30. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  31. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  32. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/.github/workflows/publish-pypi.yml +0 -0
  33. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/.github/workflows/publish-testpypi.yml +0 -0
  34. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/.gitignore +0 -0
  35. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/.mcp.json.example +0 -0
  36. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/.python-version +0 -0
  37. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/CONTRIBUTING.md +0 -0
  38. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/LICENSE +0 -0
  39. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/SECURITY.md +0 -0
  40. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/assets/nlp2sql-logo.png +0 -0
  41. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/benchmarks/enterprise_benchmark_results.json +0 -0
  42. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/docker/.env.example +0 -0
  43. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/docker/README.md +0 -0
  44. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/docker/docker-compose.yml +0 -0
  45. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/docker/get-db-urls.sh +0 -0
  46. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/docker/init-schema.sql +0 -0
  47. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/docker/large-schema.sql +0 -0
  48. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/docker/localstack-init.sh +0 -0
  49. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/docker/test-redshift.sh +0 -0
  50. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/docs/CONFIGURATION.md +0 -0
  51. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/docs/ENTERPRISE.md +0 -0
  52. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/docs/Redshift.md +0 -0
  53. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/README.md +0 -0
  54. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/advanced/test_multiple_providers.py +0 -0
  55. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/advanced/test_simple_api.py +0 -0
  56. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/database_specific/test_odoo_integration.py +0 -0
  57. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/default_examples.json +0 -0
  58. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/documentation/real_world_example.py +0 -0
  59. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/getting_started/basic_usage.py +0 -0
  60. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/getting_started/simple_demo.py +0 -0
  61. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/getting_started/test_api_setup.py +0 -0
  62. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/schema_management/test_auto_schema.py +0 -0
  63. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/schema_management/test_schema_filters_comprehensive.py +0 -0
  64. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/strands-fastapi-agent/.dockerignore +0 -0
  65. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/strands-fastapi-agent/.env.example +0 -0
  66. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/strands-fastapi-agent/DOCKER_README.md +0 -0
  67. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/strands-fastapi-agent/DOCKER_SETUP.md +0 -0
  68. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/strands-fastapi-agent/DOCKER_TESTING_SUMMARY.md +0 -0
  69. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/strands-fastapi-agent/Dockerfile +0 -0
  70. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/strands-fastapi-agent/INDEX.md +0 -0
  71. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/strands-fastapi-agent/README.md +0 -0
  72. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/strands-fastapi-agent/docker/init-ecommerce-schema.sql +0 -0
  73. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/strands-fastapi-agent/docker-compose.yml +0 -0
  74. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/strands-fastapi-agent/main.py +0 -0
  75. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/strands-fastapi-agent/pyproject.toml +0 -0
  76. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/examples/strands-fastapi-agent/test-docker-endpoints.sh +0 -0
  77. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/install-dev.sh +0 -0
  78. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/mcp_server/README.md +0 -0
  79. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/mcp_server/__init__.py +0 -0
  80. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/mcp_server/example_config.json +0 -0
  81. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/mcp_server/pyproject.toml +0 -0
  82. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/mcp_server/server.py +0 -0
  83. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/adapters/__init__.py +0 -0
  84. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/adapters/local_embedding_adapter.py +0 -0
  85. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/adapters/openai_embedding_adapter.py +0 -0
  86. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/config/__init__.py +0 -0
  87. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/core/__init__.py +0 -0
  88. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/core/entities.py +0 -0
  89. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/exceptions/__init__.py +0 -0
  90. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/factories/__init__.py +0 -0
  91. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/factories/repository_factory.py +0 -0
  92. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/ports/__init__.py +0 -0
  93. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/ports/ai_provider.py +0 -0
  94. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/ports/cache.py +0 -0
  95. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/ports/embedding_provider.py +0 -0
  96. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/ports/query_optimizer.py +0 -0
  97. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/ports/schema_repository.py +0 -0
  98. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/ports/schema_strategy.py +0 -0
  99. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/schema/__init__.py +0 -0
  100. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/schema/embedding_manager.py +0 -0
  101. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/schema/example_loader.py +0 -0
  102. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/schema/example_store.py +0 -0
  103. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/services/__init__.py +0 -0
  104. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/src/nlp2sql/utils/__init__.py +0 -0
  105. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/test_tmp/.gitignore +0 -0
  106. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/tests/mcp/__init__.py +0 -0
  107. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/tests/mcp/test_sql_security.py +0 -0
  108. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/tests/test_basic.py +0 -0
  109. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/tests/test_embedding_manager.py +0 -0
  110. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/tests/test_integration.py +0 -0
  111. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/tests/test_redshift_integration.py +0 -0
  112. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/tests/test_repository_cache.py +0 -0
  113. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/tests/test_repository_factory.py +0 -0
  114. {nlp2sql-0.2.0rc6 → nlp2sql-0.2.0rc7}/uv.lock +0 -0
@@ -51,6 +51,8 @@ The codebase follows Clean Architecture (Hexagonal/Ports & Adapters):
51
51
  ```
52
52
  src/nlp2sql/
53
53
  ├── core/ # Business entities (pure Python, no external dependencies)
54
+ │ ├── entities.py # Query, SQLQuery, DatabaseType
55
+ │ └── database_prompts.py # SQL dialect hints for AI providers
54
56
  ├── ports/ # Interfaces/abstractions (contracts)
55
57
  │ ├── ai_provider.py # AIProviderPort - interface for AI providers
56
58
  │ ├── embedding_provider.py # EmbeddingProviderPort - interface for embeddings
@@ -69,7 +71,8 @@ src/nlp2sql/
69
71
  ├── schema/ # Schema management
70
72
  │ ├── schema_manager.py # Coordinates filtering and strategies
71
73
  │ ├── schema_analyzer.py # Scores schema relevance
72
- │ └── schema_embedding_manager.py # FAISS vector embeddings
74
+ │ ├── schema_embedding_manager.py # FAISS vector embeddings
75
+ │ └── example_store.py # ExampleStore - FAISS-indexed few-shot examples
73
76
  ├── config/ # Configuration (Pydantic Settings)
74
77
  ├── exceptions/ # Custom exceptions hierarchy
75
78
  └── cli.py # Click-based CLI
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nlp2sql
3
- Version: 0.2.0rc6
3
+ Version: 0.2.0rc7
4
4
  Summary: Enterprise-ready Natural Language to SQL converter with multi-provider support. Built for production scale (1000+ tables) with Clean Architecture.
5
5
  Project-URL: Homepage, https://github.com/luiscarbonel1991/nlp2sql
6
6
  Project-URL: Bug Tracker, https://github.com/luiscarbonel1991/nlp2sql/issues
@@ -180,7 +180,33 @@ async def main():
180
180
  result2 = await service.generate_sql("Show recent orders")
181
181
  ```
182
182
 
183
- ### 4. Large Database with Schema Filtering
183
+ ### 4. Few-Shot Examples (Improved Accuracy)
184
+
185
+ ```python
186
+ from nlp2sql import ExampleStore, create_embedding_provider, create_and_initialize_service
187
+
188
+ # Create example store with domain-specific examples
189
+ embedding_provider = create_embedding_provider("openai", api_key=os.getenv("OPENAI_API_KEY"))
190
+ example_store = ExampleStore(embedding_provider=embedding_provider)
191
+ await example_store.add_examples([
192
+ {
193
+ "question": "What was the total revenue last month?",
194
+ "sql": "SELECT SUM(gross_revenue) FROM sales WHERE date >= DATE_TRUNC('month', DATEADD(month, -1, CURRENT_DATE))",
195
+ "database_type": "redshift",
196
+ "metadata": {"category": "aggregation"}
197
+ }
198
+ ])
199
+
200
+ service = await create_and_initialize_service(
201
+ database_url="redshift://user:pass@host:5439/db",
202
+ ai_provider="openai",
203
+ api_key=os.getenv("OPENAI_API_KEY"),
204
+ example_store=example_store
205
+ )
206
+ result = await service.generate_sql("Show me total sales this quarter")
207
+ ```
208
+
209
+ ### 5. Large Database with Schema Filtering
184
210
 
185
211
  ```python
186
212
  from nlp2sql import create_and_initialize_service
@@ -101,7 +101,33 @@ async def main():
101
101
  result2 = await service.generate_sql("Show recent orders")
102
102
  ```
103
103
 
104
- ### 4. Large Database with Schema Filtering
104
+ ### 4. Few-Shot Examples (Improved Accuracy)
105
+
106
+ ```python
107
+ from nlp2sql import ExampleStore, create_embedding_provider, create_and_initialize_service
108
+
109
+ # Create example store with domain-specific examples
110
+ embedding_provider = create_embedding_provider("openai", api_key=os.getenv("OPENAI_API_KEY"))
111
+ example_store = ExampleStore(embedding_provider=embedding_provider)
112
+ await example_store.add_examples([
113
+ {
114
+ "question": "What was the total revenue last month?",
115
+ "sql": "SELECT SUM(gross_revenue) FROM sales WHERE date >= DATE_TRUNC('month', DATEADD(month, -1, CURRENT_DATE))",
116
+ "database_type": "redshift",
117
+ "metadata": {"category": "aggregation"}
118
+ }
119
+ ])
120
+
121
+ service = await create_and_initialize_service(
122
+ database_url="redshift://user:pass@host:5439/db",
123
+ ai_provider="openai",
124
+ api_key=os.getenv("OPENAI_API_KEY"),
125
+ example_store=example_store
126
+ )
127
+ result = await service.generate_sql("Show me total sales this quarter")
128
+ ```
129
+
130
+ ### 5. Large Database with Schema Filtering
105
131
 
106
132
  ```python
107
133
  from nlp2sql import create_and_initialize_service
@@ -80,6 +80,7 @@ print(result['validation']) # Validation results
80
80
  | `api_key` | `str` | Yes | Provider API key |
81
81
  | `embedding_provider_type` | `str` | No | Embedding provider: `local` (default), `openai` |
82
82
  | `schema_filters` | `dict` | No | Schema filtering options |
83
+ | `example_store` | `ExampleStore` | No | Pre-loaded few-shot examples for better SQL generation |
83
84
 
84
85
  **Returns:** `dict` with keys `sql`, `confidence`, `explanation`, `validation`
85
86
 
@@ -111,6 +112,7 @@ result2 = await service.generate_sql("Show recent orders")
111
112
  | `schema_filters` | `dict` | No | Schema filtering options |
112
113
  | `embedding_provider_type` | `str` | No | Embedding provider type (see note below) |
113
114
  | `database_type` | `DatabaseType` | No | Database type (auto-detected) |
115
+ | `example_store` | `ExampleStore` | No | Pre-loaded few-shot examples for better SQL generation |
114
116
 
115
117
  **Note on `embedding_provider_type`:** This controls how schema relevance filtering works. With `"local"` or `"openai"`, the system builds a FAISS vector index for semantic search (recommended for databases with 50+ tables). With `None` (default), the system attempts to load local embeddings silently and falls back to text-only matching if `sentence-transformers` is not installed. Text-only matching uses exact name, normalized singular/plural, and token overlap -- effective for small schemas but less accurate for large ones.
116
118
 
@@ -141,6 +143,42 @@ result = await service.generate_sql(
141
143
  )
142
144
  ```
143
145
 
146
+ ### Few-Shot Examples
147
+
148
+ Improve SQL accuracy by providing domain-specific examples via `ExampleStore`. Examples are indexed with FAISS for semantic retrieval -- the most relevant examples are automatically included in the LLM prompt.
149
+
150
+ ```python
151
+ from nlp2sql import ExampleStore, create_embedding_provider, create_and_initialize_service
152
+
153
+ # Create example store
154
+ embedding_provider = create_embedding_provider("openai", api_key="your-key")
155
+ example_store = ExampleStore(embedding_provider=embedding_provider)
156
+
157
+ # Add domain-specific examples
158
+ await example_store.add_examples([
159
+ {
160
+ "question": "What was the total revenue last month?",
161
+ "sql": "SELECT SUM(gross_revenue) FROM sales WHERE date >= DATE_TRUNC('month', DATEADD(month, -1, CURRENT_DATE))",
162
+ "database_type": "redshift",
163
+ "metadata": {"category": "aggregation"}
164
+ },
165
+ {
166
+ "question": "Show orders by country",
167
+ "sql": "SELECT country, COUNT(*) AS total_orders FROM orders GROUP BY country ORDER BY total_orders DESC",
168
+ "database_type": "redshift",
169
+ "metadata": {"category": "aggregation"}
170
+ }
171
+ ])
172
+
173
+ # Pass to service
174
+ service = await create_and_initialize_service(
175
+ database_url="redshift://user:pass@host:5439/db",
176
+ ai_provider="openai",
177
+ api_key="your-key",
178
+ example_store=example_store
179
+ )
180
+ ```
181
+
144
182
  ### Embedding Providers
145
183
 
146
184
  ```python
@@ -229,7 +229,8 @@ The system has multiple caching layers:
229
229
  ```
230
230
  src/nlp2sql/
231
231
  ├── core/ # Business entities (pure Python, no external dependencies)
232
- │ └── entities.py # Query, SQLQuery, DatabaseType
232
+ │ ├── entities.py # Query, SQLQuery, DatabaseType
233
+ │ └── database_prompts.py # SQL dialect hints for AI providers (centralized)
233
234
  ├── ports/ # Interfaces/abstractions
234
235
  │ ├── ai_provider.py # AIProviderPort, QueryContext, QueryResponse
235
236
  │ ├── schema_repository.py # SchemaRepositoryPort, TableInfo
@@ -250,7 +251,8 @@ src/nlp2sql/
250
251
  ├── schema/ # Schema management
251
252
  │ ├── manager.py # SchemaManager (coordinates strategies)
252
253
  │ ├── analyzer.py # SchemaAnalyzer (scoring, compression)
253
- │ └── embedding_manager.py # SchemaEmbeddingManager (FAISS + TF-IDF)
254
+ │ ├── embedding_manager.py # SchemaEmbeddingManager (FAISS + TF-IDF)
255
+ │ └── example_store.py # ExampleStore (FAISS-indexed few-shot examples)
254
256
  ├── config/ # Configuration
255
257
  │ └── settings.py # Pydantic Settings (env vars)
256
258
  ├── exceptions/ # Custom exception hierarchy
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nlp2sql"
3
- version = "0.2.0rc6"
3
+ version = "0.2.0rc7"
4
4
  description = "Enterprise-ready Natural Language to SQL converter with multi-provider support. Built for production scale (1000+ tables) with Clean Architecture."
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -11,9 +11,10 @@ from .exceptions import *
11
11
  from .factories import RepositoryFactory
12
12
  from .ports.embedding_provider import EmbeddingProviderPort
13
13
  from .ports.schema_repository import SchemaRepositoryPort
14
+ from .schema.example_store import ExampleStore
14
15
  from .services.query_service import QueryGenerationService
15
16
 
16
- __version__ = "0.2.0rc6"
17
+ __version__ = "0.2.0rc7"
17
18
  __author__ = "Luis Carbonel"
18
19
  __email__ = "devhighlevel@gmail.com"
19
20
 
@@ -38,6 +39,8 @@ __all__ = [
38
39
  "SQLQuery",
39
40
  # Embedding Provider
40
41
  "EmbeddingProviderPort",
42
+ # Example Store
43
+ "ExampleStore",
41
44
  # Configuration
42
45
  "settings",
43
46
  # Exceptions
@@ -130,9 +133,7 @@ async def create_repository(
130
133
  repo = await create_repository("postgresql://user:pass@localhost/db")
131
134
  tables = await repo.get_tables()
132
135
  """
133
- return await RepositoryFactory.create_and_initialize(
134
- database_url, schema_name, database_type
135
- )
136
+ return await RepositoryFactory.create_and_initialize(database_url, schema_name, database_type)
136
137
 
137
138
 
138
139
  def create_query_service(
@@ -144,6 +145,7 @@ def create_query_service(
144
145
  embedding_provider: Optional[EmbeddingProviderPort] = None,
145
146
  embedding_provider_type: Optional[str] = None,
146
147
  schema_name: str = "public",
148
+ example_store: Optional[ExampleStore] = None,
147
149
  ) -> QueryGenerationService:
148
150
  """
149
151
  Create a configured query service instance.
@@ -224,6 +226,7 @@ def create_query_service(
224
226
  schema_filters=schema_filters,
225
227
  embedding_provider=embedding_provider,
226
228
  schema_name=schema_name,
229
+ example_store=example_store,
227
230
  )
228
231
 
229
232
  return service
@@ -238,6 +241,7 @@ async def create_and_initialize_service(
238
241
  embedding_provider: Optional[EmbeddingProviderPort] = None,
239
242
  embedding_provider_type: Optional[str] = None,
240
243
  schema_name: str = "public",
244
+ example_store: Optional[ExampleStore] = None,
241
245
  ) -> QueryGenerationService:
242
246
  """
243
247
  Create and initialize a query service with automatic schema loading.
@@ -274,6 +278,7 @@ async def create_and_initialize_service(
274
278
  embedding_provider,
275
279
  embedding_provider_type,
276
280
  schema_name,
281
+ example_store,
277
282
  )
278
283
  await service.initialize(database_type)
279
284
  return service
@@ -289,6 +294,7 @@ async def generate_sql_from_db(
289
294
  embedding_provider: Optional[EmbeddingProviderPort] = None,
290
295
  embedding_provider_type: Optional[str] = None,
291
296
  schema_name: str = "public",
297
+ example_store: Optional[ExampleStore] = None,
292
298
  **kwargs,
293
299
  ) -> Dict[str, Any]:
294
300
  """
@@ -346,5 +352,6 @@ async def generate_sql_from_db(
346
352
  embedding_provider,
347
353
  embedding_provider_type,
348
354
  schema_name,
355
+ example_store,
349
356
  )
350
357
  return await service.generate_sql(question=question, database_type=database_type, **kwargs)
@@ -129,7 +129,11 @@ class AnthropicAdapter(AIProviderPort):
129
129
  # Add instructions
130
130
  prompt_parts.append("Instructions:")
131
131
  prompt_parts.append(f"1. Generate a {context.database_type} SQL query that answers the question")
132
- prompt_parts.append("2. Use only the tables and columns from the provided schema")
132
+ prompt_parts.append(
133
+ "2. Use EXACT column names as listed in the schema above. "
134
+ "NEVER abbreviate, shorten, or infer column names. "
135
+ "If a column is not explicitly listed in the schema, DO NOT use it."
136
+ )
133
137
  prompt_parts.append("3. Follow SQL best practices and optimize for performance")
134
138
  prompt_parts.append("4. Include appropriate JOINs, WHERE clauses, and ORDER BY if needed")
135
139
  prompt_parts.append("5. Return the response in JSON format with 'sql', 'explanation', and 'confidence' fields")
@@ -151,15 +155,9 @@ Your response must be a valid JSON object with exactly these fields:
151
155
 
152
156
  Ensure the JSON is properly formatted with no syntax errors. Escape any quotes inside strings properly."""
153
157
 
154
- database_specific = {
155
- "postgres": "You specialize in PostgreSQL syntax and features. Use PostgreSQL-specific functions and syntax when appropriate.",
156
- "mysql": "You specialize in MySQL syntax and features. Use MySQL-specific functions and syntax when appropriate.",
157
- "sqlite": "You specialize in SQLite syntax and features. Use SQLite-specific functions and syntax when appropriate.",
158
- "mssql": "You specialize in SQL Server syntax and features. Use T-SQL syntax when appropriate.",
159
- "oracle": "You specialize in Oracle SQL syntax and features. Use Oracle-specific functions and syntax when appropriate.",
160
- }
158
+ from ..core.database_prompts import get_database_hint
161
159
 
162
- specific_prompt = database_specific.get(database_type.lower(), "")
160
+ specific_prompt = get_database_hint(database_type)
163
161
 
164
162
  return f"{base_prompt} {specific_prompt}"
165
163
 
@@ -142,7 +142,11 @@ class GeminiAdapter(AIProviderPort):
142
142
  # Add instructions
143
143
  prompt_parts.append("Instructions:")
144
144
  prompt_parts.append(f"1. Generate a {context.database_type} SQL query that answers the question")
145
- prompt_parts.append("2. Use only the tables and columns from the provided schema")
145
+ prompt_parts.append(
146
+ "2. Use EXACT column names as listed in the schema above. "
147
+ "NEVER abbreviate, shorten, or infer column names. "
148
+ "If a column is not explicitly listed in the schema, DO NOT use it."
149
+ )
146
150
  prompt_parts.append("3. Follow SQL best practices and optimize for performance")
147
151
  prompt_parts.append("4. Include appropriate JOINs, WHERE clauses, and ORDER BY if needed")
148
152
  prompt_parts.append("5. Return the response in JSON format with 'sql', 'explanation', and 'confidence' fields")
@@ -164,15 +168,9 @@ Your response must be a valid JSON object with exactly these fields:
164
168
 
165
169
  Ensure the JSON is properly formatted with no syntax errors. Escape any quotes inside strings properly."""
166
170
 
167
- database_specific = {
168
- "postgres": "You specialize in PostgreSQL syntax and features. Use PostgreSQL-specific functions and syntax when appropriate.",
169
- "mysql": "You specialize in MySQL syntax and features. Use MySQL-specific functions and syntax when appropriate.",
170
- "sqlite": "You specialize in SQLite syntax and features. Use SQLite-specific functions and syntax when appropriate.",
171
- "mssql": "You specialize in SQL Server syntax and features. Use T-SQL syntax when appropriate.",
172
- "oracle": "You specialize in Oracle SQL syntax and features. Use Oracle-specific functions and syntax when appropriate.",
173
- }
171
+ from ..core.database_prompts import get_database_hint
174
172
 
175
- specific_prompt = database_specific.get(database_type.lower(), "")
173
+ specific_prompt = get_database_hint(database_type)
176
174
 
177
175
  return f"{base_prompt} {specific_prompt}"
178
176
 
@@ -163,7 +163,11 @@ class OpenAIAdapter(AIProviderPort):
163
163
  # Add instructions
164
164
  prompt_parts.append("Instructions:")
165
165
  prompt_parts.append(f"1. Generate a {context.database_type} SQL query that answers the question")
166
- prompt_parts.append("2. Use only the tables and columns from the provided schema")
166
+ prompt_parts.append(
167
+ "2. Use EXACT column names as listed in the schema above. "
168
+ "NEVER abbreviate, shorten, or infer column names. "
169
+ "If a column is not explicitly listed in the schema, DO NOT use it."
170
+ )
167
171
  prompt_parts.append("3. Follow SQL best practices and optimize for performance")
168
172
  prompt_parts.append("4. Include appropriate JOINs, WHERE clauses, and ORDER BY if needed")
169
173
  prompt_parts.append("5. Return the response in JSON format with 'sql', 'explanation', and 'confidence' fields")
@@ -204,15 +208,9 @@ Your response must be a valid JSON object with exactly these fields:
204
208
 
205
209
  Ensure the JSON is properly formatted with no syntax errors. Escape any quotes inside strings properly."""
206
210
 
207
- database_specific = {
208
- "postgres": "You specialize in PostgreSQL syntax and features. Use PostgreSQL-specific functions and syntax when appropriate.",
209
- "mysql": "You specialize in MySQL syntax and features. Use MySQL-specific functions and syntax when appropriate.",
210
- "sqlite": "You specialize in SQLite syntax and features. Use SQLite-specific functions and syntax when appropriate.",
211
- "mssql": "You specialize in SQL Server syntax and features. Use T-SQL syntax when appropriate.",
212
- "oracle": "You specialize in Oracle SQL syntax and features. Use Oracle-specific functions and syntax when appropriate.",
213
- }
211
+ from ..core.database_prompts import get_database_hint
214
212
 
215
- specific_prompt = database_specific.get(database_type.lower(), "")
213
+ specific_prompt = get_database_hint(database_type)
216
214
 
217
215
  return f"{base_prompt} {specific_prompt}"
218
216
 
@@ -406,16 +406,18 @@ class PostgreSQLRepository(SchemaRepositoryPort):
406
406
  col_name = row[5]
407
407
  if col_name and col_name not in tables_dict[table_name]["_seen_columns"]:
408
408
  tables_dict[table_name]["_seen_columns"].add(col_name)
409
- tables_dict[table_name]["columns"].append({
410
- "name": col_name,
411
- "type": row[6],
412
- "nullable": row[7] == "YES",
413
- "default": row[8],
414
- "max_length": row[9],
415
- "precision": row[10],
416
- "scale": row[11],
417
- "description": row[12],
418
- })
409
+ tables_dict[table_name]["columns"].append(
410
+ {
411
+ "name": col_name,
412
+ "type": row[6],
413
+ "nullable": row[7] == "YES",
414
+ "default": row[8],
415
+ "max_length": row[9],
416
+ "precision": row[10],
417
+ "scale": row[11],
418
+ "description": row[12],
419
+ }
420
+ )
419
421
 
420
422
  # Add foreign key (if present and not seen)
421
423
  fk_column = row[15]
@@ -424,12 +426,14 @@ class PostgreSQLRepository(SchemaRepositoryPort):
424
426
  fk_key = f"{fk_constraint}:{fk_column}"
425
427
  if fk_key not in tables_dict[table_name]["_seen_fks"]:
426
428
  tables_dict[table_name]["_seen_fks"].add(fk_key)
427
- tables_dict[table_name]["foreign_keys"].append({
428
- "column": fk_column,
429
- "ref_table": row[16],
430
- "ref_column": row[17],
431
- "constraint_name": fk_constraint,
432
- })
429
+ tables_dict[table_name]["foreign_keys"].append(
430
+ {
431
+ "column": fk_column,
432
+ "ref_table": row[16],
433
+ "ref_column": row[17],
434
+ "constraint_name": fk_constraint,
435
+ }
436
+ )
433
437
 
434
438
  # Convert to TableInfo objects
435
439
  tables = []
@@ -438,18 +442,20 @@ class PostgreSQLRepository(SchemaRepositoryPort):
438
442
  del table_data["_seen_columns"]
439
443
  del table_data["_seen_fks"]
440
444
 
441
- tables.append(TableInfo(
442
- name=table_data["name"],
443
- schema=table_data["schema"],
444
- columns=table_data["columns"],
445
- primary_keys=table_data["primary_keys"],
446
- foreign_keys=table_data["foreign_keys"],
447
- indexes=table_data["indexes"],
448
- row_count=table_data["row_count"],
449
- size_bytes=table_data["size_bytes"],
450
- description=table_data["description"],
451
- last_updated=datetime.now(),
452
- ))
445
+ tables.append(
446
+ TableInfo(
447
+ name=table_data["name"],
448
+ schema=table_data["schema"],
449
+ columns=table_data["columns"],
450
+ primary_keys=table_data["primary_keys"],
451
+ foreign_keys=table_data["foreign_keys"],
452
+ indexes=table_data["indexes"],
453
+ row_count=table_data["row_count"],
454
+ size_bytes=table_data["size_bytes"],
455
+ description=table_data["description"],
456
+ last_updated=datetime.now(),
457
+ )
458
+ )
453
459
 
454
460
  logger.info("Tables processed from bulk query", count=len(tables))
455
461
  return tables
@@ -12,6 +12,7 @@ from typing import Any, Dict, List, Optional, Tuple
12
12
 
13
13
  import psycopg2
14
14
  import structlog
15
+ from psycopg2 import sql as psycopg2_sql
15
16
  from psycopg2.extras import RealDictCursor
16
17
 
17
18
  from ..exceptions import SchemaException, SecurityException
@@ -333,9 +334,7 @@ class RedshiftRepository(SchemaRepositoryPort):
333
334
 
334
335
  try:
335
336
  start_time = time.time()
336
- rows = await asyncio.to_thread(
337
- self._execute_query, bulk_query, (schema,)
338
- )
337
+ rows = await asyncio.to_thread(self._execute_query, bulk_query, (schema,))
339
338
 
340
339
  elapsed_ms = round((time.time() - start_time) * 1000, 2)
341
340
  logger.info("Bulk query completed", rows=len(rows), elapsed_ms=elapsed_ms)
@@ -374,34 +373,38 @@ class RedshiftRepository(SchemaRepositoryPort):
374
373
  col_name = row.get("column_name")
375
374
  if col_name and col_name not in tables_dict[table_name]["_seen_columns"]:
376
375
  tables_dict[table_name]["_seen_columns"].add(col_name)
377
- tables_dict[table_name]["columns"].append({
378
- "name": col_name,
379
- "type": row.get("data_type", ""),
380
- "nullable": row.get("is_nullable") == "YES",
381
- "default": row.get("column_default"),
382
- "max_length": row.get("character_maximum_length"),
383
- "precision": row.get("numeric_precision"),
384
- "scale": row.get("numeric_scale"),
385
- "description": "",
386
- })
376
+ tables_dict[table_name]["columns"].append(
377
+ {
378
+ "name": col_name,
379
+ "type": row.get("data_type", ""),
380
+ "nullable": row.get("is_nullable") == "YES",
381
+ "default": row.get("column_default"),
382
+ "max_length": row.get("character_maximum_length"),
383
+ "precision": row.get("numeric_precision"),
384
+ "scale": row.get("numeric_scale"),
385
+ "description": "",
386
+ }
387
+ )
387
388
 
388
389
  # Convert to TableInfo objects
389
390
  tables = []
390
391
  for table_data in tables_dict.values():
391
392
  del table_data["_seen_columns"]
392
393
 
393
- tables.append(TableInfo(
394
- name=table_data["name"],
395
- schema=table_data["schema"],
396
- columns=table_data["columns"],
397
- primary_keys=table_data["primary_keys"],
398
- foreign_keys=table_data["foreign_keys"],
399
- indexes=table_data["indexes"],
400
- row_count=0,
401
- size_bytes=0,
402
- description=table_data["description"],
403
- last_updated=datetime.now(),
404
- ))
394
+ tables.append(
395
+ TableInfo(
396
+ name=table_data["name"],
397
+ schema=table_data["schema"],
398
+ columns=table_data["columns"],
399
+ primary_keys=table_data["primary_keys"],
400
+ foreign_keys=table_data["foreign_keys"],
401
+ indexes=table_data["indexes"],
402
+ row_count=0,
403
+ size_bytes=0,
404
+ description=table_data["description"],
405
+ last_updated=datetime.now(),
406
+ )
407
+ )
405
408
 
406
409
  logger.info("Tables processed from bulk query", count=len(tables))
407
410
  return tables
@@ -646,8 +649,11 @@ class RedshiftRepository(SchemaRepositoryPort):
646
649
  start_time = time.time()
647
650
  conn = self._get_connection()
648
651
  try:
649
- # Set statement timeout (in milliseconds for Redshift)
650
652
  cursor = conn.cursor()
653
+ # Set search_path so unqualified table names resolve to our schema
654
+ cursor.execute(
655
+ psycopg2_sql.SQL("SET search_path TO {}, public").format(psycopg2_sql.Identifier(self.schema_name))
656
+ )
651
657
  cursor.execute(f"SET statement_timeout TO {timeout_seconds * 1000}")
652
658
  cursor.close()
653
659
 
@@ -913,6 +913,7 @@ def cache_clear(clear_all: bool, embeddings: bool, queries: bool, tables: bool):
913
913
  if Path(path).exists():
914
914
  if Path(path).is_dir():
915
915
  import shutil
916
+
916
917
  shutil.rmtree(path)
917
918
  else:
918
919
  Path(path).unlink()
@@ -925,6 +926,7 @@ def cache_clear(clear_all: bool, embeddings: bool, queries: bool, tables: bool):
925
926
  if Path(path).exists():
926
927
  if Path(path).is_dir():
927
928
  import shutil
929
+
928
930
  shutil.rmtree(path)
929
931
  else:
930
932
  Path(path).unlink()
@@ -29,7 +29,7 @@ class Settings(BaseSettings):
29
29
 
30
30
  # General settings
31
31
  app_name: str = "nlp2sql"
32
- version: str = "0.2.0rc6"
32
+ version: str = "0.2.0rc7"
33
33
  debug: bool = Field(default=False, validation_alias="NLP2SQL_DEBUG")
34
34
  log_level: LogLevel = Field(default=LogLevel.INFO, validation_alias="NLP2SQL_LOG_LEVEL")
35
35
 
@@ -0,0 +1,48 @@
1
+ """SQL dialect hints for AI providers.
2
+
3
+ Each entry provides database-specific guidance that helps AI providers
4
+ generate syntactically correct SQL for the target database. Centralized
5
+ here to avoid duplication across adapters.
6
+ """
7
+
8
+ DATABASE_SQL_HINTS: dict[str, str] = {
9
+ "postgres": (
10
+ "You specialize in PostgreSQL syntax and features. "
11
+ "Use PostgreSQL-specific functions and syntax when appropriate."
12
+ ),
13
+ "mysql": ("You specialize in MySQL syntax and features. Use MySQL-specific functions and syntax when appropriate."),
14
+ "sqlite": (
15
+ "You specialize in SQLite syntax and features. Use SQLite-specific functions and syntax when appropriate."
16
+ ),
17
+ "mssql": ("You specialize in SQL Server syntax and features. Use T-SQL syntax when appropriate."),
18
+ "oracle": (
19
+ "You specialize in Oracle SQL syntax and features. Use Oracle-specific functions and syntax when appropriate."
20
+ ),
21
+ "redshift": (
22
+ "You specialize in Amazon Redshift SQL syntax for data warehouse analytics. "
23
+ "CRITICAL RULES: "
24
+ "1) Use DATEADD(datepart, n, date) instead of INTERVAL arithmetic "
25
+ "(e.g., DATEADD(month, -1, CURRENT_DATE) not CURRENT_DATE - INTERVAL '1 month'). "
26
+ "2) Use DATE_TRUNC('unit', date) for truncation - NEVER use TRUNC(date, 'format'). "
27
+ "3) Use LISTAGG(col, delimiter) WITHIN GROUP (ORDER BY ...) instead of STRING_AGG. "
28
+ "4) CONCAT() only takes 2 arguments - use || operator to chain more. "
29
+ "5) Use GETDATE() or CURRENT_DATE instead of NOW(). "
30
+ "6) DISTINCT ON is not supported - use ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) = 1 pattern. "
31
+ "7) generate_series() is not supported - use CTEs or date tables for sequences. "
32
+ "8) No ARRAY types or ARRAY_AGG - use SUPER type for semi-structured data. "
33
+ "9) Use APPROXIMATE COUNT(DISTINCT col) for large cardinality counts. "
34
+ "10) Avoid SELECT * - only select needed columns for columnar storage efficiency."
35
+ ),
36
+ }
37
+
38
+
39
+ def get_database_hint(database_type: str) -> str:
40
+ """Get SQL dialect hint for a database type.
41
+
42
+ Args:
43
+ database_type: Database type string (e.g., 'postgres', 'redshift').
44
+
45
+ Returns:
46
+ Dialect-specific prompt hint, or empty string if unknown.
47
+ """
48
+ return DATABASE_SQL_HINTS.get(database_type.lower(), "")