nl2sql-api 0.1.2__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nl2sql_api/auth.py +92 -0
- nl2sql_api/main.py +11 -9
- nl2sql_api/models/query.py +20 -19
- nl2sql_api/routes/datasource.py +12 -5
- nl2sql_api/routes/health.py +5 -3
- nl2sql_api/routes/indexing.py +11 -5
- nl2sql_api/routes/llm.py +9 -4
- nl2sql_api/routes/query.py +22 -3
- nl2sql_api/services/indexing.py +2 -8
- nl2sql_api/services/query.py +4 -21
- nl2sql_api-0.2.1.dist-info/METADATA +71 -0
- {nl2sql_api-0.1.2.dist-info → nl2sql_api-0.2.1.dist-info}/RECORD +15 -14
- nl2sql_api-0.1.2.dist-info/METADATA +0 -11
- {nl2sql_api-0.1.2.dist-info → nl2sql_api-0.2.1.dist-info}/WHEEL +0 -0
- {nl2sql_api-0.1.2.dist-info → nl2sql_api-0.2.1.dist-info}/entry_points.txt +0 -0
- {nl2sql_api-0.1.2.dist-info → nl2sql_api-0.2.1.dist-info}/top_level.txt +0 -0
nl2sql_api/auth.py
ADDED
|
@@ -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
|
+
)
|
nl2sql_api/main.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
)
|
|
22
|
-
|
|
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
|
|
nl2sql_api/models/query.py
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
|
|
1
3
|
from pydantic import BaseModel, Field
|
|
2
|
-
|
|
4
|
+
|
|
5
|
+
from nl2sql import QueryResult, SubQueryResult
|
|
3
6
|
|
|
4
7
|
|
|
5
8
|
class QueryRequest(BaseModel):
|
|
6
9
|
natural_language: str
|
|
7
10
|
datasource_id: Optional[str] = None
|
|
8
11
|
execute: bool = True
|
|
9
|
-
user_context: Optional[Dict[str, Any]] =
|
|
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
|
+
)
|
|
10
19
|
|
|
11
20
|
|
|
12
|
-
class SubQueryResponse(
|
|
13
|
-
"""One decomposed sub-query
|
|
14
|
-
id: str = ""
|
|
15
|
-
intent: str = ""
|
|
16
|
-
sql: str = ""
|
|
17
|
-
datasource_id: str = ""
|
|
18
|
-
schema_version: str = ""
|
|
21
|
+
class SubQueryResponse(SubQueryResult):
|
|
22
|
+
"""One decomposed sub-query: its plan, validation checks, SQL and row sample."""
|
|
19
23
|
|
|
20
24
|
|
|
21
|
-
class QueryResponse(
|
|
22
|
-
"""
|
|
25
|
+
class QueryResponse(QueryResult):
|
|
26
|
+
"""The engine's ``QueryResult``, served as is.
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
26
33
|
"""
|
|
27
34
|
sub_queries: List[SubQueryResponse] = Field(default_factory=list)
|
|
28
|
-
final_answer: Optional[Dict[str, Any]] = None
|
|
29
|
-
errors: List[Dict[str, Any]] = Field(default_factory=list)
|
|
30
|
-
trace_id: Optional[str] = None
|
|
31
|
-
reasoning: List[Dict[str, Any]] = Field(default_factory=list)
|
|
32
|
-
warnings: List[Dict[str, Any]] = Field(default_factory=list)
|
|
33
|
-
artifact_refs: Dict[str, Dict[str, Any]] = Field(default_factory=dict)
|
nl2sql_api/routes/datasource.py
CHANGED
|
@@ -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:
|
nl2sql_api/routes/health.py
CHANGED
|
@@ -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()
|
nl2sql_api/routes/indexing.py
CHANGED
|
@@ -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:
|
nl2sql_api/routes/llm.py
CHANGED
|
@@ -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:
|
nl2sql_api/routes/query.py
CHANGED
|
@@ -2,26 +2,45 @@ import logging
|
|
|
2
2
|
|
|
3
3
|
from fastapi import APIRouter, HTTPException, Depends
|
|
4
4
|
from typing import Annotated
|
|
5
|
+
from nl2sql import UserContext
|
|
6
|
+
from nl2sql_api import auth
|
|
5
7
|
from nl2sql_api.models.query import QueryRequest, QueryResponse
|
|
6
8
|
from nl2sql_api.dependencies import get_query_service
|
|
7
9
|
from nl2sql_api.services import QueryService
|
|
8
10
|
|
|
9
11
|
logger = logging.getLogger(__name__)
|
|
10
12
|
|
|
11
|
-
router = APIRouter()
|
|
13
|
+
router = APIRouter(tags=["query"])
|
|
12
14
|
|
|
13
15
|
QuerySvc = Annotated[QueryService, Depends(get_query_service)]
|
|
16
|
+
Caller = Annotated[UserContext, Depends(auth.get_user_context)]
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
# Synchronous on purpose: the pipeline performs blocking LLM and database calls,
|
|
17
20
|
# so Starlette runs this handler in its threadpool instead of on the event loop.
|
|
18
|
-
@router.post(
|
|
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
|
+
)
|
|
19
27
|
def execute_query(
|
|
20
28
|
payload: QueryRequest,
|
|
21
29
|
service: QuerySvc,
|
|
30
|
+
user_context: Caller,
|
|
22
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
|
+
"""
|
|
23
42
|
try:
|
|
24
|
-
return service.execute_query(payload)
|
|
43
|
+
return service.execute_query(payload, user_context)
|
|
25
44
|
except Exception:
|
|
26
45
|
# Pipeline failures are reported in QueryResponse.errors with a 200; reaching
|
|
27
46
|
# here means something genuinely unexpected broke.
|
nl2sql_api/services/indexing.py
CHANGED
|
@@ -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
|
-
"""
|
|
24
|
-
|
|
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()
|
nl2sql_api/services/query.py
CHANGED
|
@@ -1,34 +1,17 @@
|
|
|
1
|
-
from nl2sql import NL2SQL
|
|
1
|
+
from nl2sql import NL2SQL, UserContext
|
|
2
2
|
from nl2sql_api.models.query import QueryRequest, QueryResponse
|
|
3
|
-
from nl2sql.auth.models import UserContext
|
|
4
3
|
|
|
5
4
|
|
|
6
5
|
class QueryService:
|
|
7
6
|
def __init__(self, engine: NL2SQL):
|
|
8
7
|
self.engine = engine
|
|
9
8
|
|
|
10
|
-
def execute_query(self, request: QueryRequest) -> QueryResponse:
|
|
11
|
-
|
|
12
|
-
user_context = None
|
|
13
|
-
if request.user_context:
|
|
14
|
-
user_context = UserContext(**request.user_context)
|
|
15
|
-
|
|
9
|
+
def execute_query(self, request: QueryRequest, user_context: UserContext) -> QueryResponse:
|
|
10
|
+
"""Run the question as ``user_context``, which the auth dependency supplies."""
|
|
16
11
|
result = self.engine.run_query(
|
|
17
12
|
request.natural_language,
|
|
18
13
|
datasource_id=request.datasource_id,
|
|
19
14
|
execute=request.execute,
|
|
20
15
|
user_context=user_context,
|
|
21
16
|
)
|
|
22
|
-
|
|
23
|
-
return QueryResponse(
|
|
24
|
-
sub_queries=[sub_query.model_dump() for sub_query in result.sub_queries],
|
|
25
|
-
final_answer=result.final_answer,
|
|
26
|
-
errors=result.errors,
|
|
27
|
-
trace_id=result.trace_id,
|
|
28
|
-
reasoning=result.reasoning,
|
|
29
|
-
warnings=result.warnings,
|
|
30
|
-
artifact_refs={
|
|
31
|
-
node_id: ref.model_dump(mode="json")
|
|
32
|
-
for node_id, ref in result.artifact_refs.items()
|
|
33
|
-
},
|
|
34
|
-
)
|
|
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>.
|
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
nl2sql_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
nl2sql_api/auth.py,sha256=iLiRWaHJQce_8oWQ22JM0lABf-z91Ai9GaEDe-3FIKY,3211
|
|
2
3
|
nl2sql_api/dependencies.py,sha256=Vob6_ylRqX7A9mtLAk6P6C-dYaVj-sD6x_bgT1g7650,890
|
|
3
|
-
nl2sql_api/main.py,sha256=
|
|
4
|
+
nl2sql_api/main.py,sha256=0O_dpCbhqc-sCOQpz25X6mf9HSNiYfVO2_arc8JrXIg,2003
|
|
4
5
|
nl2sql_api/server.py,sha256=spmORkrbdnRT5F0ayW5JqE60xaAEGItS-f_GYGvjzjc,787
|
|
5
6
|
nl2sql_api/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
7
|
nl2sql_api/models/datasource.py,sha256=05fWM3Oo3m86NsjwCU1fCyIVDRA6xU-WT2ayGwRO0us,247
|
|
7
8
|
nl2sql_api/models/llm.py,sha256=8WEtUg0vkW7v3fzsuMhhcUvHjrCrc0tH0Hy36devAhk,228
|
|
8
|
-
nl2sql_api/models/query.py,sha256=
|
|
9
|
+
nl2sql_api/models/query.py,sha256=u_UGLcChK9_lrXV4yS9OVI4QmhWNbtjPUHCdm-nZ7dU,1154
|
|
9
10
|
nl2sql_api/models/response.py,sha256=unon5gOq3FeLKULMNjgP5dmy3b_eF9b9EybuhbL4MBU,320
|
|
10
11
|
nl2sql_api/models/schema.py,sha256=9npDcphba7ZZ19d11wQKv6iNcwBwuedrBNleNi5aejE,251
|
|
11
12
|
nl2sql_api/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
nl2sql_api/routes/datasource.py,sha256=
|
|
13
|
-
nl2sql_api/routes/health.py,sha256=
|
|
14
|
-
nl2sql_api/routes/indexing.py,sha256=
|
|
15
|
-
nl2sql_api/routes/llm.py,sha256=
|
|
16
|
-
nl2sql_api/routes/query.py,sha256=
|
|
13
|
+
nl2sql_api/routes/datasource.py,sha256=OMVde4EO5E51bpjZlexN3x743LJcoRnsxNMNoJCHcLw,2398
|
|
14
|
+
nl2sql_api/routes/health.py,sha256=aisKvQsvRDiZHQ1oP1igIo1xq-70fDR2ABhmBh5uRW8,845
|
|
15
|
+
nl2sql_api/routes/indexing.py,sha256=PJYDjoehrEZLfID7wMWCqbacTv7dHDhq23jJA5E6cx8,2711
|
|
16
|
+
nl2sql_api/routes/llm.py,sha256=rhTf8pkXQxmY4dZ6DREqxqy4oT0vs1f9WO8SgiJVhuU,1388
|
|
17
|
+
nl2sql_api/routes/query.py,sha256=NxNtKMHI5PzCpsjIrvlpeSsg18pZl5wt-ZhRvp8OQhs,1971
|
|
17
18
|
nl2sql_api/services/__init__.py,sha256=wai2Xe8XI6FIbEqrfwQD2Ghm19YuGM0l1xzzgAjT9IM,296
|
|
18
19
|
nl2sql_api/services/datasource.py,sha256=XHioCMbZw0YDnFzYYxSSXd7R-T6ObQf6t0SZP510qx8,1753
|
|
19
20
|
nl2sql_api/services/health.py,sha256=hmv4IN4HqXqJXRHbdYuQdPnwUYUJ84Shdsa6-4CsTqY,579
|
|
20
|
-
nl2sql_api/services/indexing.py,sha256=
|
|
21
|
+
nl2sql_api/services/indexing.py,sha256=mIyoppv83-MJP0xAQXvDLqKwWZ-2V6l1VHjx15Py6Oc,907
|
|
21
22
|
nl2sql_api/services/llm.py,sha256=U4GmHI4Np2kuELEqYS1xI0lnApYRY2x8Nw8C9-mo-W8,843
|
|
22
|
-
nl2sql_api/services/query.py,sha256=
|
|
23
|
-
nl2sql_api-0.1.
|
|
24
|
-
nl2sql_api-0.1.
|
|
25
|
-
nl2sql_api-0.1.
|
|
26
|
-
nl2sql_api-0.1.
|
|
27
|
-
nl2sql_api-0.1.
|
|
23
|
+
nl2sql_api/services/query.py,sha256=1-jzT2VN6lQVmVTEsdK5PAAtjFck7lXu0dfetXKd63k,657
|
|
24
|
+
nl2sql_api-0.2.1.dist-info/METADATA,sha256=1NniqpaL9rRId8vOn4S1v4hWLLAhCBvFyNiULro1gqA,3175
|
|
25
|
+
nl2sql_api-0.2.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
26
|
+
nl2sql_api-0.2.1.dist-info/entry_points.txt,sha256=p5MLHkru6-8adKBwwpySsJgAsEvKHF8LDedWdw9w23M,54
|
|
27
|
+
nl2sql_api-0.2.1.dist-info/top_level.txt,sha256=c_OEDdzXMoZTtOaZS8l4qKaNF0Cz5AesYzLye2pOSvA,11
|
|
28
|
+
nl2sql_api-0.2.1.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: nl2sql-api
|
|
3
|
-
Version: 0.1.2
|
|
4
|
-
Summary: API layer for NL2SQL engine
|
|
5
|
-
Requires-Python: >=3.9
|
|
6
|
-
Requires-Dist: nl2sql-engine~=0.1
|
|
7
|
-
Requires-Dist: fastapi>=0.100.0
|
|
8
|
-
Requires-Dist: uvicorn>=0.20.0
|
|
9
|
-
Requires-Dist: pydantic>=2.0
|
|
10
|
-
Requires-Dist: python-multipart>=0.0.6
|
|
11
|
-
Requires-Dist: python-dotenv>=1.0.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|