gaard-api 0.2.5__tar.gz → 0.2.6__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.2.5/src/gaard_api.egg-info → gaard_api-0.2.6}/PKG-INFO +5 -5
- {gaard_api-0.2.5 → gaard_api-0.2.6}/pyproject.toml +5 -5
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/api/v1/admin.py +98 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/api/v1/dashboards.py +77 -1
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/main.py +1 -1
- {gaard_api-0.2.5 → gaard_api-0.2.6/src/gaard_api.egg-info}/PKG-INFO +5 -5
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api.egg-info/requires.txt +4 -4
- {gaard_api-0.2.5 → gaard_api-0.2.6}/tests/test_admin_api.py +99 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/MANIFEST.in +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/README.md +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/setup.cfg +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/__init__.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/admin/__init__.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/admin/database.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/admin/defaults.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/admin/models.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/admin/prompt_runtime.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/admin/security.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/admin/services.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/admin-web/assets/getgaard.svg +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/admin-web/assets/main.js +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/admin-web/assets/styles.css +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/admin-web/assets/vendor/gridstack/gridstack-all.js +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/admin-web/assets/vendor/gridstack/gridstack.min.css +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/admin-web/index.html +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/admin-web/package.json +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/api/__init__.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/api/v1/__init__.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/api/v1/analysis.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/api/v1/prompts.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/api/v1/query.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/api/v1/schema.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/api_registry.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/auth_dependencies.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/auth_hooks.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/cli.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/cli_commands.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/conversations.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/core/__init__.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/core/error_handlers.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/core/schema_cache.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/core/settings.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/example_data/__init__.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/example_data/medical_poc/__init__.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/example_data/medical_poc/schema.sql +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/example_data/medical_poc/seed.sql +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/example_database.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/extension_services.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/extensions.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/license.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/package_updates.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/py.typed +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/query_hooks.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/server_cli.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/siem.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/tls_http.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api.egg-info/SOURCES.txt +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api.egg-info/dependency_links.txt +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api.egg-info/entry_points.txt +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api.egg-info/top_level.txt +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/tests/test_analysis_api.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/tests/test_api_registry.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/tests/test_conversation_api.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/tests/test_error_handlers.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/tests/test_license_api.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/tests/test_license_service.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/tests/test_prompt_runtime.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/tests/test_query_error_messages.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/tests/test_server_cli.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/tests/test_settings.py +0 -0
- {gaard_api-0.2.5 → gaard_api-0.2.6}/tests/test_tls_http.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gaard-api
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: GAARD backend web services providing admin interface
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
|
-
Requires-Dist: gaard-core==0.2.
|
|
8
|
-
Requires-Dist: gaard-connectors==0.2.
|
|
9
|
-
Requires-Dist: gaard-llm==0.2.
|
|
10
|
-
Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.
|
|
7
|
+
Requires-Dist: gaard-core==0.2.6
|
|
8
|
+
Requires-Dist: gaard-connectors==0.2.6
|
|
9
|
+
Requires-Dist: gaard-llm==0.2.6
|
|
10
|
+
Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.6
|
|
11
11
|
Requires-Dist: fastapi>=0.111.0
|
|
12
12
|
Requires-Dist: uvicorn[standard]>=0.30.0
|
|
13
13
|
Requires-Dist: pydantic>=2.7.0
|
|
@@ -4,16 +4,16 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gaard-api"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.6"
|
|
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
|
-
"gaard-core==0.2.
|
|
14
|
-
"gaard-connectors==0.2.
|
|
15
|
-
"gaard-llm==0.2.
|
|
16
|
-
"gaard-plugin-api>=0.2.
|
|
13
|
+
"gaard-core==0.2.6",
|
|
14
|
+
"gaard-connectors==0.2.6",
|
|
15
|
+
"gaard-llm==0.2.6",
|
|
16
|
+
"gaard-plugin-api>=0.2.6,<0.3.0",
|
|
17
17
|
"fastapi>=0.111.0",
|
|
18
18
|
"uvicorn[standard]>=0.30.0",
|
|
19
19
|
"pydantic>=2.7.0",
|
|
@@ -13,6 +13,7 @@ from gaard_core.errors import (
|
|
|
13
13
|
QueryExecutionError,
|
|
14
14
|
SqlValidationError,
|
|
15
15
|
)
|
|
16
|
+
from gaard_core.llm_output import remove_thinking_blocks
|
|
16
17
|
from gaard_core.query_pipeline.models import (
|
|
17
18
|
OutputClassification,
|
|
18
19
|
QueryRequest,
|
|
@@ -21,6 +22,8 @@ from gaard_core.query_pipeline.models import (
|
|
|
21
22
|
)
|
|
22
23
|
from gaard_core.sql_validator.select_only import SelectOnlySqlValidator
|
|
23
24
|
from gaard_connectors import ConnectorRegistryError
|
|
25
|
+
from gaard_llm.openai_compatible.client import OpenAICompatibleClient
|
|
26
|
+
from gaard_llm.providers.models import ChatCompletionRequest, ChatMessage
|
|
24
27
|
from gaard_plugin_api import ExtensionRecord
|
|
25
28
|
from pydantic import BaseModel, Field
|
|
26
29
|
from sqlalchemy import select
|
|
@@ -251,6 +254,11 @@ class OverviewWidgetFromQueryRequest(BaseModel):
|
|
|
251
254
|
)
|
|
252
255
|
|
|
253
256
|
|
|
257
|
+
class OverviewWidgetTitleSuggestionRequest(BaseModel):
|
|
258
|
+
question: str = Field(min_length=1)
|
|
259
|
+
sql: str | None = None
|
|
260
|
+
|
|
261
|
+
|
|
254
262
|
class DatasourceConnectorRequest(BaseModel):
|
|
255
263
|
connector_key: str = Field(min_length=1, pattern=r"^[a-zA-Z0-9_-]+$")
|
|
256
264
|
name: str = Field(min_length=1)
|
|
@@ -503,6 +511,79 @@ def build_client_widget_key(session: Session, label: str, question: str) -> str:
|
|
|
503
511
|
return widget_key
|
|
504
512
|
|
|
505
513
|
|
|
514
|
+
def normalize_metric_title(value: str, fallback: str) -> str:
|
|
515
|
+
cleaned = remove_thinking_blocks(value).strip()
|
|
516
|
+
|
|
517
|
+
if cleaned.startswith("```"):
|
|
518
|
+
cleaned = cleaned.removeprefix("```").strip()
|
|
519
|
+
if cleaned.startswith("text"):
|
|
520
|
+
cleaned = cleaned.removeprefix("text").strip()
|
|
521
|
+
if cleaned.endswith("```"):
|
|
522
|
+
cleaned = cleaned.removesuffix("```").strip()
|
|
523
|
+
|
|
524
|
+
cleaned = cleaned.strip().strip('"').strip("'")
|
|
525
|
+
cleaned = re.sub(r"\s+", " ", cleaned)
|
|
526
|
+
cleaned = cleaned.rstrip(".:;-")
|
|
527
|
+
if not cleaned:
|
|
528
|
+
cleaned = fallback.strip()
|
|
529
|
+
|
|
530
|
+
return cleaned[:80].strip() or "Saved Metric"
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def fallback_metric_title(question: str) -> str:
|
|
534
|
+
normalized = re.sub(r"\s+", " ", question.strip())
|
|
535
|
+
normalized = normalized.rstrip("?!.:;-")
|
|
536
|
+
if not normalized:
|
|
537
|
+
return "Saved Metric"
|
|
538
|
+
|
|
539
|
+
return normalized[0].upper() + normalized[1:80]
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def suggest_metric_title_with_llm(session: Session, request: OverviewWidgetTitleSuggestionRequest) -> str:
|
|
543
|
+
llm_config = get_llm_runtime_config(session)
|
|
544
|
+
if llm_config.provider != "openai-compatible":
|
|
545
|
+
raise ConfigurationError(f"Unsupported GAARD_LLM_PROVIDER: {llm_config.provider}")
|
|
546
|
+
if not llm_config.api_key or llm_config.api_key == "change-me":
|
|
547
|
+
raise ConfigurationError("GAARD_LLM_API_KEY must be configured to suggest metric titles.")
|
|
548
|
+
|
|
549
|
+
client = OpenAICompatibleClient(
|
|
550
|
+
base_url=llm_config.base_url,
|
|
551
|
+
api_key=llm_config.api_key,
|
|
552
|
+
timeout_seconds=llm_config.timeout_seconds,
|
|
553
|
+
)
|
|
554
|
+
fallback = fallback_metric_title(request.question)
|
|
555
|
+
response = client.create_chat_completion(
|
|
556
|
+
ChatCompletionRequest(
|
|
557
|
+
model=llm_config.model,
|
|
558
|
+
temperature=0.0,
|
|
559
|
+
extra_body=llm_config.extra_body,
|
|
560
|
+
messages=[
|
|
561
|
+
ChatMessage(
|
|
562
|
+
role="system",
|
|
563
|
+
content=(
|
|
564
|
+
"You write short human-friendly metric names for dashboard widgets. "
|
|
565
|
+
"Return only the metric name, without quotes, markdown, explanations, "
|
|
566
|
+
"or trailing punctuation. Use the same language as the user question. "
|
|
567
|
+
"Prefer 3 to 8 words and keep it under 80 characters."
|
|
568
|
+
),
|
|
569
|
+
),
|
|
570
|
+
ChatMessage(
|
|
571
|
+
role="user",
|
|
572
|
+
content=(
|
|
573
|
+
"User question:\n"
|
|
574
|
+
f"{request.question.strip()}\n\n"
|
|
575
|
+
"Generated SQL, if useful:\n"
|
|
576
|
+
f"{(request.sql or '').strip()}\n\n"
|
|
577
|
+
"Return the metric name only."
|
|
578
|
+
),
|
|
579
|
+
),
|
|
580
|
+
],
|
|
581
|
+
)
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
return normalize_metric_title(response.content, fallback)
|
|
585
|
+
|
|
586
|
+
|
|
506
587
|
def build_client_excel_datasource_key(session: Session, filename: str) -> str:
|
|
507
588
|
stem = Path(filename).stem or "excel"
|
|
508
589
|
normalized = re.sub(r"[^a-z0-9_-]+", "_", stem.lower()).strip("_-") or "excel"
|
|
@@ -1416,6 +1497,23 @@ def create_overview_widget_from_query(
|
|
|
1416
1497
|
}
|
|
1417
1498
|
|
|
1418
1499
|
|
|
1500
|
+
@router.post("/overview/widgets/title-suggestion")
|
|
1501
|
+
def suggest_overview_widget_title(
|
|
1502
|
+
request: OverviewWidgetTitleSuggestionRequest,
|
|
1503
|
+
_principal: AuthenticatedSession = Depends(get_current_api_user),
|
|
1504
|
+
session: Session = Depends(get_session),
|
|
1505
|
+
) -> dict[str, Any]:
|
|
1506
|
+
try:
|
|
1507
|
+
title = suggest_metric_title_with_llm(session, request)
|
|
1508
|
+
except (ConfigurationError, LlmProviderError, ValueError) as exc:
|
|
1509
|
+
raise HTTPException(
|
|
1510
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
1511
|
+
detail=str(exc),
|
|
1512
|
+
) from exc
|
|
1513
|
+
|
|
1514
|
+
return {"title": title}
|
|
1515
|
+
|
|
1516
|
+
|
|
1419
1517
|
@router.put("/overview/widgets/{widget_key}")
|
|
1420
1518
|
def update_overview_widget(
|
|
1421
1519
|
widget_key: str,
|
|
@@ -48,6 +48,11 @@ class DashboardCreateRequest(BaseModel):
|
|
|
48
48
|
description: str = Field(default="", max_length=2_000)
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
class DashboardUpdateRequest(BaseModel):
|
|
52
|
+
name: str = Field(min_length=1, max_length=255)
|
|
53
|
+
description: str = Field(default="", max_length=2_000)
|
|
54
|
+
|
|
55
|
+
|
|
51
56
|
class ActiveDashboardRequest(BaseModel):
|
|
52
57
|
dashboard_id: str = Field(min_length=1, max_length=64)
|
|
53
58
|
|
|
@@ -58,6 +63,10 @@ class DashboardWidgetCreateRequest(BaseModel):
|
|
|
58
63
|
visualization_type: str = Field(pattern=r"^(number|bar|stacked_bar|line|multi_line|pie|area|table)$")
|
|
59
64
|
|
|
60
65
|
|
|
66
|
+
class SavedMetricUpdateRequest(BaseModel):
|
|
67
|
+
label: str = Field(min_length=1, max_length=255)
|
|
68
|
+
|
|
69
|
+
|
|
61
70
|
class DashboardWidgetLayoutItem(BaseModel):
|
|
62
71
|
widget_id: str = Field(min_length=1, max_length=64)
|
|
63
72
|
x: int = Field(ge=0, le=11)
|
|
@@ -308,16 +317,49 @@ def list_dashboards(
|
|
|
308
317
|
|
|
309
318
|
@router.get("/dashboards/metrics")
|
|
310
319
|
def list_saved_dashboard_metrics(
|
|
320
|
+
include_result: bool = True,
|
|
311
321
|
principal: AuthenticatedSession = Depends(get_current_api_user),
|
|
312
322
|
session: Session = Depends(get_session),
|
|
313
323
|
) -> dict[str, Any]:
|
|
314
324
|
metrics = list_saved_metrics_for_owner(session, dashboard_owner_user_id(principal))
|
|
315
325
|
return {
|
|
316
|
-
"items": [
|
|
326
|
+
"items": [
|
|
327
|
+
serialize_saved_metric(session, metric, include_result=include_result)
|
|
328
|
+
for metric in metrics
|
|
329
|
+
],
|
|
317
330
|
"viewer": dashboard_owner_username(principal),
|
|
318
331
|
}
|
|
319
332
|
|
|
320
333
|
|
|
334
|
+
@router.patch("/dashboards/metrics/{widget_key}")
|
|
335
|
+
def update_saved_dashboard_metric(
|
|
336
|
+
widget_key: str,
|
|
337
|
+
request: SavedMetricUpdateRequest,
|
|
338
|
+
principal: AuthenticatedSession = Depends(get_current_api_user),
|
|
339
|
+
session: Session = Depends(get_session),
|
|
340
|
+
) -> dict[str, Any]:
|
|
341
|
+
owner_user_id = dashboard_owner_user_id(principal)
|
|
342
|
+
metric = get_saved_metric_for_owner(session, widget_key, owner_user_id)
|
|
343
|
+
if metric is None:
|
|
344
|
+
raise HTTPException(
|
|
345
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
346
|
+
detail="Saved metric not found.",
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
label = request.label.strip()
|
|
350
|
+
if not label:
|
|
351
|
+
raise HTTPException(
|
|
352
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
353
|
+
detail="Metric name is required.",
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
metric.label = label
|
|
357
|
+
metric.updated_by = dashboard_owner_username(principal)
|
|
358
|
+
session.commit()
|
|
359
|
+
|
|
360
|
+
return {"item": serialize_saved_metric(session, metric, include_result=False)}
|
|
361
|
+
|
|
362
|
+
|
|
321
363
|
@router.post("/dashboards")
|
|
322
364
|
def create_dashboard(
|
|
323
365
|
request: DashboardCreateRequest,
|
|
@@ -378,6 +420,40 @@ def set_active_dashboard(
|
|
|
378
420
|
}
|
|
379
421
|
|
|
380
422
|
|
|
423
|
+
@router.put("/dashboards/{dashboard_id}")
|
|
424
|
+
def update_dashboard(
|
|
425
|
+
dashboard_id: str,
|
|
426
|
+
request: DashboardUpdateRequest,
|
|
427
|
+
principal: AuthenticatedSession = Depends(get_current_api_user),
|
|
428
|
+
session: Session = Depends(get_session),
|
|
429
|
+
) -> dict[str, Any]:
|
|
430
|
+
dashboard = get_dashboard_for_owner(
|
|
431
|
+
session,
|
|
432
|
+
dashboard_id,
|
|
433
|
+
dashboard_owner_user_id(principal),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
if dashboard is None:
|
|
437
|
+
raise HTTPException(
|
|
438
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
439
|
+
detail="Dashboard not found.",
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
name = request.name.strip()
|
|
443
|
+
if not name:
|
|
444
|
+
raise HTTPException(
|
|
445
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
446
|
+
detail="Dashboard name is required.",
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
dashboard.name = name
|
|
450
|
+
dashboard.description = request.description.strip()
|
|
451
|
+
dashboard.owner_username = dashboard_owner_username(principal)
|
|
452
|
+
session.commit()
|
|
453
|
+
|
|
454
|
+
return {"item": serialize_dashboard(dashboard)}
|
|
455
|
+
|
|
456
|
+
|
|
381
457
|
@router.get("/dashboards/{dashboard_id}/widgets")
|
|
382
458
|
def list_widgets_for_dashboard(
|
|
383
459
|
dashboard_id: str,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gaard-api
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: GAARD backend web services providing admin interface
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
|
-
Requires-Dist: gaard-core==0.2.
|
|
8
|
-
Requires-Dist: gaard-connectors==0.2.
|
|
9
|
-
Requires-Dist: gaard-llm==0.2.
|
|
10
|
-
Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.
|
|
7
|
+
Requires-Dist: gaard-core==0.2.6
|
|
8
|
+
Requires-Dist: gaard-connectors==0.2.6
|
|
9
|
+
Requires-Dist: gaard-llm==0.2.6
|
|
10
|
+
Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.6
|
|
11
11
|
Requires-Dist: fastapi>=0.111.0
|
|
12
12
|
Requires-Dist: uvicorn[standard]>=0.30.0
|
|
13
13
|
Requires-Dist: pydantic>=2.7.0
|
|
@@ -400,6 +400,30 @@ def test_dashboards_are_scoped_to_authenticated_user(admin_client: TestClient) -
|
|
|
400
400
|
assert metrics_response.json()["items"][0]["widget_key"] == "admin_saved_metric"
|
|
401
401
|
assert metrics_response.json()["items"][0]["result"]["status"] == "ok"
|
|
402
402
|
|
|
403
|
+
lightweight_metrics_response = admin_client.get(
|
|
404
|
+
"/api/v1/dashboards/metrics?include_result=false",
|
|
405
|
+
headers=admin_headers,
|
|
406
|
+
)
|
|
407
|
+
assert lightweight_metrics_response.status_code == 200
|
|
408
|
+
assert lightweight_metrics_response.json()["items"][0]["widget_key"] == "admin_saved_metric"
|
|
409
|
+
assert "result" not in lightweight_metrics_response.json()["items"][0]
|
|
410
|
+
|
|
411
|
+
rename_metric_response = admin_client.patch(
|
|
412
|
+
"/api/v1/dashboards/metrics/admin_saved_metric",
|
|
413
|
+
headers=admin_headers,
|
|
414
|
+
json={"label": "Updated prompt count"},
|
|
415
|
+
)
|
|
416
|
+
assert rename_metric_response.status_code == 200
|
|
417
|
+
assert rename_metric_response.json()["item"]["label"] == "Updated prompt count"
|
|
418
|
+
assert "result" not in rename_metric_response.json()["item"]
|
|
419
|
+
|
|
420
|
+
cross_rename_metric_response = admin_client.patch(
|
|
421
|
+
"/api/v1/dashboards/metrics/admin_saved_metric",
|
|
422
|
+
headers=analyst_headers,
|
|
423
|
+
json={"label": "Cross user rename"},
|
|
424
|
+
)
|
|
425
|
+
assert cross_rename_metric_response.status_code == 404
|
|
426
|
+
|
|
403
427
|
dashboard_widget_response = admin_client.post(
|
|
404
428
|
f"/api/v1/dashboards/{admin_dashboard['id']}/widgets",
|
|
405
429
|
headers=admin_headers,
|
|
@@ -483,6 +507,30 @@ def test_dashboards_are_scoped_to_authenticated_user(admin_client: TestClient) -
|
|
|
483
507
|
)
|
|
484
508
|
assert cross_delete.status_code == 404
|
|
485
509
|
|
|
510
|
+
update_dashboard_response = admin_client.put(
|
|
511
|
+
f"/api/v1/dashboards/{admin_dashboard['id']}",
|
|
512
|
+
headers=admin_headers,
|
|
513
|
+
json={
|
|
514
|
+
"name": "Operations renamed",
|
|
515
|
+
"description": "Updated daily operational dashboard.",
|
|
516
|
+
},
|
|
517
|
+
)
|
|
518
|
+
assert update_dashboard_response.status_code == 200
|
|
519
|
+
assert update_dashboard_response.json()["item"]["name"] == "Operations renamed"
|
|
520
|
+
assert update_dashboard_response.json()["item"]["description"] == (
|
|
521
|
+
"Updated daily operational dashboard."
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
cross_update_dashboard_response = admin_client.put(
|
|
525
|
+
f"/api/v1/dashboards/{analyst_dashboard['id']}",
|
|
526
|
+
headers=admin_headers,
|
|
527
|
+
json={
|
|
528
|
+
"name": "Cross user rename",
|
|
529
|
+
"description": "Should not be allowed.",
|
|
530
|
+
},
|
|
531
|
+
)
|
|
532
|
+
assert cross_update_dashboard_response.status_code == 404
|
|
533
|
+
|
|
486
534
|
admin_dashboards = admin_client.get(
|
|
487
535
|
"/api/v1/admin/dashboards",
|
|
488
536
|
headers=admin_headers,
|
|
@@ -1432,6 +1480,57 @@ def test_overview_widget_can_be_saved_from_query_and_deleted(
|
|
|
1432
1480
|
)
|
|
1433
1481
|
|
|
1434
1482
|
|
|
1483
|
+
def test_overview_widget_title_suggestion_uses_llm(
|
|
1484
|
+
admin_client: TestClient,
|
|
1485
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1486
|
+
) -> None:
|
|
1487
|
+
headers = auth_headers(admin_client)
|
|
1488
|
+
|
|
1489
|
+
with create_session() as session:
|
|
1490
|
+
set_setting(session, "gaard_llm_api_key", "test-key", "test")
|
|
1491
|
+
set_setting(session, "gaard_llm_model", "title-model", "test")
|
|
1492
|
+
session.commit()
|
|
1493
|
+
|
|
1494
|
+
class FakeOpenAICompatibleClient:
|
|
1495
|
+
init_kwargs: dict | None = None
|
|
1496
|
+
requests = []
|
|
1497
|
+
|
|
1498
|
+
def __init__(self, **kwargs) -> None:
|
|
1499
|
+
self.__class__.init_kwargs = kwargs
|
|
1500
|
+
|
|
1501
|
+
def create_chat_completion(self, request):
|
|
1502
|
+
self.__class__.requests.append(request)
|
|
1503
|
+
return ChatCompletionResponse(
|
|
1504
|
+
content="```text\nDoctors by Specialty.\n```",
|
|
1505
|
+
model=request.model,
|
|
1506
|
+
)
|
|
1507
|
+
|
|
1508
|
+
monkeypatch.setattr(
|
|
1509
|
+
"gaard_api.api.v1.admin.OpenAICompatibleClient",
|
|
1510
|
+
FakeOpenAICompatibleClient,
|
|
1511
|
+
)
|
|
1512
|
+
|
|
1513
|
+
response = admin_client.post(
|
|
1514
|
+
"/api/v1/admin/overview/widgets/title-suggestion",
|
|
1515
|
+
headers=headers,
|
|
1516
|
+
json={
|
|
1517
|
+
"question": "How many doctors are there by specialty?",
|
|
1518
|
+
"sql": "SELECT specialization, COUNT(*) FROM doctors GROUP BY specialization",
|
|
1519
|
+
},
|
|
1520
|
+
)
|
|
1521
|
+
|
|
1522
|
+
assert response.status_code == 200
|
|
1523
|
+
assert response.json() == {"title": "Doctors by Specialty"}
|
|
1524
|
+
assert FakeOpenAICompatibleClient.init_kwargs is not None
|
|
1525
|
+
assert FakeOpenAICompatibleClient.init_kwargs["api_key"] == "test-key"
|
|
1526
|
+
request = FakeOpenAICompatibleClient.requests[0]
|
|
1527
|
+
assert request.model == "title-model"
|
|
1528
|
+
assert request.temperature == 0.0
|
|
1529
|
+
assert request.messages[0].role == "system"
|
|
1530
|
+
assert "same language as the user question" in request.messages[0].content
|
|
1531
|
+
assert "How many doctors" in request.messages[1].content
|
|
1532
|
+
|
|
1533
|
+
|
|
1435
1534
|
def test_overview_widget_from_query_strips_datasource_qualifier(
|
|
1436
1535
|
admin_client: TestClient,
|
|
1437
1536
|
) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gaard_api-0.2.5 → gaard_api-0.2.6}/src/gaard_api/admin-web/assets/vendor/gridstack/gridstack-all.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|