gaard-api 0.1.0__tar.gz → 0.1.2__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.
- gaard_api-0.1.2/MANIFEST.in +2 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/PKG-INFO +13 -4
- {gaard_api-0.1.0 → gaard_api-0.1.2}/README.md +10 -2
- {gaard_api-0.1.0 → gaard_api-0.1.2}/pyproject.toml +12 -3
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/admin/database.py +0 -29
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/admin/services.py +15 -30
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/admin-web/assets/main.js +74 -19
- gaard_api-0.1.2/src/gaard_api/admin-web/package.json +10 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/admin-web/src/main.ts +110 -15
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/api/v1/admin.py +38 -13
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/api/v1/prompts.py +4 -4
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/api/v1/query.py +15 -11
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/api/v1/schema.py +4 -4
- gaard_api-0.1.2/src/gaard_api/example_data/__init__.py +1 -0
- gaard_api-0.1.2/src/gaard_api/example_data/medical_poc/__init__.py +1 -0
- gaard_api-0.1.2/src/gaard_api/example_data/medical_poc/schema.sql +140 -0
- gaard_api-0.1.2/src/gaard_api/example_data/medical_poc/seed.sql +635 -0
- gaard_api-0.1.2/src/gaard_api/example_database.py +135 -0
- gaard_api-0.1.2/src/gaard_api/extensions.py +18 -0
- gaard_api-0.1.2/src/gaard_api/server_cli.py +62 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api.egg-info/PKG-INFO +13 -4
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api.egg-info/SOURCES.txt +8 -3
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api.egg-info/requires.txt +2 -1
- {gaard_api-0.1.0 → gaard_api-0.1.2}/tests/test_admin_api.py +48 -15
- gaard_api-0.1.2/tests/test_server_cli.py +111 -0
- gaard_api-0.1.0/src/gaard_api/dependencies/__init__.py +0 -0
- gaard_api-0.1.0/src/gaard_api/schemas/__init__.py +0 -0
- gaard_api-0.1.0/src/gaard_api/server_cli.py +0 -25
- gaard_api-0.1.0/src/gaard_api/services/__init__.py +0 -0
- gaard_api-0.1.0/tests/test_server_cli.py +0 -20
- {gaard_api-0.1.0 → gaard_api-0.1.2}/setup.cfg +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/__init__.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/admin/__init__.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/admin/defaults.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/admin/models.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/admin/prompt_runtime.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/admin/security.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/admin-web/assets/styles.css +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/admin-web/index.html +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/api/__init__.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/api/v1/__init__.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/cli.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/cli_commands.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/core/__init__.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/core/error_handlers.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/core/schema_cache.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/core/settings.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api/main.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api.egg-info/dependency_links.txt +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api.egg-info/entry_points.txt +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/src/gaard_api.egg-info/top_level.txt +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/tests/test_error_handlers.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/tests/test_investigation_error_messages.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/tests/test_prompt_runtime.py +0 -0
- {gaard_api-0.1.0 → gaard_api-0.1.2}/tests/test_settings.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gaard-api
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: GAARD backend web services providing admin interface
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
Requires-Dist: gaard-core==0.1.0
|
|
8
|
-
Requires-Dist: gaard-connectors==0.1.
|
|
8
|
+
Requires-Dist: gaard-connectors==0.1.1
|
|
9
9
|
Requires-Dist: gaard-llm==0.1.0
|
|
10
|
+
Requires-Dist: gaard-plugin-api<0.2.0,>=0.1.0
|
|
10
11
|
Requires-Dist: fastapi>=0.111.0
|
|
11
12
|
Requires-Dist: uvicorn[standard]>=0.30.0
|
|
12
13
|
Requires-Dist: pydantic>=2.7.0
|
|
@@ -34,11 +35,19 @@ For more informacion see https://github.com/pkroliszewski/gaard
|
|
|
34
35
|
After installation, start it with:
|
|
35
36
|
|
|
36
37
|
```bash
|
|
38
|
+
gaard-core install-example-database
|
|
37
39
|
gaard-core start
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
`gaard-core install-example-database` creates the bundled Medical POC SQLite
|
|
43
|
+
database at `examples/medical-poc/demo.db` in the current working directory,
|
|
44
|
+
registers it as the active `default` datasource in `metadata.db`, and matches
|
|
45
|
+
the default datasource URL. Use
|
|
46
|
+
`gaard-core install-example-database --output /path/to/demo.db` to place it
|
|
47
|
+
elsewhere; the command prints the SQLite datasource URL saved in metadata.
|
|
48
|
+
|
|
49
|
+
`gaard-core start` accepts `--host`, `--port`, and `--reload`. By default the
|
|
50
|
+
API is available at `http://localhost:8000` and the admin application at
|
|
42
51
|
`http://localhost:8000/admin`.
|
|
43
52
|
|
|
44
53
|
`gaard-api start` is an alias. `gaard admin` remains available for compatibility.
|
|
@@ -13,11 +13,19 @@ For more informacion see https://github.com/pkroliszewski/gaard
|
|
|
13
13
|
After installation, start it with:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
+
gaard-core install-example-database
|
|
16
17
|
gaard-core start
|
|
17
18
|
```
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
`gaard-core install-example-database` creates the bundled Medical POC SQLite
|
|
21
|
+
database at `examples/medical-poc/demo.db` in the current working directory,
|
|
22
|
+
registers it as the active `default` datasource in `metadata.db`, and matches
|
|
23
|
+
the default datasource URL. Use
|
|
24
|
+
`gaard-core install-example-database --output /path/to/demo.db` to place it
|
|
25
|
+
elsewhere; the command prints the SQLite datasource URL saved in metadata.
|
|
26
|
+
|
|
27
|
+
`gaard-core start` accepts `--host`, `--port`, and `--reload`. By default the
|
|
28
|
+
API is available at `http://localhost:8000` and the admin application at
|
|
21
29
|
`http://localhost:8000/admin`.
|
|
22
30
|
|
|
23
31
|
`gaard-api start` is an alias. `gaard admin` remains available for compatibility.
|
|
@@ -4,15 +4,16 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gaard-api"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "GAARD backend web services providing admin interface"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
11
|
|
|
12
12
|
dependencies = [
|
|
13
13
|
"gaard-core==0.1.0",
|
|
14
|
-
"gaard-connectors==0.1.
|
|
14
|
+
"gaard-connectors==0.1.1",
|
|
15
15
|
"gaard-llm==0.1.0",
|
|
16
|
+
"gaard-plugin-api>=0.1.0,<0.2.0",
|
|
16
17
|
"fastapi>=0.111.0",
|
|
17
18
|
"uvicorn[standard]>=0.30.0",
|
|
18
19
|
"pydantic>=2.7.0",
|
|
@@ -44,7 +45,7 @@ gaard-core = "gaard_api.server_cli:main"
|
|
|
44
45
|
admin = "gaard_api.cli_commands:register"
|
|
45
46
|
|
|
46
47
|
[tool.setuptools]
|
|
47
|
-
include-package-data =
|
|
48
|
+
include-package-data = false
|
|
48
49
|
|
|
49
50
|
[tool.setuptools.packages.find]
|
|
50
51
|
where = ["src"]
|
|
@@ -52,6 +53,14 @@ where = ["src"]
|
|
|
52
53
|
[tool.setuptools.package-data]
|
|
53
54
|
gaard_api = [
|
|
54
55
|
"admin-web/**/*",
|
|
56
|
+
"example_data/medical_poc/*.sql",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[tool.setuptools.exclude-package-data]
|
|
60
|
+
gaard_api = [
|
|
61
|
+
"**/__pycache__/*",
|
|
62
|
+
"**/*.pyc",
|
|
63
|
+
"**/*.pyo",
|
|
55
64
|
]
|
|
56
65
|
|
|
57
66
|
[tool.ruff]
|
|
@@ -190,22 +190,6 @@ def seed_prompts(session: Session) -> None:
|
|
|
190
190
|
def seed_datasource_connectors(session: Session) -> None:
|
|
191
191
|
migrate_postgres_sql_dialect(session)
|
|
192
192
|
|
|
193
|
-
default_connector = session.scalar(
|
|
194
|
-
select(DatasourceConnector).where(DatasourceConnector.connector_key == "default")
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
if default_connector is None:
|
|
198
|
-
session.add(
|
|
199
|
-
DatasourceConnector(
|
|
200
|
-
connector_key="default",
|
|
201
|
-
name="Medical POC SQLite",
|
|
202
|
-
database_type="sqlite",
|
|
203
|
-
database_url=settings.gaard_datasource_url,
|
|
204
|
-
sql_dialect=settings.gaard_sql_dialect,
|
|
205
|
-
active=True,
|
|
206
|
-
)
|
|
207
|
-
)
|
|
208
|
-
|
|
209
193
|
metadata_connector = session.scalar(
|
|
210
194
|
select(DatasourceConnector).where(DatasourceConnector.connector_key == "metadata-db")
|
|
211
195
|
)
|
|
@@ -230,19 +214,6 @@ def seed_datasource_connectors(session: Session) -> None:
|
|
|
230
214
|
metadata_connector.active = False
|
|
231
215
|
metadata_connector.updated_by = "system"
|
|
232
216
|
|
|
233
|
-
active_user_connector = session.scalar(
|
|
234
|
-
select(DatasourceConnector).where(
|
|
235
|
-
DatasourceConnector.connector_key != "metadata-db",
|
|
236
|
-
DatasourceConnector.active.is_(True),
|
|
237
|
-
)
|
|
238
|
-
)
|
|
239
|
-
default_connector = session.scalar(
|
|
240
|
-
select(DatasourceConnector).where(DatasourceConnector.connector_key == "default")
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
if active_user_connector is None and default_connector is not None:
|
|
244
|
-
default_connector.active = True
|
|
245
|
-
|
|
246
217
|
|
|
247
218
|
def infer_datasource_type(database_url: str) -> tuple[str, str]:
|
|
248
219
|
if database_url.startswith("sqlite"):
|
|
@@ -5,12 +5,10 @@ from dataclasses import dataclass
|
|
|
5
5
|
from datetime import UTC, datetime, timedelta
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
-
from sqlalchemy import create_engine, text
|
|
9
8
|
from sqlalchemy import delete, desc, select
|
|
10
9
|
from sqlalchemy.exc import SQLAlchemyError
|
|
11
10
|
from sqlalchemy.orm import Session
|
|
12
11
|
|
|
13
|
-
from gaard_connectors.sqlalchemy.introspector import SQLAlchemySchemaIntrospector
|
|
14
12
|
from gaard_core.errors import LlmProviderError
|
|
15
13
|
from gaard_core.llm_output import remove_thinking_blocks
|
|
16
14
|
from gaard_core.query_pipeline.models import OutputClassification, QueryRequest, QueryResponse
|
|
@@ -33,6 +31,7 @@ from gaard_api.admin.models import (
|
|
|
33
31
|
PromptTemplate,
|
|
34
32
|
)
|
|
35
33
|
from gaard_api.core.settings import settings
|
|
34
|
+
from gaard_api.extensions import get_connector_registry
|
|
36
35
|
|
|
37
36
|
|
|
38
37
|
def json_dumps(value: Any) -> str:
|
|
@@ -43,24 +42,16 @@ def json_loads(value: str) -> Any:
|
|
|
43
42
|
return json.loads(value or "{}")
|
|
44
43
|
|
|
45
44
|
|
|
46
|
-
SUPPORTED_DATABASE_TYPES = {
|
|
47
|
-
"sqlite": ("sqlite://", "sqlite"),
|
|
48
|
-
"postgresql": ("postgresql://", "postgresql+psycopg://"),
|
|
49
|
-
"mysql": ("mysql://", "mysql+pymysql://"),
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
45
|
def validate_datasource_url(database_type: str, database_url: str) -> None:
|
|
54
|
-
|
|
46
|
+
get_connector_registry().get(database_type).validate_database_url(database_url)
|
|
55
47
|
|
|
56
|
-
if prefixes is None:
|
|
57
|
-
raise ValueError("Unsupported datasource type.")
|
|
58
48
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
49
|
+
def validate_datasource_configuration(
|
|
50
|
+
database_type: str,
|
|
51
|
+
database_url: str,
|
|
52
|
+
sql_dialect: str,
|
|
53
|
+
) -> None:
|
|
54
|
+
get_connector_registry().validate(database_type, database_url, sql_dialect)
|
|
64
55
|
|
|
65
56
|
|
|
66
57
|
def mask_database_url(database_url: str) -> str:
|
|
@@ -1896,18 +1887,7 @@ def set_active_datasource_connector(
|
|
|
1896
1887
|
|
|
1897
1888
|
|
|
1898
1889
|
def test_datasource_connection(connector: DatasourceConnector) -> None:
|
|
1899
|
-
|
|
1900
|
-
{"check_same_thread": False}
|
|
1901
|
-
if connector.database_url.startswith("sqlite")
|
|
1902
|
-
else {}
|
|
1903
|
-
)
|
|
1904
|
-
engine = create_engine(connector.database_url, connect_args=connect_args)
|
|
1905
|
-
|
|
1906
|
-
try:
|
|
1907
|
-
with engine.connect() as connection:
|
|
1908
|
-
connection.execute(text("SELECT 1"))
|
|
1909
|
-
finally:
|
|
1910
|
-
engine.dispose()
|
|
1890
|
+
get_connector_registry().get(connector.database_type).connection_tester(connector.database_url)
|
|
1911
1891
|
|
|
1912
1892
|
|
|
1913
1893
|
def get_datasource_schema_cache(
|
|
@@ -1935,7 +1915,12 @@ def introspect_datasource_connector(
|
|
|
1935
1915
|
connector: DatasourceConnector,
|
|
1936
1916
|
actor: str,
|
|
1937
1917
|
) -> DatasourceSchemaCache:
|
|
1938
|
-
schema =
|
|
1918
|
+
schema = (
|
|
1919
|
+
get_connector_registry()
|
|
1920
|
+
.get(connector.database_type)
|
|
1921
|
+
.introspector_factory(connector.database_url)
|
|
1922
|
+
.introspect()
|
|
1923
|
+
)
|
|
1939
1924
|
existing = get_datasource_schema_cache(session, connector.id)
|
|
1940
1925
|
existing_settings = (
|
|
1941
1926
|
json_loads(existing.table_settings_json) if existing is not None else {}
|
|
@@ -81,6 +81,7 @@ const state = {
|
|
|
81
81
|
llmConfig: null,
|
|
82
82
|
governancePolicy: null,
|
|
83
83
|
datasources: [],
|
|
84
|
+
datasourceTypes: [],
|
|
84
85
|
selectedDatasourceId: null,
|
|
85
86
|
datasourceSchema: null,
|
|
86
87
|
datasourceSchemaLoading: false,
|
|
@@ -946,7 +947,14 @@ function getSelectedDatasource() {
|
|
|
946
947
|
}
|
|
947
948
|
function renderDatasourceForm(connector) {
|
|
948
949
|
const systemManaged = connector?.system_managed === true;
|
|
949
|
-
const
|
|
950
|
+
const selectedTypeKey = connector?.database_type || state.datasourceTypes[0]?.type_key || "";
|
|
951
|
+
const selectedType = getDatasourceType(selectedTypeKey);
|
|
952
|
+
const databaseUrlSchema = selectedType?.config_schema?.properties?.database_url;
|
|
953
|
+
const selectedSqlDialect = connector?.sql_dialect || selectedType?.default_sql_dialect || "";
|
|
954
|
+
const databaseUrl = connector?.database_url || String(databaseUrlSchema?.default || "");
|
|
955
|
+
const unavailableType = Boolean(selectedTypeKey && !selectedType);
|
|
956
|
+
const disabled = systemManaged || unavailableType || !selectedType ? "disabled" : "";
|
|
957
|
+
const connectorDescription = selectedType?.description || (unavailableType ? `Connector type '${selectedTypeKey}' is unavailable. Install or enable its plugin before editing this datasource.` : "No connector types are available. Install or enable a connector plugin.");
|
|
950
958
|
return `
|
|
951
959
|
<form id="datasource-form" class="form-grid">
|
|
952
960
|
<input type="hidden" name="id" value="${escapeHtml(connector?.id || "")}" />
|
|
@@ -954,32 +962,74 @@ function renderDatasourceForm(connector) {
|
|
|
954
962
|
<label>Connector key<input name="connector_key" ${connector || systemManaged ? "readonly" : ""} ${disabled} value="${escapeHtml(connector?.connector_key || "")}" /></label>
|
|
955
963
|
<label>Name<input name="name" ${disabled} value="${escapeHtml(connector?.name || "")}" /></label>
|
|
956
964
|
<div class="subgrid">
|
|
957
|
-
<label>
|
|
958
|
-
<label>SQL dialect<select name="sql_dialect" ${disabled}>${renderSqlDialectOptions(
|
|
965
|
+
<label>Connector type<select id="datasource-type" name="database_type" ${disabled}>${renderDatasourceTypeOptions(selectedTypeKey)}</select></label>
|
|
966
|
+
<label>SQL dialect<select id="datasource-sql-dialect" name="sql_dialect" ${disabled}>${renderSqlDialectOptions(selectedType, selectedSqlDialect)}</select></label>
|
|
959
967
|
</div>
|
|
960
|
-
<
|
|
968
|
+
<p id="datasource-type-description" class="muted">${escapeHtml(connectorDescription)}</p>
|
|
969
|
+
<label><span id="datasource-url-label">${escapeHtml(databaseUrlSchema?.title || "Database URL")}</span><input id="datasource-url" name="database_url" ${disabled} placeholder="${escapeHtml(databaseUrlSchema?.description || "")}" value="${escapeHtml(databaseUrl)}" /></label>
|
|
961
970
|
<label class="inline-check"><input name="active" type="checkbox" ${connector?.active ? "checked" : ""} ${disabled} /> Active datasource</label>
|
|
962
971
|
<div class="button-row">
|
|
963
|
-
<button type="button" id="test-datasource">Test</button>
|
|
972
|
+
<button type="button" id="test-datasource" ${disabled}>Test</button>
|
|
964
973
|
<button type="button" id="introspect-datasource" ${connector ? "" : "disabled"}>Schema introspection</button>
|
|
965
974
|
<button type="button" id="activate-datasource" ${connector && !connector.active && !systemManaged ? "" : "disabled"}>Activate</button>
|
|
966
975
|
<button class="primary" type="submit" ${systemManaged ? "disabled" : ""}>${connector ? "Save" : "Create"}</button>
|
|
967
976
|
</div>
|
|
968
977
|
</form>`;
|
|
969
978
|
}
|
|
970
|
-
function
|
|
971
|
-
return
|
|
972
|
-
"sqlite",
|
|
973
|
-
"postgresql",
|
|
974
|
-
"mysql"
|
|
975
|
-
].map((value)=>`<option value="${value}" ${selected === value ? "selected" : ""}>${value}</option>`).join("");
|
|
979
|
+
function getDatasourceType(typeKey) {
|
|
980
|
+
return state.datasourceTypes.find((item)=>item.type_key === typeKey) || null;
|
|
976
981
|
}
|
|
977
|
-
function
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
982
|
+
function renderDatasourceTypeOptions(selected) {
|
|
983
|
+
const datasourceTypes = [
|
|
984
|
+
...state.datasourceTypes
|
|
985
|
+
];
|
|
986
|
+
if (selected && !getDatasourceType(selected)) {
|
|
987
|
+
datasourceTypes.unshift({
|
|
988
|
+
type_key: selected,
|
|
989
|
+
label: `${selected} (plugin unavailable)`,
|
|
990
|
+
description: "",
|
|
991
|
+
sql_dialects: [],
|
|
992
|
+
default_sql_dialect: "",
|
|
993
|
+
config_schema: {}
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
if (!datasourceTypes.length) {
|
|
997
|
+
return `<option value="" selected>No connector types available</option>`;
|
|
998
|
+
}
|
|
999
|
+
return datasourceTypes.map((item)=>`<option value="${escapeHtml(item.type_key)}" ${item.type_key === selected ? "selected" : ""}>${escapeHtml(item.label)}</option>`).join("");
|
|
1000
|
+
}
|
|
1001
|
+
function renderSqlDialectOptions(datasourceType, selected) {
|
|
1002
|
+
const dialects = [
|
|
1003
|
+
...datasourceType?.sql_dialects || []
|
|
1004
|
+
];
|
|
1005
|
+
if (selected && !dialects.includes(selected)) {
|
|
1006
|
+
dialects.unshift(selected);
|
|
1007
|
+
}
|
|
1008
|
+
if (!dialects.length) {
|
|
1009
|
+
return `<option value="" selected>No SQL dialect available</option>`;
|
|
1010
|
+
}
|
|
1011
|
+
return dialects.map((value)=>`<option value="${escapeHtml(value)}" ${selected === value ? "selected" : ""}>${escapeHtml(value)}</option>`).join("");
|
|
1012
|
+
}
|
|
1013
|
+
function syncDatasourceTypeFields(event) {
|
|
1014
|
+
const typeKey = event.currentTarget.value;
|
|
1015
|
+
const datasourceType = getDatasourceType(typeKey);
|
|
1016
|
+
const sqlDialect = document.querySelector("#datasource-sql-dialect");
|
|
1017
|
+
const description = document.querySelector("#datasource-type-description");
|
|
1018
|
+
const urlLabel = document.querySelector("#datasource-url-label");
|
|
1019
|
+
const urlInput = document.querySelector("#datasource-url");
|
|
1020
|
+
const databaseUrlSchema = datasourceType?.config_schema?.properties?.database_url;
|
|
1021
|
+
if (sqlDialect) {
|
|
1022
|
+
sqlDialect.innerHTML = renderSqlDialectOptions(datasourceType, datasourceType?.default_sql_dialect || "");
|
|
1023
|
+
}
|
|
1024
|
+
if (description) {
|
|
1025
|
+
description.textContent = datasourceType?.description || "Connector type is unavailable.";
|
|
1026
|
+
}
|
|
1027
|
+
if (urlLabel) {
|
|
1028
|
+
urlLabel.textContent = databaseUrlSchema?.title || "Database URL";
|
|
1029
|
+
}
|
|
1030
|
+
if (urlInput) {
|
|
1031
|
+
urlInput.placeholder = databaseUrlSchema?.description || "";
|
|
1032
|
+
}
|
|
983
1033
|
}
|
|
984
1034
|
function renderModeOptions(selected, values) {
|
|
985
1035
|
return values.map((value)=>`<option value="${value}" ${selected === value ? "selected" : ""}>${value}</option>`).join("");
|
|
@@ -1315,6 +1365,7 @@ function attachSectionHandlers() {
|
|
|
1315
1365
|
document.querySelector("#llm-config-form")?.addEventListener("submit", saveLlmConfig);
|
|
1316
1366
|
document.querySelector("#governance-policy-form")?.addEventListener("submit", saveGovernancePolicy);
|
|
1317
1367
|
document.querySelector("#datasource-form")?.addEventListener("submit", saveDatasource);
|
|
1368
|
+
document.querySelector("#datasource-type")?.addEventListener("change", syncDatasourceTypeFields);
|
|
1318
1369
|
document.querySelector("#datasource-schema-form")?.addEventListener("submit", saveDatasourceSchema);
|
|
1319
1370
|
document.querySelector("#schema-show-enabled-only")?.addEventListener("change", (event)=>{
|
|
1320
1371
|
syncDatasourceSchemaDraftFromForm();
|
|
@@ -1974,8 +2025,12 @@ async function loadPrompts() {
|
|
|
1974
2025
|
render();
|
|
1975
2026
|
}
|
|
1976
2027
|
async function loadDatasources() {
|
|
1977
|
-
const
|
|
1978
|
-
|
|
2028
|
+
const [datasources, datasourceTypes] = await Promise.all([
|
|
2029
|
+
api("/api/v1/admin/datasources"),
|
|
2030
|
+
api("/api/v1/admin/datasource-types")
|
|
2031
|
+
]);
|
|
2032
|
+
state.datasources = datasources.items || [];
|
|
2033
|
+
state.datasourceTypes = datasourceTypes.items || [];
|
|
1979
2034
|
if (!state.selectedDatasourceId || state.selectedDatasourceId === "new") {
|
|
1980
2035
|
state.selectedDatasourceId = state.datasources[0]?.id || null;
|
|
1981
2036
|
}
|
|
@@ -26,14 +26,31 @@ type DatasourceConnector = {
|
|
|
26
26
|
id: number;
|
|
27
27
|
connector_key: string;
|
|
28
28
|
name: string;
|
|
29
|
-
database_type:
|
|
29
|
+
database_type: string;
|
|
30
30
|
database_url: string;
|
|
31
31
|
masked_database_url: string;
|
|
32
|
-
sql_dialect:
|
|
32
|
+
sql_dialect: string;
|
|
33
33
|
active: boolean;
|
|
34
34
|
system_managed: boolean;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
type DatasourceTypeConfigSchema = {
|
|
38
|
+
properties?: Record<string, {
|
|
39
|
+
title?: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
default?: unknown;
|
|
42
|
+
}>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type DatasourceType = {
|
|
46
|
+
type_key: string;
|
|
47
|
+
label: string;
|
|
48
|
+
description: string;
|
|
49
|
+
sql_dialects: string[];
|
|
50
|
+
default_sql_dialect: string;
|
|
51
|
+
config_schema: DatasourceTypeConfigSchema;
|
|
52
|
+
};
|
|
53
|
+
|
|
37
54
|
type OverviewDatasource = {
|
|
38
55
|
connector_key: string;
|
|
39
56
|
name: string;
|
|
@@ -108,6 +125,7 @@ type State = {
|
|
|
108
125
|
llmConfig: any | null;
|
|
109
126
|
governancePolicy: any | null;
|
|
110
127
|
datasources: DatasourceConnector[];
|
|
128
|
+
datasourceTypes: DatasourceType[];
|
|
111
129
|
selectedDatasourceId: number | "new" | null;
|
|
112
130
|
datasourceSchema: any | null;
|
|
113
131
|
datasourceSchemaLoading: boolean;
|
|
@@ -167,6 +185,7 @@ const state: State = {
|
|
|
167
185
|
llmConfig: null,
|
|
168
186
|
governancePolicy: null,
|
|
169
187
|
datasources: [],
|
|
188
|
+
datasourceTypes: [],
|
|
170
189
|
selectedDatasourceId: null,
|
|
171
190
|
datasourceSchema: null,
|
|
172
191
|
datasourceSchemaLoading: false,
|
|
@@ -1143,7 +1162,17 @@ function getSelectedDatasource(): DatasourceConnector | null {
|
|
|
1143
1162
|
|
|
1144
1163
|
function renderDatasourceForm(connector: DatasourceConnector | null): string {
|
|
1145
1164
|
const systemManaged = connector?.system_managed === true;
|
|
1146
|
-
const
|
|
1165
|
+
const selectedTypeKey = connector?.database_type || state.datasourceTypes[0]?.type_key || "";
|
|
1166
|
+
const selectedType = getDatasourceType(selectedTypeKey);
|
|
1167
|
+
const databaseUrlSchema = selectedType?.config_schema?.properties?.database_url;
|
|
1168
|
+
const selectedSqlDialect = connector?.sql_dialect || selectedType?.default_sql_dialect || "";
|
|
1169
|
+
const databaseUrl = connector?.database_url || String(databaseUrlSchema?.default || "");
|
|
1170
|
+
const unavailableType = Boolean(selectedTypeKey && !selectedType);
|
|
1171
|
+
const disabled = systemManaged || unavailableType || !selectedType ? "disabled" : "";
|
|
1172
|
+
const connectorDescription = selectedType?.description
|
|
1173
|
+
|| (unavailableType
|
|
1174
|
+
? `Connector type '${selectedTypeKey}' is unavailable. Install or enable its plugin before editing this datasource.`
|
|
1175
|
+
: "No connector types are available. Install or enable a connector plugin.");
|
|
1147
1176
|
return `
|
|
1148
1177
|
<form id="datasource-form" class="form-grid">
|
|
1149
1178
|
<input type="hidden" name="id" value="${escapeHtml(connector?.id || "")}" />
|
|
@@ -1151,13 +1180,14 @@ function renderDatasourceForm(connector: DatasourceConnector | null): string {
|
|
|
1151
1180
|
<label>Connector key<input name="connector_key" ${connector || systemManaged ? "readonly" : ""} ${disabled} value="${escapeHtml(connector?.connector_key || "")}" /></label>
|
|
1152
1181
|
<label>Name<input name="name" ${disabled} value="${escapeHtml(connector?.name || "")}" /></label>
|
|
1153
1182
|
<div class="subgrid">
|
|
1154
|
-
<label>
|
|
1155
|
-
<label>SQL dialect<select name="sql_dialect" ${disabled}>${renderSqlDialectOptions(
|
|
1183
|
+
<label>Connector type<select id="datasource-type" name="database_type" ${disabled}>${renderDatasourceTypeOptions(selectedTypeKey)}</select></label>
|
|
1184
|
+
<label>SQL dialect<select id="datasource-sql-dialect" name="sql_dialect" ${disabled}>${renderSqlDialectOptions(selectedType, selectedSqlDialect)}</select></label>
|
|
1156
1185
|
</div>
|
|
1157
|
-
<
|
|
1186
|
+
<p id="datasource-type-description" class="muted">${escapeHtml(connectorDescription)}</p>
|
|
1187
|
+
<label><span id="datasource-url-label">${escapeHtml(databaseUrlSchema?.title || "Database URL")}</span><input id="datasource-url" name="database_url" ${disabled} placeholder="${escapeHtml(databaseUrlSchema?.description || "")}" value="${escapeHtml(databaseUrl)}" /></label>
|
|
1158
1188
|
<label class="inline-check"><input name="active" type="checkbox" ${connector?.active ? "checked" : ""} ${disabled} /> Active datasource</label>
|
|
1159
1189
|
<div class="button-row">
|
|
1160
|
-
<button type="button" id="test-datasource">Test</button>
|
|
1190
|
+
<button type="button" id="test-datasource" ${disabled}>Test</button>
|
|
1161
1191
|
<button type="button" id="introspect-datasource" ${connector ? "" : "disabled"}>Schema introspection</button>
|
|
1162
1192
|
<button type="button" id="activate-datasource" ${connector && !connector.active && !systemManaged ? "" : "disabled"}>Activate</button>
|
|
1163
1193
|
<button class="primary" type="submit" ${systemManaged ? "disabled" : ""}>${connector ? "Save" : "Create"}</button>
|
|
@@ -1165,18 +1195,78 @@ function renderDatasourceForm(connector: DatasourceConnector | null): string {
|
|
|
1165
1195
|
</form>`;
|
|
1166
1196
|
}
|
|
1167
1197
|
|
|
1168
|
-
function
|
|
1169
|
-
return
|
|
1170
|
-
|
|
1198
|
+
function getDatasourceType(typeKey: string): DatasourceType | null {
|
|
1199
|
+
return state.datasourceTypes.find(item => item.type_key === typeKey) || null;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function renderDatasourceTypeOptions(selected: string): string {
|
|
1203
|
+
const datasourceTypes = [...state.datasourceTypes];
|
|
1204
|
+
|
|
1205
|
+
if (selected && !getDatasourceType(selected)) {
|
|
1206
|
+
datasourceTypes.unshift({
|
|
1207
|
+
type_key: selected,
|
|
1208
|
+
label: `${selected} (plugin unavailable)`,
|
|
1209
|
+
description: "",
|
|
1210
|
+
sql_dialects: [],
|
|
1211
|
+
default_sql_dialect: "",
|
|
1212
|
+
config_schema: {},
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
if (!datasourceTypes.length) {
|
|
1217
|
+
return `<option value="" selected>No connector types available</option>`;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
return datasourceTypes
|
|
1221
|
+
.map(item => `<option value="${escapeHtml(item.type_key)}" ${item.type_key === selected ? "selected" : ""}>${escapeHtml(item.label)}</option>`)
|
|
1171
1222
|
.join("");
|
|
1172
1223
|
}
|
|
1173
1224
|
|
|
1174
|
-
function renderSqlDialectOptions(selected: string): string {
|
|
1175
|
-
|
|
1176
|
-
|
|
1225
|
+
function renderSqlDialectOptions(datasourceType: DatasourceType | null, selected: string): string {
|
|
1226
|
+
const dialects = [...(datasourceType?.sql_dialects || [])];
|
|
1227
|
+
|
|
1228
|
+
if (selected && !dialects.includes(selected)) {
|
|
1229
|
+
dialects.unshift(selected);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if (!dialects.length) {
|
|
1233
|
+
return `<option value="" selected>No SQL dialect available</option>`;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
return dialects
|
|
1237
|
+
.map(value => `<option value="${escapeHtml(value)}" ${selected === value ? "selected" : ""}>${escapeHtml(value)}</option>`)
|
|
1177
1238
|
.join("");
|
|
1178
1239
|
}
|
|
1179
1240
|
|
|
1241
|
+
function syncDatasourceTypeFields(event: Event): void {
|
|
1242
|
+
const typeKey = (event.currentTarget as HTMLSelectElement).value;
|
|
1243
|
+
const datasourceType = getDatasourceType(typeKey);
|
|
1244
|
+
const sqlDialect = document.querySelector<HTMLSelectElement>("#datasource-sql-dialect");
|
|
1245
|
+
const description = document.querySelector<HTMLElement>("#datasource-type-description");
|
|
1246
|
+
const urlLabel = document.querySelector<HTMLElement>("#datasource-url-label");
|
|
1247
|
+
const urlInput = document.querySelector<HTMLInputElement>("#datasource-url");
|
|
1248
|
+
const databaseUrlSchema = datasourceType?.config_schema?.properties?.database_url;
|
|
1249
|
+
|
|
1250
|
+
if (sqlDialect) {
|
|
1251
|
+
sqlDialect.innerHTML = renderSqlDialectOptions(
|
|
1252
|
+
datasourceType,
|
|
1253
|
+
datasourceType?.default_sql_dialect || "",
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (description) {
|
|
1258
|
+
description.textContent = datasourceType?.description || "Connector type is unavailable.";
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
if (urlLabel) {
|
|
1262
|
+
urlLabel.textContent = databaseUrlSchema?.title || "Database URL";
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (urlInput) {
|
|
1266
|
+
urlInput.placeholder = databaseUrlSchema?.description || "";
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1180
1270
|
function renderModeOptions(selected: string, values: string[]): string {
|
|
1181
1271
|
return values
|
|
1182
1272
|
.map(value => `<option value="${value}" ${selected === value ? "selected" : ""}>${value}</option>`)
|
|
@@ -1533,6 +1623,7 @@ function attachSectionHandlers(): void {
|
|
|
1533
1623
|
document.querySelector<HTMLFormElement>("#llm-config-form")?.addEventListener("submit", saveLlmConfig);
|
|
1534
1624
|
document.querySelector<HTMLFormElement>("#governance-policy-form")?.addEventListener("submit", saveGovernancePolicy);
|
|
1535
1625
|
document.querySelector<HTMLFormElement>("#datasource-form")?.addEventListener("submit", saveDatasource);
|
|
1626
|
+
document.querySelector<HTMLSelectElement>("#datasource-type")?.addEventListener("change", syncDatasourceTypeFields);
|
|
1536
1627
|
document.querySelector<HTMLFormElement>("#datasource-schema-form")?.addEventListener("submit", saveDatasourceSchema);
|
|
1537
1628
|
document.querySelector<HTMLInputElement>("#schema-show-enabled-only")?.addEventListener("change", event => {
|
|
1538
1629
|
syncDatasourceSchemaDraftFromForm();
|
|
@@ -2252,8 +2343,12 @@ async function loadPrompts(): Promise<void> {
|
|
|
2252
2343
|
}
|
|
2253
2344
|
|
|
2254
2345
|
async function loadDatasources(): Promise<void> {
|
|
2255
|
-
const
|
|
2256
|
-
|
|
2346
|
+
const [datasources, datasourceTypes] = await Promise.all([
|
|
2347
|
+
api<{ items: DatasourceConnector[] }>("/api/v1/admin/datasources"),
|
|
2348
|
+
api<{ items: DatasourceType[] }>("/api/v1/admin/datasource-types"),
|
|
2349
|
+
]);
|
|
2350
|
+
state.datasources = datasources.items || [];
|
|
2351
|
+
state.datasourceTypes = datasourceTypes.items || [];
|
|
2257
2352
|
if (!state.selectedDatasourceId || state.selectedDatasourceId === "new") {
|
|
2258
2353
|
state.selectedDatasourceId = state.datasources[0]?.id || null;
|
|
2259
2354
|
}
|