nlp2sql 0.2.0rc16__tar.gz → 0.2.0rc17__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.
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.github/workflows/ci.yml +4 -3
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/CHANGELOG.md +53 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/PKG-INFO +1 -1
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/docs/API.md +31 -11
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/docs/CONFIGURATION.md +7 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/mcp_server/__init__.py +1 -1
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/mcp_server/pyproject.toml +2 -2
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/pyproject.toml +1 -1
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/__init__.py +14 -1
- nlp2sql-0.2.0rc17/src/nlp2sql/adapters/_provider_errors.py +99 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/anthropic_adapter.py +58 -19
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/gemini_adapter.py +66 -32
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/openai_adapter.py +43 -23
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/postgres_repository.py +11 -21
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/redshift_adapter.py +11 -4
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/cli.py +8 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/client.py +6 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/config/settings.py +3 -1
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/exceptions/__init__.py +33 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/schema/manager.py +6 -1
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/services/query_service.py +61 -4
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/fakes/fake_openai.py +24 -4
- nlp2sql-0.2.0rc17/tests/test_cost_per_question.py +117 -0
- nlp2sql-0.2.0rc17/tests/test_provider_errors.py +267 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_query_service_contract.py +63 -0
- nlp2sql-0.2.0rc17/tests/test_refresh_schema.py +46 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_retrieval_pipeline.py +2 -0
- nlp2sql-0.2.0rc17/tests/test_schema_context_cache.py +58 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/uv.lock +1 -1
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.claude/commands/db.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.claude/commands/quality.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.claude/commands/test.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.claude/rules/adapters.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.claude/rules/architecture.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.claude/rules/python-style.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.claude/rules/testing.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.claude/settings.json +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.github/scripts/verify_release_tag.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.github/workflows/publish-pypi.yml +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.github/workflows/publish-testpypi.yml +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.gitignore +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.mcp.json.example +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/.python-version +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/CLAUDE.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/CONTRIBUTING.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/LICENSE +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/README.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/SECURITY.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/assets/nlp2sql-logo.png +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/benchmarks/enterprise_benchmark_results.json +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/docker/.env.example +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/docker/README.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/docker/docker-compose.yml +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/docker/get-db-urls.sh +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/docker/init-schema.sql +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/docker/large-schema.sql +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/docker/localstack-init.sh +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/docker/test-redshift.sh +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/docs/ARCHITECTURE.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/docs/ENTERPRISE.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/docs/Redshift.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/README.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/advanced/test_multiple_providers.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/advanced/test_simple_api.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/database_specific/test_odoo_integration.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/default_examples.json +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/documentation/real_world_example.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/getting_started/basic_usage.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/getting_started/simple_demo.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/getting_started/test_api_setup.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/schema_management/test_auto_schema.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/schema_management/test_schema_filters_comprehensive.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/strands-fastapi-agent/.dockerignore +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/strands-fastapi-agent/.env.example +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/strands-fastapi-agent/DOCKER_README.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/strands-fastapi-agent/DOCKER_SETUP.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/strands-fastapi-agent/DOCKER_TESTING_SUMMARY.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/strands-fastapi-agent/Dockerfile +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/strands-fastapi-agent/INDEX.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/strands-fastapi-agent/README.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/strands-fastapi-agent/docker/init-ecommerce-schema.sql +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/strands-fastapi-agent/docker-compose.yml +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/strands-fastapi-agent/main.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/strands-fastapi-agent/pyproject.toml +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/examples/strands-fastapi-agent/test-docker-endpoints.sh +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/install-dev.sh +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/mcp_server/README.md +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/mcp_server/example_config.json +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/mcp_server/server.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/__init__.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/default_error_classifier.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/default_repair_policy.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/dict_semantic_resolver.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/file_semantic_resolver.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/local_embedding_adapter.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/noop_semantic_resolver.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/noop_semantic_validator.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/openai_embedding_adapter.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/pgvector_example_repository.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/regex_query_validator.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/adapters/schema_repository_execution_adapter.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/config/__init__.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/core/__init__.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/core/database_prompts.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/core/entities.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/core/provider_config.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/core/result.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/core/runtime.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/core/sql_keywords.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/core/sql_safety.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/factories/__init__.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/factories/repository_factory.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/ports/__init__.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/ports/ai_provider.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/ports/cache.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/ports/embedding_provider.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/ports/error_classifier.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/ports/example_repository.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/ports/query_execution.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/ports/query_optimizer.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/ports/query_safety.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/ports/query_validator.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/ports/repair_policy.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/ports/schema_repository.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/ports/schema_strategy.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/ports/semantic_resolver.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/ports/semantic_validator.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/schema/__init__.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/schema/analyzer.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/schema/embedding_manager.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/schema/example_loader.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/schema/example_store.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/services/__init__.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/services/example_selection_service.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/services/prompt_assembly_service.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/services/query_analysis_service.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/services/query_repair_service.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/services/semantic_resolution_service.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/services/semantic_validation_service.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/services/sql_intent_planning_service.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/utils/__init__.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/utils/artifact_loader.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/utils/helpers.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/utils/semantic_prompt.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/src/nlp2sql/utils/storage.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/test_tmp/.gitignore +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/conftest.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/data/gold_questions.json +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/fakes/__init__.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/mcp/__init__.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/mcp/test_sql_security.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_artifact_loader.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_basic.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_batch_scoring.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_cli_semantic_support.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_cli_setup.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_client_hooks.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_column_validation.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_connect_embeddings.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_database_prompts.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_dsl_integration.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_embedding_manager.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_false_invalid_regression.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_first_run.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_integration.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_model_defaults.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_pgvector_example_repository.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_postgres_integration.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_provider_compat_llm.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_redshift_integration.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_repository_cache.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_repository_factory.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_sql_safety.py +0 -0
- {nlp2sql-0.2.0rc16 → nlp2sql-0.2.0rc17}/tests/test_version_consistency.py +0 -0
|
@@ -90,8 +90,9 @@ jobs:
|
|
|
90
90
|
NLP2SQL_TEST_POSTGRES_DB: testdb
|
|
91
91
|
run: pytest tests/test_pgvector_example_repository.py -m integration -q
|
|
92
92
|
|
|
93
|
-
# README Quick Start per provider
|
|
94
|
-
#
|
|
93
|
+
# README Quick Start per provider, the false-invalid gold set and the cost of
|
|
94
|
+
# one question (LLM calls, tokens, database connections), with the OpenAI SDK
|
|
95
|
+
# pointed at tests/fakes/fake_openai.py (no keys, no cost).
|
|
95
96
|
- name: First-run regression tests
|
|
96
97
|
env:
|
|
97
98
|
PYTHONPATH: src
|
|
@@ -101,7 +102,7 @@ jobs:
|
|
|
101
102
|
NLP2SQL_TEST_POSTGRES_USER: testuser
|
|
102
103
|
NLP2SQL_TEST_POSTGRES_PASS: testpass
|
|
103
104
|
NLP2SQL_TEST_POSTGRES_DB: testdb
|
|
104
|
-
run: pytest tests/test_first_run.py tests/test_false_invalid_regression.py -m integration -q
|
|
105
|
+
run: pytest tests/test_first_run.py tests/test_false_invalid_regression.py tests/test_cost_per_question.py -m integration -q
|
|
105
106
|
|
|
106
107
|
# Installs the built wheel the way a user does: fresh environment, no lock
|
|
107
108
|
# file, latest dependency versions. Catches undeclared dependencies (rc13-rc15
|
|
@@ -22,6 +22,59 @@ pre-1.0.
|
|
|
22
22
|
|
|
23
23
|
## [Unreleased]
|
|
24
24
|
|
|
25
|
+
## [0.2.0rc17] - 2026-10-20
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- **User-visible: the LLM no longer reviews its own SQL by default.** Every `ask()`
|
|
29
|
+
made a second LLM call that sent the whole schema context back to the model; it
|
|
30
|
+
doubled the latency of each question and its tokens were not reported. Column,
|
|
31
|
+
dialect, semantic and read-only checks still run, and execution modes still
|
|
32
|
+
validate against the database. Turn the review back on with
|
|
33
|
+
`connect(..., llm_validation=True)`, `NLP2SQL_LLM_VALIDATION=true` or
|
|
34
|
+
`nlp2sql query --llm-validation`. `NLP2SQL.validate(sql)` still asks the LLM.
|
|
35
|
+
- `QueryResult.tokens_used` now counts every LLM call made for the question
|
|
36
|
+
(retries, repairs and LLM validation); it used to report only the last
|
|
37
|
+
generation call. `metadata["token_usage"]` has the breakdown.
|
|
38
|
+
- `refresh_schema()` (PostgreSQL and Redshift) now drops the cached table list so
|
|
39
|
+
the next `get_tables()` reads the database again. PostgreSQL no longer runs
|
|
40
|
+
`ANALYZE` on the whole database on the first `connect()` and every 24 hours.
|
|
41
|
+
- Provider calls are retried only on rate limits, timeouts, connection errors and
|
|
42
|
+
provider-side errors. Authentication and request errors fail on the first
|
|
43
|
+
attempt, instead of after about 8 seconds and 3 requests.
|
|
44
|
+
- An answer from the model that is not the expected JSON (not JSON, not an object,
|
|
45
|
+
or without `sql`) is asked for again once, without waiting; it used to be retried
|
|
46
|
+
up to 3 times with 4-10 second waits. Both answers count in `tokens_used` and in
|
|
47
|
+
`metadata["token_usage"]["llm_calls"]`.
|
|
48
|
+
- An answer cut off at `max_tokens` (OpenAI `finish_reason="length"`, Anthropic
|
|
49
|
+
`stop_reason="max_tokens"`, Gemini `MAX_TOKENS`) fails on the first attempt with a
|
|
50
|
+
`ProviderException` that says to raise `max_tokens`, instead of "Invalid JSON
|
|
51
|
+
response" after 3 attempts. Anthropic refusals and Gemini safety blocks raise
|
|
52
|
+
`ProviderRequestError`.
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
- `llm_validation` option on `connect()`, `create_query_service()` and
|
|
56
|
+
`create_and_initialize_service()`, `NLP2SQL_LLM_VALIDATION`, and
|
|
57
|
+
`nlp2sql query --llm-validation`.
|
|
58
|
+
- `ProviderAuthenticationError`, `ProviderRequestError`, `ProviderRateLimitError`
|
|
59
|
+
and `ProviderUnavailableError`, subclasses of `ProviderException`, raised by the
|
|
60
|
+
OpenAI, Anthropic and Gemini adapters and available as the `__cause__` of the
|
|
61
|
+
`QueryGenerationException` raised by `ask()`.
|
|
62
|
+
- `ProviderResponseFormatError` (subclass of `ProviderException`): the model answered
|
|
63
|
+
twice without the expected JSON. `details["tokens_used"]` has the tokens of both
|
|
64
|
+
answers.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
- Each `ask()` opened 5 new database connections per table in the schema context
|
|
68
|
+
(41 for a multi-table question). The context is now built from the table list
|
|
69
|
+
loaded at `connect()`, with no extra connections.
|
|
70
|
+
- `RedshiftRepository.refresh_schema()` was a no-op.
|
|
71
|
+
- The Anthropic and Gemini adapters raised `TypeError` instead of
|
|
72
|
+
`TokenLimitException` when the context was too large, and dropped the SDK error
|
|
73
|
+
as the cause of generation failures.
|
|
74
|
+
- The Anthropic adapter reported its own errors as "Failed to parse response: ...".
|
|
75
|
+
- `docs/API.md` showed catching `TokenLimitException` and `ProviderException`
|
|
76
|
+
directly from `ask()`; they arrive as the cause of `QueryGenerationException`.
|
|
77
|
+
|
|
25
78
|
## [0.2.0rc16] - 2026-10-19
|
|
26
79
|
|
|
27
80
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: nlp2sql
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.0rc17
|
|
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
|
|
@@ -57,6 +57,7 @@ nlp = await nlp2sql.connect(
|
|
|
57
57
|
| `hooks` | `ExecutionHooks | None` | Execution, classification, and repair hooks |
|
|
58
58
|
| `semantic_hooks` | `SemanticHooks | None` | Semantic resolver, validator, and optional default semantic context |
|
|
59
59
|
| `semantic_context` | `SemanticContext | dict | None` | Default semantic context applied to future `ask()` calls |
|
|
60
|
+
| `llm_validation` | `bool | None` | Also ask the LLM to review each query (a second call). Default: `NLP2SQL_LLM_VALIDATION`, off |
|
|
60
61
|
|
|
61
62
|
|
|
62
63
|
### `hooks`
|
|
@@ -157,7 +158,7 @@ If you do not pass `execution_mode`, the library infers it from `validate` and `
|
|
|
157
158
|
| `explanation` | `str | None` | Optional natural-language explanation |
|
|
158
159
|
| `provider` | `str` | Provider that generated the SQL |
|
|
159
160
|
| `database_type` | `str` | Postgres or Redshift |
|
|
160
|
-
| `tokens_used` | `int` | Tokens
|
|
161
|
+
| `tokens_used` | `int` | Tokens of every LLM call made for the question |
|
|
161
162
|
| `generation_time_ms` | `float` | End-to-end generation time |
|
|
162
163
|
| `examples_used` | `int` | Count of selected examples |
|
|
163
164
|
| `metadata` | `dict[str, object]` | Runtime metadata for debugging and integration |
|
|
@@ -175,6 +176,7 @@ The metadata payload is intentionally rich. Common fields include:
|
|
|
175
176
|
| `selected_examples` | Few-shot examples selected for prompting |
|
|
176
177
|
| `repair_attempts` | Repair loop details when repair is enabled |
|
|
177
178
|
| `execution_validation` | Execution-time validation outcome |
|
|
179
|
+
| `token_usage` | `total`, `generation`, `validation` and `llm_calls` for the question |
|
|
178
180
|
| `analysis` | Query analysis hints |
|
|
179
181
|
|
|
180
182
|
|
|
@@ -272,29 +274,47 @@ print(result["validation"]["is_valid"])
|
|
|
272
274
|
|
|
273
275
|
## Error Handling
|
|
274
276
|
|
|
277
|
+
`ask()` raises `QueryGenerationException` when SQL cannot be generated. The underlying error is
|
|
278
|
+
its `__cause__`, typed so callers can react to each case:
|
|
279
|
+
|
|
275
280
|
```python
|
|
276
281
|
from nlp2sql.exceptions import (
|
|
277
|
-
|
|
282
|
+
ProviderAuthenticationError,
|
|
283
|
+
ProviderRateLimitError,
|
|
284
|
+
ProviderRequestError,
|
|
285
|
+
ProviderResponseFormatError,
|
|
286
|
+
ProviderUnavailableError,
|
|
278
287
|
QueryGenerationException,
|
|
279
288
|
SchemaException,
|
|
280
|
-
SecurityException,
|
|
281
289
|
TokenLimitException,
|
|
282
290
|
)
|
|
283
291
|
|
|
284
292
|
try:
|
|
285
293
|
result = await nlp.ask("Show revenue by month")
|
|
286
|
-
except
|
|
287
|
-
|
|
294
|
+
except QueryGenerationException as error:
|
|
295
|
+
cause = error.__cause__
|
|
296
|
+
if isinstance(cause, ProviderAuthenticationError):
|
|
297
|
+
print("Invalid or unauthorized API key (HTTP 401/403).")
|
|
298
|
+
elif isinstance(cause, TokenLimitException):
|
|
299
|
+
print("The schema context is too large. Add schema filters.")
|
|
300
|
+
elif isinstance(cause, (ProviderRateLimitError, ProviderUnavailableError)):
|
|
301
|
+
print("Transient provider error; already retried three times.")
|
|
302
|
+
elif isinstance(cause, ProviderRequestError):
|
|
303
|
+
print("The provider rejected the request, e.g. an unknown model.")
|
|
304
|
+
elif isinstance(cause, ProviderResponseFormatError):
|
|
305
|
+
print("The model answered twice without the expected JSON.")
|
|
306
|
+
else:
|
|
307
|
+
print("The provider could not generate SQL.")
|
|
288
308
|
except SchemaException:
|
|
289
309
|
print("Database connection or schema discovery failed.")
|
|
290
|
-
except QueryGenerationException:
|
|
291
|
-
print("The provider could not generate SQL.")
|
|
292
|
-
except ProviderException:
|
|
293
|
-
print("Provider auth, network, or rate-limit error.")
|
|
294
|
-
except SecurityException:
|
|
295
|
-
print("Generated SQL failed safety checks.")
|
|
296
310
|
```
|
|
297
311
|
|
|
312
|
+
Only rate-limit and unavailable errors are retried with backoff; authentication and request
|
|
313
|
+
errors fail on the first attempt. An answer that is not the expected JSON is asked for again
|
|
314
|
+
once, without waiting, and `tokens_used` counts both answers. An answer cut off at `max_tokens`
|
|
315
|
+
is not asked for again: it fails with a `ProviderException` that says to raise `max_tokens`.
|
|
316
|
+
Queries that are not read-only come back with `is_valid=False` and are never executed.
|
|
317
|
+
|
|
298
318
|
## CLI Reference
|
|
299
319
|
|
|
300
320
|
### `nlp2sql query`
|
|
@@ -73,6 +73,7 @@ export NLP2SQL_EMBEDDING_PROVIDER=local
|
|
|
73
73
|
|
|
74
74
|
| Variable | Default | Description |
|
|
75
75
|
|----------|---------|-------------|
|
|
76
|
+
| `NLP2SQL_LLM_VALIDATION` | `false` | Also ask the LLM to review each generated query (a second call per question) |
|
|
76
77
|
| `NLP2SQL_LOG_LEVEL` | `INFO` | Logging verbosity |
|
|
77
78
|
| `NLP2SQL_CACHE_ENABLED` | `true` | Enable or disable query-result caching |
|
|
78
79
|
| `NLP2SQL_ENV` | unset | Optional environment label |
|
|
@@ -184,6 +185,12 @@ The runtime has three practical execution modes.
|
|
|
184
185
|
| `generate_and_validate` | safe validation of generated SQL against a readonly execution target |
|
|
185
186
|
| `generate_validate_repair` | production-like usage where repair is worth the extra latency |
|
|
186
187
|
|
|
188
|
+
Every mode checks column names, dialect rules, semantic rules and that the query is read-only,
|
|
189
|
+
without extra LLM calls. An additional LLM review of the generated SQL is off by default: it
|
|
190
|
+
doubles the latency of each question, and executing the query against the database gives a
|
|
191
|
+
stronger signal. Turn it on with `connect(..., llm_validation=True)`,
|
|
192
|
+
`NLP2SQL_LLM_VALIDATION=true` or `nlp2sql query --llm-validation`.
|
|
193
|
+
|
|
187
194
|
### Python
|
|
188
195
|
|
|
189
196
|
```python
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nlp2sql-mcp"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.0rc17"
|
|
4
4
|
description = "MCP server for nlp2sql - Natural Language to SQL conversion"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.9"
|
|
@@ -21,4 +21,4 @@ build-backend = "hatchling.build"
|
|
|
21
21
|
[tool.mcp]
|
|
22
22
|
name = "nlp2sql"
|
|
23
23
|
description = "Convert natural language queries to SQL using multiple AI providers"
|
|
24
|
-
version = "0.2.
|
|
24
|
+
version = "0.2.0rc17"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nlp2sql"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.0rc17"
|
|
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"}
|
|
@@ -46,7 +46,7 @@ from .ports.semantic_validator import SemanticValidatorPort
|
|
|
46
46
|
from .schema.example_store import ExampleStore
|
|
47
47
|
from .services.query_service import QueryGenerationService
|
|
48
48
|
|
|
49
|
-
__version__ = "0.2.
|
|
49
|
+
__version__ = "0.2.0rc17"
|
|
50
50
|
__author__ = "Luis Carbonel"
|
|
51
51
|
__email__ = "devhighlevel@gmail.com"
|
|
52
52
|
|
|
@@ -112,6 +112,11 @@ __all__ = [
|
|
|
112
112
|
"NLP2SQLException",
|
|
113
113
|
"SchemaException",
|
|
114
114
|
"ProviderException",
|
|
115
|
+
"ProviderAuthenticationError",
|
|
116
|
+
"ProviderRequestError",
|
|
117
|
+
"ProviderRateLimitError",
|
|
118
|
+
"ProviderUnavailableError",
|
|
119
|
+
"ProviderResponseFormatError",
|
|
115
120
|
"TokenLimitException",
|
|
116
121
|
"QueryGenerationException",
|
|
117
122
|
"OptimizationException",
|
|
@@ -219,6 +224,7 @@ def create_query_service(
|
|
|
219
224
|
semantic_validator: SemanticValidatorPort | None = None,
|
|
220
225
|
*,
|
|
221
226
|
provider_config: Optional[ProviderConfig] = None,
|
|
227
|
+
llm_validation: Optional[bool] = None,
|
|
222
228
|
) -> QueryGenerationService:
|
|
223
229
|
"""
|
|
224
230
|
Create a configured query service instance.
|
|
@@ -239,6 +245,8 @@ def create_query_service(
|
|
|
239
245
|
provider_config: Optional ProviderConfig for full control over provider settings
|
|
240
246
|
(model, temperature, max_tokens, extra_kwargs). Individual kwargs like api_key
|
|
241
247
|
and model take precedence over values in provider_config.
|
|
248
|
+
llm_validation: Also ask the LLM to review each generated query (a second call per
|
|
249
|
+
question). If None, uses NLP2SQL_LLM_VALIDATION (default: off).
|
|
242
250
|
|
|
243
251
|
Returns:
|
|
244
252
|
Configured QueryGenerationService instance
|
|
@@ -352,6 +360,7 @@ def create_query_service(
|
|
|
352
360
|
repair_policy=repair_policy,
|
|
353
361
|
semantic_resolver=semantic_resolver,
|
|
354
362
|
semantic_validator=semantic_validator,
|
|
363
|
+
llm_validation=llm_validation,
|
|
355
364
|
)
|
|
356
365
|
|
|
357
366
|
|
|
@@ -373,6 +382,7 @@ async def create_and_initialize_service(
|
|
|
373
382
|
semantic_validator: SemanticValidatorPort | None = None,
|
|
374
383
|
*,
|
|
375
384
|
provider_config: Optional[ProviderConfig] = None,
|
|
385
|
+
llm_validation: Optional[bool] = None,
|
|
376
386
|
) -> QueryGenerationService:
|
|
377
387
|
"""
|
|
378
388
|
Create and initialize a query service with automatic schema loading.
|
|
@@ -393,6 +403,8 @@ async def create_and_initialize_service(
|
|
|
393
403
|
schema_name: Database schema name (default: 'public')
|
|
394
404
|
example_store: Optional pre-loaded few-shot examples for better SQL generation
|
|
395
405
|
provider_config: Optional ProviderConfig for full control over provider settings
|
|
406
|
+
llm_validation: Also ask the LLM to review each generated query. If None, uses
|
|
407
|
+
NLP2SQL_LLM_VALIDATION (default: off).
|
|
396
408
|
|
|
397
409
|
Returns:
|
|
398
410
|
Initialized QueryGenerationService ready for queries
|
|
@@ -423,6 +435,7 @@ async def create_and_initialize_service(
|
|
|
423
435
|
semantic_resolver,
|
|
424
436
|
semantic_validator,
|
|
425
437
|
provider_config=provider_config,
|
|
438
|
+
llm_validation=llm_validation,
|
|
426
439
|
)
|
|
427
440
|
await service.initialize(database_type)
|
|
428
441
|
return service
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Translate provider SDK errors into nlp2sql exceptions, and decide what is retried.
|
|
2
|
+
|
|
3
|
+
The AI adapters retry only transient failures. Before, every exception was retried
|
|
4
|
+
three times with backoff, so an invalid API key took about 8 seconds to surface.
|
|
5
|
+
Works without importing any SDK: OpenAI and Anthropic errors carry ``status_code``,
|
|
6
|
+
google.api_core errors carry an integer ``code``.
|
|
7
|
+
|
|
8
|
+
Output that is not the expected JSON is a different failure: the request worked, the
|
|
9
|
+
model answered badly. ``call_with_format_retry`` asks again once, without waiting,
|
|
10
|
+
and counts the tokens of both answers.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Any, Awaitable, Callable, Dict, NamedTuple, Optional, TypeVar
|
|
14
|
+
|
|
15
|
+
import structlog
|
|
16
|
+
|
|
17
|
+
from ..exceptions import (
|
|
18
|
+
ProviderAuthenticationError,
|
|
19
|
+
ProviderException,
|
|
20
|
+
ProviderRateLimitError,
|
|
21
|
+
ProviderRequestError,
|
|
22
|
+
ProviderResponseFormatError,
|
|
23
|
+
ProviderUnavailableError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = structlog.get_logger()
|
|
27
|
+
|
|
28
|
+
TRANSIENT_PROVIDER_ERRORS = (ProviderRateLimitError, ProviderUnavailableError)
|
|
29
|
+
FORMAT_RETRY_ATTEMPTS = 2
|
|
30
|
+
|
|
31
|
+
ResponseT = TypeVar("ResponseT")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _http_status(exc: BaseException) -> Optional[int]:
|
|
35
|
+
for attribute in ("status_code", "code"):
|
|
36
|
+
value = getattr(exc, attribute, None)
|
|
37
|
+
if isinstance(value, int) and 100 <= value < 600:
|
|
38
|
+
return int(value)
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _is_transport_error(exc: BaseException) -> bool:
|
|
43
|
+
if isinstance(exc, (TimeoutError, ConnectionError)):
|
|
44
|
+
return True
|
|
45
|
+
name = type(exc).__name__
|
|
46
|
+
return "Timeout" in name or "Connection" in name
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def classify_provider_error(exc: BaseException, message: str) -> ProviderException:
|
|
50
|
+
"""Return the nlp2sql exception for ``exc``; nlp2sql exceptions pass through unchanged."""
|
|
51
|
+
if isinstance(exc, ProviderException):
|
|
52
|
+
return exc
|
|
53
|
+
|
|
54
|
+
status = _http_status(exc)
|
|
55
|
+
if status in (401, 403):
|
|
56
|
+
return ProviderAuthenticationError(message, {"status_code": status})
|
|
57
|
+
if status == 429:
|
|
58
|
+
return ProviderRateLimitError(message, {"status_code": status})
|
|
59
|
+
if status is not None and (status >= 500 or status in (408, 409)):
|
|
60
|
+
return ProviderUnavailableError(message, {"status_code": status})
|
|
61
|
+
if status is not None and 400 <= status < 500:
|
|
62
|
+
return ProviderRequestError(message, {"status_code": status})
|
|
63
|
+
if _is_transport_error(exc):
|
|
64
|
+
return ProviderUnavailableError(message)
|
|
65
|
+
return ProviderException(message)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ParsedGeneration(NamedTuple):
|
|
69
|
+
response: Any
|
|
70
|
+
result: Dict[str, Any]
|
|
71
|
+
tokens_used: int
|
|
72
|
+
format_retries: int
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def call_with_format_retry(
|
|
76
|
+
send: Callable[[], Awaitable[ResponseT]],
|
|
77
|
+
parse: Callable[[ResponseT], Dict[str, Any]],
|
|
78
|
+
tokens_of: Callable[[ResponseT], int],
|
|
79
|
+
*,
|
|
80
|
+
provider: str,
|
|
81
|
+
attempts: int = FORMAT_RETRY_ATTEMPTS,
|
|
82
|
+
) -> ParsedGeneration:
|
|
83
|
+
"""Send a generation request and parse it, asking again when the output is malformed.
|
|
84
|
+
|
|
85
|
+
Tokens are read before parsing, so a discarded answer still counts. Every other
|
|
86
|
+
error propagates on the first attempt; transient ones are retried by the caller.
|
|
87
|
+
"""
|
|
88
|
+
tokens_used = 0
|
|
89
|
+
for attempt in range(1, attempts + 1):
|
|
90
|
+
response = await send()
|
|
91
|
+
tokens_used += tokens_of(response)
|
|
92
|
+
try:
|
|
93
|
+
return ParsedGeneration(response, parse(response), tokens_used, attempt - 1)
|
|
94
|
+
except ProviderResponseFormatError as error:
|
|
95
|
+
error.details["tokens_used"] = tokens_used
|
|
96
|
+
if attempt == attempts:
|
|
97
|
+
raise
|
|
98
|
+
logger.warning("Malformed model output, asking again", provider=provider, error=str(error))
|
|
99
|
+
raise ValueError("attempts must be at least 1")
|
|
@@ -5,14 +5,15 @@ from typing import Any, Dict, Optional
|
|
|
5
5
|
|
|
6
6
|
import anthropic
|
|
7
7
|
import structlog
|
|
8
|
-
from tenacity import retry, stop_after_attempt, wait_exponential
|
|
8
|
+
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
|
|
9
9
|
|
|
10
10
|
from ..config.settings import settings
|
|
11
11
|
from ..core.provider_config import ProviderConfig
|
|
12
|
-
from ..exceptions import ProviderException, TokenLimitException
|
|
12
|
+
from ..exceptions import ProviderException, ProviderRequestError, ProviderResponseFormatError, TokenLimitException
|
|
13
13
|
from ..ports.ai_provider import AIProviderPort, AIProviderType, QueryContext, QueryResponse
|
|
14
14
|
from ..utils.helpers import first_not_none
|
|
15
15
|
from ..utils.semantic_prompt import format_semantic_context_lines, format_sql_intent_plan_lines
|
|
16
|
+
from ._provider_errors import TRANSIENT_PROVIDER_ERRORS, call_with_format_retry, classify_provider_error
|
|
16
17
|
|
|
17
18
|
logger = structlog.get_logger()
|
|
18
19
|
|
|
@@ -119,7 +120,12 @@ class AnthropicAdapter(AIProviderPort):
|
|
|
119
120
|
"""Get maximum input context size for the model."""
|
|
120
121
|
return self._max_context_size
|
|
121
122
|
|
|
122
|
-
@retry(
|
|
123
|
+
@retry(
|
|
124
|
+
stop=stop_after_attempt(3),
|
|
125
|
+
wait=wait_exponential(multiplier=1, min=4, max=10),
|
|
126
|
+
retry=retry_if_exception_type(TRANSIENT_PROVIDER_ERRORS),
|
|
127
|
+
reraise=True,
|
|
128
|
+
)
|
|
123
129
|
async def generate_query(self, context: QueryContext) -> QueryResponse:
|
|
124
130
|
"""Generate SQL query using Claude."""
|
|
125
131
|
try:
|
|
@@ -133,22 +139,27 @@ class AnthropicAdapter(AIProviderPort):
|
|
|
133
139
|
# Create messages
|
|
134
140
|
messages = [{"role": "user", "content": prompt}]
|
|
135
141
|
|
|
136
|
-
# Call Claude API (no sampling parameters: see class docstring)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
# Call Claude API (no sampling parameters: see class docstring); malformed
|
|
143
|
+
# output is asked for again once
|
|
144
|
+
generation = await call_with_format_retry(
|
|
145
|
+
lambda: self.client.messages.create(
|
|
146
|
+
model=self.model,
|
|
147
|
+
max_tokens=context.max_tokens,
|
|
148
|
+
system=system_prompt,
|
|
149
|
+
messages=messages,
|
|
150
|
+
),
|
|
151
|
+
self._parse_response,
|
|
152
|
+
self._total_tokens,
|
|
153
|
+
provider=self.provider_type.value,
|
|
142
154
|
)
|
|
143
|
-
|
|
144
|
-
# Parse response
|
|
145
|
-
result = self._parse_response(response)
|
|
155
|
+
response, result = generation.response, generation.result
|
|
146
156
|
|
|
147
157
|
# Create QueryResponse
|
|
148
158
|
metadata = {
|
|
149
159
|
"model": self.model,
|
|
150
160
|
"input_tokens": response.usage.input_tokens,
|
|
151
161
|
"output_tokens": response.usage.output_tokens,
|
|
162
|
+
"format_retries": generation.format_retries,
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
# Include raw response if available
|
|
@@ -159,14 +170,17 @@ class AnthropicAdapter(AIProviderPort):
|
|
|
159
170
|
sql=result["sql"],
|
|
160
171
|
explanation=result.get("explanation", ""),
|
|
161
172
|
confidence=result.get("confidence", 0.8),
|
|
162
|
-
tokens_used=
|
|
173
|
+
tokens_used=generation.tokens_used,
|
|
163
174
|
provider=self.provider_type.value,
|
|
164
175
|
metadata=metadata,
|
|
165
176
|
)
|
|
166
177
|
|
|
167
178
|
except Exception as e:
|
|
168
|
-
logger.error("Anthropic query generation failed", error=str(e))
|
|
169
|
-
|
|
179
|
+
logger.error("Anthropic query generation failed", error=str(e), error_type=type(e).__name__)
|
|
180
|
+
error = classify_provider_error(e, f"Anthropic query generation failed: {e!s}")
|
|
181
|
+
if error is e:
|
|
182
|
+
raise
|
|
183
|
+
raise error from e
|
|
170
184
|
|
|
171
185
|
def _build_prompt(self, context: QueryContext) -> str:
|
|
172
186
|
"""Build prompt for query generation."""
|
|
@@ -256,6 +270,15 @@ Ensure the JSON is properly formatted with no syntax errors. Escape any quotes i
|
|
|
256
270
|
def _parse_response(self, response) -> Dict[str, Any]:
|
|
257
271
|
"""Parse Anthropic response."""
|
|
258
272
|
try:
|
|
273
|
+
stop_reason = getattr(response, "stop_reason", None)
|
|
274
|
+
if stop_reason == "max_tokens":
|
|
275
|
+
# An answer cut at max_tokens would be cut again, so it is not retried.
|
|
276
|
+
raise ProviderException("Response truncated at max_tokens (stop_reason=max_tokens); raise max_tokens")
|
|
277
|
+
if stop_reason == "refusal":
|
|
278
|
+
raise ProviderRequestError("Claude declined to answer (stop_reason=refusal)")
|
|
279
|
+
if not response.content:
|
|
280
|
+
raise ProviderResponseFormatError(f"Empty response (stop_reason={stop_reason})")
|
|
281
|
+
|
|
259
282
|
raw_content = response.content[0].text.strip()
|
|
260
283
|
content = raw_content
|
|
261
284
|
|
|
@@ -279,8 +302,8 @@ Ensure the JSON is properly formatted with no syntax errors. Escape any quotes i
|
|
|
279
302
|
result = json.loads(content)
|
|
280
303
|
|
|
281
304
|
# Validate required fields
|
|
282
|
-
if "sql" not in result:
|
|
283
|
-
raise
|
|
305
|
+
if not isinstance(result, dict) or "sql" not in result:
|
|
306
|
+
raise ProviderResponseFormatError("Response missing required 'sql' field")
|
|
284
307
|
|
|
285
308
|
# Set defaults
|
|
286
309
|
result.setdefault("explanation", "")
|
|
@@ -291,14 +314,21 @@ Ensure the JSON is properly formatted with no syntax errors. Escape any quotes i
|
|
|
291
314
|
|
|
292
315
|
return result
|
|
293
316
|
|
|
317
|
+
except ProviderException:
|
|
318
|
+
raise
|
|
294
319
|
except json.JSONDecodeError as e:
|
|
295
320
|
# Log the problematic content
|
|
296
321
|
logger.error("JSON parsing failed", content=content, error=str(e))
|
|
297
|
-
raise
|
|
322
|
+
raise ProviderResponseFormatError(f"Invalid JSON response: {e!s}") from e
|
|
298
323
|
except Exception as e:
|
|
299
324
|
logger.error("Response parsing failed", error=str(e))
|
|
300
325
|
raise ProviderException(f"Failed to parse response: {e!s}")
|
|
301
326
|
|
|
327
|
+
@staticmethod
|
|
328
|
+
def _total_tokens(response) -> int:
|
|
329
|
+
usage = response.usage
|
|
330
|
+
return (getattr(usage, "input_tokens", 0) or 0) + (getattr(usage, "output_tokens", 0) or 0)
|
|
331
|
+
|
|
302
332
|
async def _validate_token_count(self, context: QueryContext) -> None:
|
|
303
333
|
"""Validate that context doesn't exceed token limits."""
|
|
304
334
|
# Build prompt to count tokens
|
|
@@ -315,7 +345,11 @@ Ensure the JSON is properly formatted with no syntax errors. Escape any quotes i
|
|
|
315
345
|
available_tokens = max_context - context.max_tokens # Reserve space for response
|
|
316
346
|
|
|
317
347
|
if total_tokens > available_tokens:
|
|
318
|
-
raise TokenLimitException(
|
|
348
|
+
raise TokenLimitException(
|
|
349
|
+
f"Context too large: {total_tokens} tokens exceeds limit of {available_tokens}",
|
|
350
|
+
tokens_used=total_tokens,
|
|
351
|
+
max_tokens=available_tokens,
|
|
352
|
+
)
|
|
319
353
|
|
|
320
354
|
logger.debug(
|
|
321
355
|
"Token validation passed",
|
|
@@ -379,6 +413,11 @@ Check for:
|
|
|
379
413
|
result.setdefault("issues", [])
|
|
380
414
|
result.setdefault("suggestions", [])
|
|
381
415
|
result.setdefault("complexity", "moderate")
|
|
416
|
+
# Reported so the service can account for every LLM call of a question
|
|
417
|
+
usage = getattr(message, "usage", None)
|
|
418
|
+
input_tokens = getattr(usage, "input_tokens", 0) or 0
|
|
419
|
+
output_tokens = getattr(usage, "output_tokens", 0) or 0
|
|
420
|
+
result["_tokens_used"] = input_tokens + output_tokens
|
|
382
421
|
|
|
383
422
|
return result
|
|
384
423
|
|