gaard-api 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ from fastapi import APIRouter
2
+
3
+ from gaard_connectors.sqlalchemy.introspector import SQLAlchemySchemaIntrospector
4
+ from gaard_core.schema.context import SchemaContextService
5
+ from gaard_core.schema.models import DatabaseSchema
6
+
7
+ from gaard_api.admin.services import (
8
+ get_datasource_schema_context_safe,
9
+ selected_schema_from_cache,
10
+ )
11
+ from gaard_api.core.schema_cache import schema_context_cache
12
+ from gaard_api.core.settings import settings
13
+
14
+ router = APIRouter()
15
+
16
+
17
+ def get_schema_cache_key(database_url: str | None = None, sql_dialect: str | None = None) -> str:
18
+ return (
19
+ f"{sql_dialect or settings.gaard_sql_dialect}:"
20
+ f"{database_url or settings.gaard_datasource_url}"
21
+ )
22
+
23
+
24
+ @router.get("/schema", response_model=DatabaseSchema)
25
+ def get_schema() -> DatabaseSchema:
26
+ datasource_context = get_datasource_schema_context_safe()
27
+
28
+ if datasource_context is not None:
29
+ _connector, schema_cache = datasource_context
30
+ return selected_schema_from_cache(schema_cache)
31
+
32
+ introspector = SQLAlchemySchemaIntrospector(
33
+ database_url=settings.gaard_datasource_url,
34
+ )
35
+
36
+ service = SchemaContextService(
37
+ introspector=introspector,
38
+ cache=schema_context_cache,
39
+ )
40
+
41
+ context = service.get_schema_context(get_schema_cache_key())
42
+
43
+ return context.database_schema
44
+
45
+
46
+ @router.delete("/schema/cache")
47
+ def invalidate_schema_cache() -> dict[str, str]:
48
+ datasource_context = get_datasource_schema_context_safe()
49
+
50
+ if datasource_context is not None:
51
+ connector, _schema_cache = datasource_context
52
+ schema_context_cache.invalidate(
53
+ get_schema_cache_key(connector.database_url, connector.sql_dialect)
54
+ )
55
+ else:
56
+ schema_context_cache.invalidate(get_schema_cache_key())
57
+
58
+ return {
59
+ "status": "invalidated",
60
+ }
gaard_api/cli.py ADDED
@@ -0,0 +1,19 @@
1
+ import argparse
2
+ from importlib.metadata import entry_points
3
+
4
+
5
+ def main() -> None:
6
+ parser = argparse.ArgumentParser(prog="gaard")
7
+ subparsers = parser.add_subparsers(dest="command", required=True)
8
+
9
+ commands = {}
10
+
11
+ for ep in entry_points(group="gaard.commands"):
12
+ command_func = ep.load()
13
+ commands[ep.name] = command_func
14
+ command_func(subparsers)
15
+
16
+ args = parser.parse_args()
17
+
18
+ if args.command in commands:
19
+ args.func(args)
@@ -0,0 +1,18 @@
1
+ import uvicorn
2
+
3
+
4
+ def register(subparsers):
5
+ parser = subparsers.add_parser("admin")
6
+ parser.add_argument("--host", default="127.0.0.1")
7
+ parser.add_argument("--port", type=int, default=8000)
8
+ parser.add_argument("--reload", action="store_true")
9
+ parser.set_defaults(func=run_admin)
10
+
11
+
12
+ def run_admin(args):
13
+ uvicorn.run(
14
+ "gaard_api.main:app",
15
+ host=args.host,
16
+ port=args.port,
17
+ reload=args.reload,
18
+ )
File without changes
@@ -0,0 +1,18 @@
1
+ from fastapi import FastAPI, Request
2
+ from fastapi.responses import JSONResponse
3
+
4
+ from gaard_core.errors import GaardError
5
+
6
+
7
+ def register_error_handlers(app: FastAPI) -> None:
8
+ @app.exception_handler(GaardError)
9
+ async def handle_gaard_error(request: Request, exc: GaardError) -> JSONResponse:
10
+ return JSONResponse(
11
+ status_code=exc.status_code,
12
+ content={
13
+ "error": {
14
+ "code": exc.code,
15
+ "message": exc.message,
16
+ }
17
+ },
18
+ )
@@ -0,0 +1,7 @@
1
+ from gaard_core.schema.cache import SchemaContextCache
2
+
3
+ from gaard_api.core.settings import settings
4
+
5
+ schema_context_cache = SchemaContextCache(
6
+ ttl_seconds=settings.gaard_schema_cache_ttl_seconds,
7
+ )
@@ -0,0 +1,103 @@
1
+ from dataclasses import dataclass, field
2
+ import json
3
+ import os
4
+ from typing import Any
5
+
6
+
7
+ def env_value(name: str, default: str) -> str:
8
+ return os.environ.get(name, default)
9
+
10
+
11
+ def env_int_value(name: str, default: int) -> int:
12
+ value = os.environ.get(name)
13
+
14
+ if value is None:
15
+ return default
16
+
17
+ try:
18
+ return int(value)
19
+ except ValueError:
20
+ return default
21
+
22
+
23
+ def env_dict_value(name: str, default: dict[str, Any]) -> dict[str, Any]:
24
+ value = os.environ.get(name)
25
+
26
+ if value is None:
27
+ return dict(default)
28
+
29
+ try:
30
+ parsed = json.loads(value)
31
+ except json.JSONDecodeError:
32
+ return dict(default)
33
+
34
+ return parsed if isinstance(parsed, dict) else dict(default)
35
+
36
+
37
+ @dataclass
38
+ class Settings:
39
+ gaard_metadata_database_url: str = "sqlite:///./metadata.db"
40
+ gaard_datasource_url: str = field(
41
+ default_factory=lambda: env_value(
42
+ "GAARD_DATASOURCE_URL",
43
+ "sqlite:///./examples/medical-poc/demo.db",
44
+ )
45
+ )
46
+ gaard_query_max_rows: int = field(
47
+ default_factory=lambda: env_int_value("GAARD_QUERY_MAX_ROWS", 100)
48
+ )
49
+ gaard_query_timeout_seconds: int = field(
50
+ default_factory=lambda: env_int_value("GAARD_QUERY_TIMEOUT_SECONDS", 30)
51
+ )
52
+
53
+ gaard_schema_cache_ttl_seconds: int = field(
54
+ default_factory=lambda: env_int_value("GAARD_SCHEMA_CACHE_TTL_SECONDS", 300)
55
+ )
56
+ gaard_audit_retention_days: int = field(
57
+ default_factory=lambda: env_int_value("GAARD_AUDIT_RETENTION_DAYS", 90)
58
+ )
59
+
60
+ gaard_intent_classification_mode: str = field(
61
+ default_factory=lambda: env_value("GAARD_INTENT_CLASSIFICATION_MODE", "auto")
62
+ )
63
+ gaard_sql_generation_mode: str = field(
64
+ default_factory=lambda: env_value("GAARD_SQL_GENERATION_MODE", "llm")
65
+ )
66
+ gaard_result_interpretation_mode: str = field(
67
+ default_factory=lambda: env_value("GAARD_RESULT_INTERPRETATION_MODE", "llm")
68
+ )
69
+ gaard_output_classification_mode: str = field(
70
+ default_factory=lambda: env_value("GAARD_OUTPUT_CLASSIFICATION_MODE", "auto")
71
+ )
72
+ gaard_investigation_mode: str = field(
73
+ default_factory=lambda: env_value("GAARD_INVESTIGATION_MODE", "llm")
74
+ )
75
+ gaard_investigation_ambiguity_mode: str = field(
76
+ default_factory=lambda: env_value("GAARD_INVESTIGATION_AMBIGUITY_MODE", "clarify")
77
+ )
78
+
79
+ gaard_llm_provider: str = field(
80
+ default_factory=lambda: env_value("GAARD_LLM_PROVIDER", "openai-compatible")
81
+ )
82
+ gaard_llm_base_url: str = field(
83
+ default_factory=lambda: env_value("GAARD_LLM_BASE_URL", "https://api.openai.com/v1")
84
+ )
85
+ gaard_llm_api_key: str = field(
86
+ default_factory=lambda: env_value("GAARD_LLM_API_KEY", "change-me")
87
+ )
88
+ gaard_llm_model: str = field(
89
+ default_factory=lambda: env_value("GAARD_LLM_MODEL", "gpt-4.1-mini")
90
+ )
91
+ gaard_llm_extra_body: dict[str, Any] = field(
92
+ default_factory=lambda: env_dict_value("GAARD_LLM_EXTRA_BODY", {})
93
+ )
94
+ gaard_llm_timeout_seconds: int = field(
95
+ default_factory=lambda: env_int_value("GAARD_LLM_TIMEOUT_SECONDS", 60)
96
+ )
97
+
98
+ gaard_sql_dialect: str = field(
99
+ default_factory=lambda: env_value("GAARD_SQL_DIALECT", "sqlite")
100
+ )
101
+
102
+
103
+ settings = Settings()
File without changes
gaard_api/main.py ADDED
@@ -0,0 +1,53 @@
1
+ from fastapi import FastAPI
2
+ from fastapi.responses import FileResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+
5
+ from gaard_api.api.v1.admin import router as admin_router
6
+ from gaard_api.api.v1.prompts import router as prompts_router
7
+ from gaard_api.api.v1.query import router as query_router
8
+ from gaard_api.api.v1.schema import router as schema_router
9
+ from gaard_api.core.error_handlers import register_error_handlers
10
+ from importlib.resources import files
11
+
12
+ app = FastAPI(
13
+ title="GAARD API",
14
+ version="0.1.0",
15
+ description="Self-hosted AI SQL Gateway for governed natural-language access to relational data.",
16
+ )
17
+
18
+ register_error_handlers(app)
19
+
20
+ ADMIN_WEB_DIR = files("gaard_api").joinpath("admin-web")
21
+
22
+ if ADMIN_WEB_DIR.exists():
23
+ app.mount(
24
+ "/admin/assets",
25
+ StaticFiles(directory=ADMIN_WEB_DIR / "assets"),
26
+ name="admin-assets",
27
+ )
28
+
29
+
30
+ @app.get("/health")
31
+ def health() -> dict[str, str]:
32
+ return {"status": "ok"}
33
+
34
+
35
+ @app.get("/api/v1/health")
36
+ def api_health() -> dict[str, str]:
37
+ return {"status": "ok"}
38
+
39
+
40
+ app.include_router(query_router, prefix="/api/v1", tags=["query"])
41
+ app.include_router(schema_router, prefix="/api/v1", tags=["schema"])
42
+ app.include_router(prompts_router, prefix="/api/v1", tags=["prompts"])
43
+ app.include_router(admin_router, prefix="/api/v1/admin", tags=["admin"])
44
+
45
+
46
+ @app.get("/admin", include_in_schema=False)
47
+ def admin_index() -> FileResponse:
48
+ return FileResponse(ADMIN_WEB_DIR / "index.html")
49
+
50
+
51
+ @app.get("/admin/{path:path}", include_in_schema=False)
52
+ def admin_spa(path: str) -> FileResponse:
53
+ return FileResponse(ADMIN_WEB_DIR / "index.html")
File without changes
@@ -0,0 +1,25 @@
1
+ import argparse
2
+ from collections.abc import Sequence
3
+
4
+ from gaard_api.cli_commands import run_admin
5
+
6
+
7
+ def create_parser(prog: str | None = None) -> argparse.ArgumentParser:
8
+ parser = argparse.ArgumentParser(
9
+ prog=prog,
10
+ description="Run the GAARD API and admin application.",
11
+ )
12
+ subparsers = parser.add_subparsers(dest="command", required=True)
13
+
14
+ start_parser = subparsers.add_parser("start", help="Start the API server.")
15
+ start_parser.add_argument("--host", default="127.0.0.1")
16
+ start_parser.add_argument("--port", type=int, default=8000)
17
+ start_parser.add_argument("--reload", action="store_true")
18
+ start_parser.set_defaults(func=run_admin)
19
+
20
+ return parser
21
+
22
+
23
+ def main(argv: Sequence[str] | None = None) -> None:
24
+ args = create_parser().parse_args(argv)
25
+ args.func(args)
File without changes
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: gaard-api
3
+ Version: 0.1.0
4
+ Summary: GAARD backend web services providing admin interface
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: gaard-core==0.1.0
8
+ Requires-Dist: gaard-connectors==0.1.0
9
+ Requires-Dist: gaard-llm==0.1.0
10
+ Requires-Dist: fastapi>=0.111.0
11
+ Requires-Dist: uvicorn[standard]>=0.30.0
12
+ Requires-Dist: pydantic>=2.7.0
13
+ Requires-Dist: sqlalchemy>=2.0.0
14
+ Requires-Dist: httpx>=0.27.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
17
+ Requires-Dist: ruff>=0.5.0; extra == "dev"
18
+ Requires-Dist: mypy>=1.10.0; extra == "dev"
19
+ Requires-Dist: psycopg[binary]>=3.2.0; extra == "dev"
20
+ Requires-Dist: pymysql>=1.2.0; extra == "dev"
21
+
22
+ # GAARD - Governed AI Access to Relational Data
23
+
24
+ GAARD is a self-hosted AI SQL Gateway for governed natural-language access to relational data.
25
+
26
+ GAARD allows applications and users to ask questions about relational databases using natural language while keeping SQL generation, validation, execution, prompts, connectors, and auditability under control.
27
+
28
+ For more informacion see https://github.com/pkroliszewski/gaard
29
+
30
+ # This package
31
+
32
+ `gaard-api` provides the GAARD FastAPI backend and bundled admin application.
33
+
34
+ After installation, start it with:
35
+
36
+ ```bash
37
+ gaard-core start
38
+ ```
39
+
40
+ The command accepts `--host`, `--port`, and `--reload`. By default the API is
41
+ available at `http://localhost:8000` and the admin application at
42
+ `http://localhost:8000/admin`.
43
+
44
+ `gaard-api start` is an alias. `gaard admin` remains available for compatibility.
@@ -0,0 +1,34 @@
1
+ gaard_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ gaard_api/cli.py,sha256=ygIcIdT2kb4bU7cuabViaGmYFiIQHk9MljFTu_oJt2Q,471
3
+ gaard_api/cli_commands.py,sha256=mXwE7zSN4nqd6XvT3lu2h3xw7AC3myDrAGfzZB7rO7s,449
4
+ gaard_api/main.py,sha256=JFsv6ZIiRaZRPEMfMtWTjG_CRpSYIGzoLq53OdEVum0,1617
5
+ gaard_api/server_cli.py,sha256=YHwostkBN51MqxkFamjcukfTYhNiNbvwrlhC_fnXmlU,822
6
+ gaard_api/admin/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
+ gaard_api/admin/database.py,sha256=cWQoPku5D8sP3pakUNorPIO7mTThofuuTluC3QMAo4w,17087
8
+ gaard_api/admin/defaults.py,sha256=V5E27anqHhXvsclqL65ddq8q6R9RuVEg5lhetot-5Mc,11112
9
+ gaard_api/admin/models.py,sha256=WBJPEixHn9CvacDphpWcFEM27Ux33N3M5oBto35Nkm4,10281
10
+ gaard_api/admin/prompt_runtime.py,sha256=RQz-h8eFKoUXkq7ymN6cG4F2N6HF9LZYAmmcywycSo8,8476
11
+ gaard_api/admin/security.py,sha256=4reNbtO7aNmf_5j6cuARirzAT0TSzqbCeOpRgE8WRAw,1030
12
+ gaard_api/admin/services.py,sha256=0yEVcL1RS5ZWPMZN1d8q5UdL_jnIw5jXIqrEopYOlBM,68720
13
+ gaard_api/admin-web/index.html,sha256=VNt4pS4u85BHM3-U8PAyGUmlYI-uiRg7kSlZhl-Zb-I,373
14
+ gaard_api/admin-web/assets/main.js,sha256=zb5VQiXDSaUOmSZ7Clhb2Wgn4caPyd6rb0O3kzzWPhQ,93625
15
+ gaard_api/admin-web/assets/styles.css,sha256=92iCON8Vsd2MU7leyKG1uC7PSt_4MeiW8t7X1JV_ehM,15984
16
+ gaard_api/admin-web/src/main.ts,sha256=p7sesHn8egEYvGUvcjc4S8X9P5afhP28zGv4tubZi60,95982
17
+ gaard_api/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ gaard_api/api/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ gaard_api/api/v1/admin.py,sha256=uSCY5v8TUX4qAQeV9rhaRrIonkjuq749kNhIChgyINk,67950
20
+ gaard_api/api/v1/prompts.py,sha256=BCdv-5yuQIIMmIo9_JWxsXUSN2JOB4RmkCFQov5g-Q4,2153
21
+ gaard_api/api/v1/query.py,sha256=V_erMKxQucN1JTle4wXG60UrgpxaJXuhJL8RWOyR4aQ,39045
22
+ gaard_api/api/v1/schema.py,sha256=cJ5JxapZtXCfp_AUzviO8EGRPoCfuLrRdOr4zTAjZL4,1825
23
+ gaard_api/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ gaard_api/core/error_handlers.py,sha256=FM_6rETSeGqJSH3n9i_ozbvgUdptMDtdKRWOAaQyVlY,542
25
+ gaard_api/core/schema_cache.py,sha256=Ho1JFciVUltNeu9FaFeEp9Z2lc51YKl1MwutEWECNwY,203
26
+ gaard_api/core/settings.py,sha256=jO8hNptzivUOxaFtr6oi4zEPXVrPvAKv9Lq535OUjzs,3266
27
+ gaard_api/dependencies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
+ gaard_api/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
+ gaard_api/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
+ gaard_api-0.1.0.dist-info/METADATA,sha256=zhyABdJtSlLcERSryIXuzee-a0AQKBCfsw6ddILprmQ,1541
31
+ gaard_api-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
32
+ gaard_api-0.1.0.dist-info/entry_points.txt,sha256=5k8GFr1LdALDBBf42i2VgxyHJ2Gkml8qU8TtsZRQAkE,180
33
+ gaard_api-0.1.0.dist-info/top_level.txt,sha256=5g2-WOFGbK-SoVjsJniCaQH1bwSHF6SWPJVYNH4bjfc,10
34
+ gaard_api-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,7 @@
1
+ [console_scripts]
2
+ gaard = gaard_api.cli:main
3
+ gaard-api = gaard_api.server_cli:main
4
+ gaard-core = gaard_api.server_cli:main
5
+
6
+ [gaard.commands]
7
+ admin = gaard_api.cli_commands:register
@@ -0,0 +1 @@
1
+ gaard_api