nl2sql-api 0.1.2__tar.gz → 0.2.1__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 (50) hide show
  1. nl2sql_api-0.2.1/PKG-INFO +71 -0
  2. nl2sql_api-0.2.1/README.md +42 -0
  3. nl2sql_api-0.2.1/pyproject.toml +45 -0
  4. nl2sql_api-0.2.1/src/nl2sql_api/auth.py +92 -0
  5. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/main.py +11 -9
  6. nl2sql_api-0.2.1/src/nl2sql_api/models/query.py +34 -0
  7. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/routes/datasource.py +12 -5
  8. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/routes/health.py +5 -3
  9. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/routes/indexing.py +11 -5
  10. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/routes/llm.py +9 -4
  11. nl2sql_api-0.2.1/src/nl2sql_api/routes/query.py +48 -0
  12. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/services/indexing.py +2 -8
  13. nl2sql_api-0.2.1/src/nl2sql_api/services/query.py +17 -0
  14. nl2sql_api-0.2.1/src/nl2sql_api.egg-info/PKG-INFO +71 -0
  15. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api.egg-info/SOURCES.txt +7 -0
  16. nl2sql_api-0.2.1/tests/test_architecture.py +40 -0
  17. nl2sql_api-0.2.1/tests/test_auth.py +103 -0
  18. nl2sql_api-0.2.1/tests/test_index_status.py +22 -0
  19. nl2sql_api-0.2.1/tests/test_openapi_docs.py +42 -0
  20. nl2sql_api-0.2.1/tests/test_query_response_shape.py +123 -0
  21. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/tests/test_query_routes.py +3 -3
  22. nl2sql_api-0.2.1/tests/test_readme_routes.py +16 -0
  23. nl2sql_api-0.1.2/PKG-INFO +0 -11
  24. nl2sql_api-0.1.2/README.md +0 -85
  25. nl2sql_api-0.1.2/pyproject.toml +0 -24
  26. nl2sql_api-0.1.2/src/nl2sql_api/models/query.py +0 -33
  27. nl2sql_api-0.1.2/src/nl2sql_api/routes/query.py +0 -29
  28. nl2sql_api-0.1.2/src/nl2sql_api/services/query.py +0 -34
  29. nl2sql_api-0.1.2/src/nl2sql_api.egg-info/PKG-INFO +0 -11
  30. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/setup.cfg +0 -0
  31. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/__init__.py +0 -0
  32. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/dependencies.py +0 -0
  33. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/models/__init__.py +0 -0
  34. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/models/datasource.py +0 -0
  35. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/models/llm.py +0 -0
  36. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/models/response.py +0 -0
  37. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/models/schema.py +0 -0
  38. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/routes/__init__.py +0 -0
  39. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/server.py +0 -0
  40. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/services/__init__.py +0 -0
  41. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/services/datasource.py +0 -0
  42. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/services/health.py +0 -0
  43. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api/services/llm.py +0 -0
  44. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api.egg-info/dependency_links.txt +0 -0
  45. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api.egg-info/entry_points.txt +0 -0
  46. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api.egg-info/requires.txt +0 -0
  47. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/src/nl2sql_api.egg-info/top_level.txt +0 -0
  48. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/tests/test_app.py +0 -0
  49. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/tests/test_cors.py +0 -0
  50. {nl2sql_api-0.1.2 → nl2sql_api-0.2.1}/tests/test_services.py +0 -0
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: nl2sql-api
3
+ Version: 0.2.1
4
+ Summary: FastAPI REST service for the nl2sql engine: ask a database questions in English over HTTP
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/nadeem4/nl2sql
7
+ Project-URL: Documentation, https://nadeem4.github.io/nl2sql/
8
+ Project-URL: Source, https://github.com/nadeem4/nl2sql
9
+ Project-URL: Issues, https://github.com/nadeem4/nl2sql/issues
10
+ Project-URL: Demo, https://nadeem4nk-nl2sql-demo.hf.space
11
+ Keywords: nl2sql,text-to-sql,natural language,sql,llm,fastapi,rest api
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Database
20
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
21
+ Requires-Python: >=3.12
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: nl2sql-engine~=0.1
24
+ Requires-Dist: fastapi>=0.100.0
25
+ Requires-Dist: uvicorn>=0.20.0
26
+ Requires-Dist: pydantic>=2.0
27
+ Requires-Dist: python-multipart>=0.0.6
28
+ Requires-Dist: python-dotenv>=1.0.0
29
+
30
+ # NL2SQL API
31
+
32
+ FastAPI REST service for the NL2SQL engine (`nl2sql-engine`). It builds one
33
+ `NL2SQL` engine at startup, from the same env file and configs as the CLI, and
34
+ serves it under `/api/v1`.
35
+
36
+ ## Running the API
37
+
38
+ ```bash
39
+ pip install nl2sql-api
40
+ ENV=demo NL2SQL_API_ROLE=admin nl2sql-api --host 127.0.0.1 --port 8000 [--reload]
41
+ # or: python -m nl2sql_api.server ..., or: uvicorn nl2sql_api.main:app
42
+ ```
43
+
44
+ Start it from the folder holding the env file and configs (the demo's use
45
+ relative paths). Interactive docs: Swagger UI at `/docs`, ReDoc at `/redoc`,
46
+ the schema at `/openapi.json`.
47
+
48
+ The API does not authenticate callers and never takes the RBAC role from the
49
+ request body. Set one of: `NL2SQL_API_ROLE_HEADER` (a header a trusted proxy
50
+ sets), `NL2SQL_API_ROLE` (one static role) or, for local testing only,
51
+ `NL2SQL_API_TRUST_BODY_ROLE=true` (the body's `user_context`; logs a warning).
52
+ With none, `/api/v1/query` answers HTTP 401.
53
+
54
+ ## Endpoints
55
+
56
+ - `POST /api/v1/query` - Ask a question as the configured role
57
+ - `GET /api/v1/health` - Liveness check
58
+ - `GET /api/v1/ready` - Readiness check (does not yet check dependencies)
59
+ - `POST /api/v1/datasource` - Register a datasource in the running process
60
+ - `GET /api/v1/datasource` - List registered datasource ids
61
+ - `GET /api/v1/datasource/{datasource_id}` - Check a datasource is registered
62
+ - `DELETE /api/v1/datasource/{datasource_id}` - Not supported yet (answers `success: false`)
63
+ - `POST /api/v1/llm` - Configure an LLM in the running process
64
+ - `GET /api/v1/llm` - List configured LLMs
65
+ - `GET /api/v1/llm/{llm_name}` - Get one configured LLM
66
+ - `POST /api/v1/index/{datasource_id}` - Index one datasource's schema
67
+ - `POST /api/v1/index-all` - Index every registered datasource
68
+ - `DELETE /api/v1/index` - Clear the vector store
69
+ - `GET /api/v1/index/status` - Index health (status, entries by type, embedding model, problems)
70
+
71
+ Reference: <https://github.com/nadeem4/nl2sql/blob/main/docs/api/rest/index.md>.
@@ -0,0 +1,42 @@
1
+ # NL2SQL API
2
+
3
+ FastAPI REST service for the NL2SQL engine (`nl2sql-engine`). It builds one
4
+ `NL2SQL` engine at startup, from the same env file and configs as the CLI, and
5
+ serves it under `/api/v1`.
6
+
7
+ ## Running the API
8
+
9
+ ```bash
10
+ pip install nl2sql-api
11
+ ENV=demo NL2SQL_API_ROLE=admin nl2sql-api --host 127.0.0.1 --port 8000 [--reload]
12
+ # or: python -m nl2sql_api.server ..., or: uvicorn nl2sql_api.main:app
13
+ ```
14
+
15
+ Start it from the folder holding the env file and configs (the demo's use
16
+ relative paths). Interactive docs: Swagger UI at `/docs`, ReDoc at `/redoc`,
17
+ the schema at `/openapi.json`.
18
+
19
+ The API does not authenticate callers and never takes the RBAC role from the
20
+ request body. Set one of: `NL2SQL_API_ROLE_HEADER` (a header a trusted proxy
21
+ sets), `NL2SQL_API_ROLE` (one static role) or, for local testing only,
22
+ `NL2SQL_API_TRUST_BODY_ROLE=true` (the body's `user_context`; logs a warning).
23
+ With none, `/api/v1/query` answers HTTP 401.
24
+
25
+ ## Endpoints
26
+
27
+ - `POST /api/v1/query` - Ask a question as the configured role
28
+ - `GET /api/v1/health` - Liveness check
29
+ - `GET /api/v1/ready` - Readiness check (does not yet check dependencies)
30
+ - `POST /api/v1/datasource` - Register a datasource in the running process
31
+ - `GET /api/v1/datasource` - List registered datasource ids
32
+ - `GET /api/v1/datasource/{datasource_id}` - Check a datasource is registered
33
+ - `DELETE /api/v1/datasource/{datasource_id}` - Not supported yet (answers `success: false`)
34
+ - `POST /api/v1/llm` - Configure an LLM in the running process
35
+ - `GET /api/v1/llm` - List configured LLMs
36
+ - `GET /api/v1/llm/{llm_name}` - Get one configured LLM
37
+ - `POST /api/v1/index/{datasource_id}` - Index one datasource's schema
38
+ - `POST /api/v1/index-all` - Index every registered datasource
39
+ - `DELETE /api/v1/index` - Clear the vector store
40
+ - `GET /api/v1/index/status` - Index health (status, entries by type, embedding model, problems)
41
+
42
+ Reference: <https://github.com/nadeem4/nl2sql/blob/main/docs/api/rest/index.md>.
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nl2sql-api"
7
+ version = "0.2.1" # x-release-please-version
8
+ description = "FastAPI REST service for the nl2sql engine: ask a database questions in English over HTTP"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ keywords = ["nl2sql", "text-to-sql", "natural language", "sql", "llm", "fastapi", "rest api"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Framework :: FastAPI",
15
+ "Intended Audience :: Developers",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Database",
21
+ "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
22
+ ]
23
+ requires-python = ">=3.12"
24
+ dependencies = [
25
+ "nl2sql-engine~=0.1",
26
+ "fastapi>=0.100.0",
27
+ "uvicorn>=0.20.0",
28
+ "pydantic>=2.0",
29
+ "python-multipart>=0.0.6",
30
+ "python-dotenv>=1.0.0"
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/nadeem4/nl2sql"
35
+ Documentation = "https://nadeem4.github.io/nl2sql/"
36
+ Source = "https://github.com/nadeem4/nl2sql"
37
+ Issues = "https://github.com/nadeem4/nl2sql/issues"
38
+ Demo = "https://nadeem4nk-nl2sql-demo.hf.space"
39
+
40
+ [project.scripts]
41
+ nl2sql-api = "nl2sql_api.server:main"
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+ include = ["nl2sql_api*"]
@@ -0,0 +1,92 @@
1
+ """Where the caller's RBAC role comes from.
2
+
3
+ The engine enforces RBAC; this module only decides which role a request carries.
4
+ It is a seam, not an auth provider: put a proxy that authenticates the caller in
5
+ front of the API and have it set the role header, or pin one static role.
6
+
7
+ Sources, first match wins:
8
+
9
+ 1. ``NL2SQL_API_ROLE_HEADER``: the name of a header a trusted proxy sets, for
10
+ example ``X-NL2SQL-Role``. Comma-separated values are several roles. Only
11
+ configure it when the proxy overwrites that header on every request, or any
12
+ client can pick its own role.
13
+ 2. ``NL2SQL_API_ROLE``: one static role for every request.
14
+ 3. ``user_context`` in the request body, only with the dev flag
15
+ ``NL2SQL_API_TRUST_BODY_ROLE=true``. For local testing: it lets any client
16
+ choose any role.
17
+
18
+ No role is a 401. The settings are read on each request.
19
+ """
20
+
21
+ import logging
22
+ import os
23
+ from typing import Any, List
24
+
25
+ from fastapi import HTTPException, Request, status
26
+ from pydantic import ValidationError
27
+
28
+ from nl2sql import UserContext
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ ROLE_HEADER_ENV = "NL2SQL_API_ROLE_HEADER"
33
+ STATIC_ROLE_ENV = "NL2SQL_API_ROLE"
34
+ TRUST_BODY_ROLE_ENV = "NL2SQL_API_TRUST_BODY_ROLE"
35
+
36
+
37
+ def _body_role_trusted() -> bool:
38
+ return os.getenv(TRUST_BODY_ROLE_ENV, "").strip().lower() in {"1", "true", "yes", "on"}
39
+
40
+
41
+ def _roles(value: str) -> List[str]:
42
+ return [role.strip() for role in value.split(",") if role.strip()]
43
+
44
+
45
+ def warn_if_body_role_trusted() -> None:
46
+ """Log a warning when the dev flag lets clients choose their own role."""
47
+ if _body_role_trusted():
48
+ logger.warning(
49
+ "%s is on: the RBAC role is taken from the request body, so any client "
50
+ "can choose any role. Use it for local testing only.",
51
+ TRUST_BODY_ROLE_ENV,
52
+ )
53
+
54
+
55
+ async def _body_user_context(request: Request) -> Any:
56
+ try:
57
+ body = await request.json()
58
+ except Exception:
59
+ return None
60
+ return body.get("user_context") if isinstance(body, dict) else None
61
+
62
+
63
+ async def get_user_context(request: Request) -> UserContext:
64
+ """Return the caller's ``UserContext``, or raise 401 when no role is configured."""
65
+ header = os.getenv(ROLE_HEADER_ENV, "").strip()
66
+ if header:
67
+ roles = _roles(request.headers.get(header, ""))
68
+ if roles:
69
+ return UserContext(roles=roles)
70
+
71
+ static_role = os.getenv(STATIC_ROLE_ENV, "").strip()
72
+ if static_role:
73
+ return UserContext(roles=_roles(static_role))
74
+
75
+ if _body_role_trusted():
76
+ payload = await _body_user_context(request)
77
+ if isinstance(payload, dict):
78
+ try:
79
+ user_context = UserContext(**payload)
80
+ except ValidationError:
81
+ user_context = None
82
+ if user_context and user_context.roles:
83
+ return user_context
84
+
85
+ raise HTTPException(
86
+ status_code=status.HTTP_401_UNAUTHORIZED,
87
+ detail=(
88
+ "No role for this request. Configure a trusted proxy header "
89
+ f"({ROLE_HEADER_ENV}) or a static role ({STATIC_ROLE_ENV}). The body's "
90
+ f"user_context is ignored unless {TRUST_BODY_ROLE_ENV}=true (local testing only)."
91
+ ),
92
+ )
@@ -6,20 +6,22 @@ from importlib.metadata import PackageNotFoundError, version
6
6
 
7
7
  from .routes import query, health, datasource, llm, indexing
8
8
  from fastapi.middleware.cors import CORSMiddleware
9
- from nl2sql import NL2SQL
10
- from nl2sql.common.logger import configure_logging
11
- from nl2sql.common.settings import settings
9
+ from nl2sql import NL2SQL, configure_logging
10
+ from .auth import warn_if_body_role_trusted
12
11
 
13
12
 
14
13
  @asynccontextmanager
15
14
  async def lifespan(app: FastAPI):
16
15
  # The library no longer configures logging on import, so the application
17
- # entry point owns it - before anything that logs is constructed.
18
- configure_logging(
19
- level="INFO",
20
- json_format=(settings.observability_exporter == "otlp"),
21
- )
22
- app.state.engine = NL2SQL()
16
+ # entry point owns it - before anything that logs is constructed. The
17
+ # exporter is read through the facade once the engine has loaded settings;
18
+ # an OTLP deployment then switches to JSON logs.
19
+ configure_logging(level="INFO")
20
+ engine = NL2SQL()
21
+ if engine.get_setting("observability_exporter") == "otlp":
22
+ configure_logging(level="INFO", json_format=True)
23
+ warn_if_body_role_trusted()
24
+ app.state.engine = engine
23
25
  yield
24
26
 
25
27
 
@@ -0,0 +1,34 @@
1
+ from typing import Any, Dict, List, Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from nl2sql import QueryResult, SubQueryResult
6
+
7
+
8
+ class QueryRequest(BaseModel):
9
+ natural_language: str
10
+ datasource_id: Optional[str] = None
11
+ execute: bool = True
12
+ user_context: Optional[Dict[str, Any]] = Field(
13
+ default=None,
14
+ description=(
15
+ "Ignored unless NL2SQL_API_TRUST_BODY_ROLE=true (local testing only); "
16
+ "the role otherwise comes from the API's auth settings."
17
+ ),
18
+ )
19
+
20
+
21
+ class SubQueryResponse(SubQueryResult):
22
+ """One decomposed sub-query: its plan, validation checks, SQL and row sample."""
23
+
24
+
25
+ class QueryResponse(QueryResult):
26
+ """The engine's ``QueryResult``, served as is.
27
+
28
+ It derives from ``nl2sql.QueryResult``, so the HTTP response cannot drift
29
+ from what the engine returns. ``sub_queries[].rows`` carries a capped
30
+ sample only; the full result set lives in artifact storage, addressable
31
+ through ``artifact_refs``. ``trace_path`` is where the run's trace was
32
+ written on the server, or None.
33
+ """
34
+ sub_queries: List[SubQueryResponse] = Field(default_factory=list)
@@ -5,37 +5,43 @@ from nl2sql_api.models.datasource import DatasourceRequest, DatasourceResponse
5
5
  from nl2sql_api.dependencies import get_datasource_service
6
6
  from nl2sql_api.services import DatasourceService
7
7
 
8
- router = APIRouter()
8
+ router = APIRouter(tags=["datasources"])
9
9
 
10
10
  DatasourceSvc = Annotated[DatasourceService, Depends(get_datasource_service)]
11
11
 
12
12
 
13
- @router.post("/datasource", response_model=DatasourceResponse)
13
+ @router.post("/datasource", response_model=DatasourceResponse, summary="Add a datasource")
14
14
  def add_datasource(
15
15
  payload: DatasourceRequest,
16
16
  service: DatasourceSvc,
17
17
  ):
18
+ """Register a datasource in this process from `config`, shaped like one entry of
19
+ `configs/datasources.yaml`. It is not written to the config file and not indexed:
20
+ call `POST /api/v1/index/{datasource_id}` next.
21
+ """
18
22
  try:
19
23
  return service.add_datasource(payload)
20
24
  except Exception as e:
21
25
  raise HTTPException(status_code=500, detail=str(e))
22
26
 
23
27
 
24
- @router.get("/datasource", response_model=Dict[str, Any])
28
+ @router.get("/datasource", response_model=Dict[str, Any], summary="List datasources")
25
29
  def list_datasources(
26
30
  service: DatasourceSvc,
27
31
  ):
32
+ """The ids of the registered datasources, as `{"datasources": [...]}`."""
28
33
  try:
29
34
  return {"datasources": service.list_datasources()}
30
35
  except Exception as e:
31
36
  raise HTTPException(status_code=500, detail=str(e))
32
37
 
33
38
 
34
- @router.get("/datasource/{datasource_id}", response_model=Dict[str, Any])
39
+ @router.get("/datasource/{datasource_id}", response_model=Dict[str, Any], summary="Check a datasource exists")
35
40
  def get_datasource(
36
41
  datasource_id: str,
37
42
  service: DatasourceSvc,
38
43
  ):
44
+ """`{"datasource_id": ..., "exists": true}`, or 404 when it is not registered. No other details yet."""
39
45
  try:
40
46
  return service.get_datasource(datasource_id)
41
47
  except ValueError as e:
@@ -44,11 +50,12 @@ def get_datasource(
44
50
  raise HTTPException(status_code=500, detail=str(e))
45
51
 
46
52
 
47
- @router.delete("/datasource/{datasource_id}", response_model=Dict[str, Any])
53
+ @router.delete("/datasource/{datasource_id}", response_model=Dict[str, Any], summary="Remove a datasource (not supported)")
48
54
  def remove_datasource(
49
55
  datasource_id: str,
50
56
  service: DatasourceSvc,
51
57
  ):
58
+ """Not supported by the engine yet: answers `success: false` (404 for an unknown id)."""
52
59
  try:
53
60
  return service.remove_datasource(datasource_id)
54
61
  except ValueError as e:
@@ -4,19 +4,21 @@ from nl2sql_api.models.response import SuccessResponse
4
4
  from nl2sql_api.dependencies import get_health_service
5
5
  from nl2sql_api.services import HealthService
6
6
 
7
- router = APIRouter()
7
+ router = APIRouter(tags=["health"])
8
8
 
9
9
  HealthSvc = Annotated[HealthService, Depends(get_health_service)]
10
10
 
11
- @router.get("/health", response_model=SuccessResponse)
11
+ @router.get("/health", response_model=SuccessResponse, summary="Liveness check")
12
12
  async def health_check(
13
13
  service: HealthSvc
14
14
  ):
15
+ """Returns `success: true` while the process is serving. Checks nothing else."""
15
16
  return service.health_check()
16
17
 
17
18
 
18
- @router.get("/ready", response_model=SuccessResponse)
19
+ @router.get("/ready", response_model=SuccessResponse, summary="Readiness check")
19
20
  async def readiness_check(
20
21
  service: HealthSvc,
21
22
  ):
23
+ """Returns `success: true`. It does not yet check datasources, the LLM or the index."""
22
24
  return service.readiness_check()
@@ -3,15 +3,16 @@ from typing import Dict, Any, Annotated
3
3
 
4
4
  from nl2sql_api.dependencies import get_indexing_service
5
5
  from nl2sql_api.services import IndexingService
6
- router = APIRouter()
6
+ router = APIRouter(tags=["indexing"])
7
7
 
8
8
  IndexingSvc = Annotated[IndexingService, Depends(get_indexing_service)]
9
9
 
10
- @router.post("/index/{datasource_id}", response_model=Dict[str, Any])
10
+ @router.post("/index/{datasource_id}", response_model=Dict[str, Any], summary="Index one datasource")
11
11
  def index_datasource(
12
12
  datasource_id: str,
13
13
  service: IndexingSvc
14
14
  ):
15
+ """Index one datasource's schema into the vector store; returns the indexing stats."""
15
16
  try:
16
17
  result = service.index_datasource(datasource_id)
17
18
 
@@ -28,10 +29,11 @@ def index_datasource(
28
29
  )
29
30
 
30
31
 
31
- @router.post("/index-all", response_model=Dict[str, Any])
32
+ @router.post("/index-all", response_model=Dict[str, Any], summary="Index every datasource")
32
33
  def index_all_datasources(
33
34
  service: IndexingSvc
34
35
  ):
36
+ """Index every registered datasource; returns the stats per datasource."""
35
37
  try:
36
38
  results = service.index_all_datasources()
37
39
 
@@ -47,10 +49,11 @@ def index_all_datasources(
47
49
  )
48
50
 
49
51
 
50
- @router.delete("/index", response_model=Dict[str, Any])
52
+ @router.delete("/index", response_model=Dict[str, Any], summary="Clear the index")
51
53
  def clear_index(
52
54
  service: IndexingSvc
53
55
  ):
56
+ """Remove every entry from the vector store. Questions need a re-index before they can be answered."""
54
57
  try:
55
58
  return service.clear_index()
56
59
  except Exception as e:
@@ -60,10 +63,13 @@ def clear_index(
60
63
  )
61
64
 
62
65
 
63
- @router.get("/index/status", response_model=Dict[str, Any])
66
+ @router.get("/index/status", response_model=Dict[str, Any], summary="Index status")
64
67
  def get_index_status(
65
68
  service: IndexingSvc
66
69
  ):
70
+ """The vector index's health: `status` (`ok`, `empty`, `stale` or `missing`), entry counts by
71
+ type, when it was built, the embedding model, one entry per registered datasource and any
72
+ problems found. Read from the live index; the same report the playground shows."""
67
73
  try:
68
74
  return service.get_index_status()
69
75
  except Exception as e:
@@ -5,36 +5,41 @@ from nl2sql_api.models.llm import LLMRequest, LLMResponse
5
5
  from nl2sql_api.dependencies import get_llm_service
6
6
  from nl2sql_api.services import LLMService
7
7
 
8
- router = APIRouter()
8
+ router = APIRouter(tags=["llm"])
9
9
 
10
10
  LLMSvc = Annotated[LLMService, Depends(get_llm_service)]
11
11
 
12
- @router.post("/llm", response_model=LLMResponse)
12
+ @router.post("/llm", response_model=LLMResponse, summary="Configure an LLM")
13
13
  def configure_llm(
14
14
  payload: LLMRequest,
15
15
  service: LLMSvc,
16
16
  ):
17
+ """Register an LLM in this process from `config` (`name` defaults to `default`).
18
+ It is not written to `configs/llm.yaml`.
19
+ """
17
20
  try:
18
21
  return service.configure_llm(payload)
19
22
  except Exception as e:
20
23
  raise HTTPException(status_code=500, detail=str(e))
21
24
 
22
25
 
23
- @router.get("/llm", response_model=Dict[str, Any])
26
+ @router.get("/llm", response_model=Dict[str, Any], summary="List LLMs")
24
27
  def list_llms(
25
28
  service: LLMSvc,
26
29
  ):
30
+ """The configured LLMs, as `{"llms": ...}`."""
27
31
  try:
28
32
  return {"llms": service.list_llms()}
29
33
  except Exception as e:
30
34
  raise HTTPException(status_code=500, detail=str(e))
31
35
 
32
36
 
33
- @router.get("/llm/{llm_name}", response_model=Dict[str, Any])
37
+ @router.get("/llm/{llm_name}", response_model=Dict[str, Any], summary="Get an LLM")
34
38
  def get_llm(
35
39
  llm_name: str,
36
40
  service: LLMSvc,
37
41
  ):
42
+ """One configured LLM by name."""
38
43
  try:
39
44
  return service.get_llm(llm_name)
40
45
  except Exception as e:
@@ -0,0 +1,48 @@
1
+ import logging
2
+
3
+ from fastapi import APIRouter, HTTPException, Depends
4
+ from typing import Annotated
5
+ from nl2sql import UserContext
6
+ from nl2sql_api import auth
7
+ from nl2sql_api.models.query import QueryRequest, QueryResponse
8
+ from nl2sql_api.dependencies import get_query_service
9
+ from nl2sql_api.services import QueryService
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ router = APIRouter(tags=["query"])
14
+
15
+ QuerySvc = Annotated[QueryService, Depends(get_query_service)]
16
+ Caller = Annotated[UserContext, Depends(auth.get_user_context)]
17
+
18
+
19
+ # Synchronous on purpose: the pipeline performs blocking LLM and database calls,
20
+ # so Starlette runs this handler in its threadpool instead of on the event loop.
21
+ @router.post(
22
+ "/query",
23
+ response_model=QueryResponse,
24
+ summary="Ask a question",
25
+ responses={401: {"description": "No role for this request (see the auth settings)."}},
26
+ )
27
+ def execute_query(
28
+ payload: QueryRequest,
29
+ service: QuerySvc,
30
+ user_context: Caller,
31
+ ):
32
+ """Run a natural-language question through the pipeline.
33
+
34
+ Returns, per sub-query, the plan, the validation checks, the SQL and a capped row
35
+ sample, plus the written answer, errors, timings and token usage. A pipeline
36
+ failure (a refusal included) comes back with status 200 and `status: "error"`.
37
+ `execute: false` plans and validates without running SQL. The RBAC role comes
38
+ from a trusted proxy header (`NL2SQL_API_ROLE_HEADER`) or a static role
39
+ (`NL2SQL_API_ROLE`); the body's `user_context` counts only with the dev flag
40
+ `NL2SQL_API_TRUST_BODY_ROLE=true`. No role is a 401.
41
+ """
42
+ try:
43
+ return service.execute_query(payload, user_context)
44
+ except Exception:
45
+ # Pipeline failures are reported in QueryResponse.errors with a 200; reaching
46
+ # here means something genuinely unexpected broke.
47
+ logger.exception("Unexpected failure while executing query")
48
+ raise HTTPException(status_code=500, detail="Failed to execute query.")
@@ -20,11 +20,5 @@ class IndexingService:
20
20
  return {"success": True, "message": "Index cleared successfully"}
21
21
 
22
22
  def get_index_status(self) -> Dict[str, Any]:
23
- """Get the status of the index."""
24
- # This would typically return information about the vector store
25
- # For now, we'll return a placeholder
26
- return {
27
- "status": "operational",
28
- "indexed_datasources": self.engine.list_datasources(),
29
- "total_indexes": len(self.engine.list_datasources()) # Placeholder
30
- }
23
+ """The vector index's health, as ``NL2SQL.index_health`` reports it."""
24
+ return self.engine.index_health()
@@ -0,0 +1,17 @@
1
+ from nl2sql import NL2SQL, UserContext
2
+ from nl2sql_api.models.query import QueryRequest, QueryResponse
3
+
4
+
5
+ class QueryService:
6
+ def __init__(self, engine: NL2SQL):
7
+ self.engine = engine
8
+
9
+ def execute_query(self, request: QueryRequest, user_context: UserContext) -> QueryResponse:
10
+ """Run the question as ``user_context``, which the auth dependency supplies."""
11
+ result = self.engine.run_query(
12
+ request.natural_language,
13
+ datasource_id=request.datasource_id,
14
+ execute=request.execute,
15
+ user_context=user_context,
16
+ )
17
+ return QueryResponse.model_validate(result.model_dump())
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: nl2sql-api
3
+ Version: 0.2.1
4
+ Summary: FastAPI REST service for the nl2sql engine: ask a database questions in English over HTTP
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/nadeem4/nl2sql
7
+ Project-URL: Documentation, https://nadeem4.github.io/nl2sql/
8
+ Project-URL: Source, https://github.com/nadeem4/nl2sql
9
+ Project-URL: Issues, https://github.com/nadeem4/nl2sql/issues
10
+ Project-URL: Demo, https://nadeem4nk-nl2sql-demo.hf.space
11
+ Keywords: nl2sql,text-to-sql,natural language,sql,llm,fastapi,rest api
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Database
20
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
21
+ Requires-Python: >=3.12
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: nl2sql-engine~=0.1
24
+ Requires-Dist: fastapi>=0.100.0
25
+ Requires-Dist: uvicorn>=0.20.0
26
+ Requires-Dist: pydantic>=2.0
27
+ Requires-Dist: python-multipart>=0.0.6
28
+ Requires-Dist: python-dotenv>=1.0.0
29
+
30
+ # NL2SQL API
31
+
32
+ FastAPI REST service for the NL2SQL engine (`nl2sql-engine`). It builds one
33
+ `NL2SQL` engine at startup, from the same env file and configs as the CLI, and
34
+ serves it under `/api/v1`.
35
+
36
+ ## Running the API
37
+
38
+ ```bash
39
+ pip install nl2sql-api
40
+ ENV=demo NL2SQL_API_ROLE=admin nl2sql-api --host 127.0.0.1 --port 8000 [--reload]
41
+ # or: python -m nl2sql_api.server ..., or: uvicorn nl2sql_api.main:app
42
+ ```
43
+
44
+ Start it from the folder holding the env file and configs (the demo's use
45
+ relative paths). Interactive docs: Swagger UI at `/docs`, ReDoc at `/redoc`,
46
+ the schema at `/openapi.json`.
47
+
48
+ The API does not authenticate callers and never takes the RBAC role from the
49
+ request body. Set one of: `NL2SQL_API_ROLE_HEADER` (a header a trusted proxy
50
+ sets), `NL2SQL_API_ROLE` (one static role) or, for local testing only,
51
+ `NL2SQL_API_TRUST_BODY_ROLE=true` (the body's `user_context`; logs a warning).
52
+ With none, `/api/v1/query` answers HTTP 401.
53
+
54
+ ## Endpoints
55
+
56
+ - `POST /api/v1/query` - Ask a question as the configured role
57
+ - `GET /api/v1/health` - Liveness check
58
+ - `GET /api/v1/ready` - Readiness check (does not yet check dependencies)
59
+ - `POST /api/v1/datasource` - Register a datasource in the running process
60
+ - `GET /api/v1/datasource` - List registered datasource ids
61
+ - `GET /api/v1/datasource/{datasource_id}` - Check a datasource is registered
62
+ - `DELETE /api/v1/datasource/{datasource_id}` - Not supported yet (answers `success: false`)
63
+ - `POST /api/v1/llm` - Configure an LLM in the running process
64
+ - `GET /api/v1/llm` - List configured LLMs
65
+ - `GET /api/v1/llm/{llm_name}` - Get one configured LLM
66
+ - `POST /api/v1/index/{datasource_id}` - Index one datasource's schema
67
+ - `POST /api/v1/index-all` - Index every registered datasource
68
+ - `DELETE /api/v1/index` - Clear the vector store
69
+ - `GET /api/v1/index/status` - Index health (status, entries by type, embedding model, problems)
70
+
71
+ Reference: <https://github.com/nadeem4/nl2sql/blob/main/docs/api/rest/index.md>.