gaard-api 0.1.1__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {gaard_api-0.1.1/src/gaard_api.egg-info → gaard_api-0.1.3}/PKG-INFO +3 -2
  2. {gaard_api-0.1.1 → gaard_api-0.1.3}/pyproject.toml +3 -2
  3. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/admin/services.py +113 -30
  4. gaard_api-0.1.3/src/gaard_api/admin-web/assets/main.js +2215 -0
  5. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/admin-web/assets/styles.css +62 -2
  6. gaard_api-0.1.3/src/gaard_api/admin-web/package.json +10 -0
  7. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/admin-web/src/main.ts +461 -51
  8. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/api/v1/admin.py +181 -13
  9. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/api/v1/prompts.py +4 -4
  10. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/api/v1/query.py +15 -11
  11. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/api/v1/schema.py +4 -4
  12. gaard_api-0.1.3/src/gaard_api/api_registry.py +266 -0
  13. gaard_api-0.1.3/src/gaard_api/extension_services.py +82 -0
  14. gaard_api-0.1.3/src/gaard_api/extensions.py +50 -0
  15. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/main.py +3 -0
  16. {gaard_api-0.1.1 → gaard_api-0.1.3/src/gaard_api.egg-info}/PKG-INFO +3 -2
  17. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api.egg-info/SOURCES.txt +5 -3
  18. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api.egg-info/requires.txt +2 -1
  19. {gaard_api-0.1.1 → gaard_api-0.1.3}/tests/test_admin_api.py +251 -0
  20. gaard_api-0.1.3/tests/test_api_registry.py +101 -0
  21. gaard_api-0.1.1/src/gaard_api/admin-web/assets/main.js +0 -2056
  22. gaard_api-0.1.1/src/gaard_api/dependencies/__init__.py +0 -0
  23. gaard_api-0.1.1/src/gaard_api/schemas/__init__.py +0 -0
  24. gaard_api-0.1.1/src/gaard_api/services/__init__.py +0 -0
  25. {gaard_api-0.1.1 → gaard_api-0.1.3}/MANIFEST.in +0 -0
  26. {gaard_api-0.1.1 → gaard_api-0.1.3}/README.md +0 -0
  27. {gaard_api-0.1.1 → gaard_api-0.1.3}/setup.cfg +0 -0
  28. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/__init__.py +0 -0
  29. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/admin/__init__.py +0 -0
  30. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/admin/database.py +0 -0
  31. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/admin/defaults.py +0 -0
  32. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/admin/models.py +0 -0
  33. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/admin/prompt_runtime.py +0 -0
  34. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/admin/security.py +0 -0
  35. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/admin-web/index.html +0 -0
  36. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/api/__init__.py +0 -0
  37. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/api/v1/__init__.py +0 -0
  38. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/cli.py +0 -0
  39. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/cli_commands.py +0 -0
  40. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/core/__init__.py +0 -0
  41. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/core/error_handlers.py +0 -0
  42. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/core/schema_cache.py +0 -0
  43. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/core/settings.py +0 -0
  44. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/example_data/__init__.py +0 -0
  45. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/example_data/medical_poc/__init__.py +0 -0
  46. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/example_data/medical_poc/schema.sql +0 -0
  47. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/example_data/medical_poc/seed.sql +0 -0
  48. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/example_database.py +0 -0
  49. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api/server_cli.py +0 -0
  50. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api.egg-info/dependency_links.txt +0 -0
  51. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api.egg-info/entry_points.txt +0 -0
  52. {gaard_api-0.1.1 → gaard_api-0.1.3}/src/gaard_api.egg-info/top_level.txt +0 -0
  53. {gaard_api-0.1.1 → gaard_api-0.1.3}/tests/test_error_handlers.py +0 -0
  54. {gaard_api-0.1.1 → gaard_api-0.1.3}/tests/test_investigation_error_messages.py +0 -0
  55. {gaard_api-0.1.1 → gaard_api-0.1.3}/tests/test_prompt_runtime.py +0 -0
  56. {gaard_api-0.1.1 → gaard_api-0.1.3}/tests/test_server_cli.py +0 -0
  57. {gaard_api-0.1.1 → gaard_api-0.1.3}/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.1
3
+ Version: 0.1.3
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.0
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
@@ -4,15 +4,16 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gaard-api"
7
- version = "0.1.1"
7
+ version = "0.1.3"
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.0",
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",
@@ -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
- prefixes = SUPPORTED_DATABASE_TYPES.get(database_type)
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
- if not database_url.startswith(prefixes):
60
- raise ValueError(
61
- f"Datasource URL for {database_type} must start with one of: "
62
- f"{', '.join(prefixes)}"
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:
@@ -827,6 +818,104 @@ def set_llm_runtime_config(
827
818
  return get_llm_runtime_config(session)
828
819
 
829
820
 
821
+ def build_llm_runtime_config(
822
+ session: Session,
823
+ *,
824
+ provider: str,
825
+ base_url: str,
826
+ api_key: str | None,
827
+ clear_api_key: bool,
828
+ model: str,
829
+ timeout_seconds: int | None,
830
+ extra_body: dict[str, Any],
831
+ ) -> LlmRuntimeConfig:
832
+ if provider != "openai-compatible":
833
+ raise ValueError("Only openai-compatible LLM provider is supported.")
834
+
835
+ current_config = get_llm_runtime_config(session)
836
+ normalized_base_url = str(base_url or "").strip()
837
+ normalized_model = str(model or "").strip()
838
+ if not normalized_base_url:
839
+ raise ValueError("LLM Base URL is required.")
840
+ if not normalized_model:
841
+ raise ValueError("LLM model is required.")
842
+ if timeout_seconds is not None and timeout_seconds < 1:
843
+ raise ValueError("LLM timeout seconds must be greater than 0.")
844
+ if not isinstance(extra_body, dict):
845
+ raise ValueError("LLM Extra body JSON must be an object.")
846
+
847
+ next_api_key = current_config.api_key
848
+ if clear_api_key:
849
+ next_api_key = "change-me"
850
+ elif api_key is not None and api_key.strip():
851
+ next_api_key = api_key.strip()
852
+
853
+ return LlmRuntimeConfig(
854
+ provider=provider,
855
+ base_url=normalized_base_url,
856
+ api_key=next_api_key,
857
+ model=normalized_model,
858
+ timeout_seconds=timeout_seconds or current_config.timeout_seconds,
859
+ extra_body=extra_body,
860
+ )
861
+
862
+
863
+ def test_llm_runtime_config(
864
+ session: Session,
865
+ *,
866
+ provider: str,
867
+ base_url: str,
868
+ api_key: str | None,
869
+ clear_api_key: bool,
870
+ model: str,
871
+ timeout_seconds: int | None,
872
+ extra_body: dict[str, Any],
873
+ ) -> dict[str, Any]:
874
+ config = build_llm_runtime_config(
875
+ session,
876
+ provider=provider,
877
+ base_url=base_url,
878
+ api_key=api_key,
879
+ clear_api_key=clear_api_key,
880
+ model=model,
881
+ timeout_seconds=timeout_seconds,
882
+ extra_body=extra_body,
883
+ )
884
+ if not config.api_key or config.api_key == "change-me":
885
+ raise ValueError("LLM API key is required for the connection test.")
886
+
887
+ client = OpenAICompatibleClient(
888
+ base_url=config.base_url,
889
+ api_key=config.api_key,
890
+ timeout_seconds=config.timeout_seconds,
891
+ )
892
+
893
+ try:
894
+ response = client.create_chat_completion(
895
+ ChatCompletionRequest(
896
+ model=config.model,
897
+ messages=[
898
+ ChatMessage(
899
+ role="user",
900
+ content="GAARD LLM connection test. Reply with OK.",
901
+ )
902
+ ],
903
+ temperature=0,
904
+ extra_body=config.extra_body,
905
+ )
906
+ )
907
+ except LlmProviderError as exc:
908
+ raise ValueError(str(exc)) from exc
909
+
910
+ if not response.content.strip():
911
+ raise ValueError("LLM provider returned an empty response.")
912
+
913
+ return {
914
+ "ok": True,
915
+ "model": response.model or config.model,
916
+ }
917
+
918
+
830
919
  def get_data_query_audit_retention_days(session: Session) -> int:
831
920
  value = get_setting(
832
921
  session,
@@ -1896,18 +1985,7 @@ def set_active_datasource_connector(
1896
1985
 
1897
1986
 
1898
1987
  def test_datasource_connection(connector: DatasourceConnector) -> None:
1899
- connect_args = (
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()
1988
+ get_connector_registry().get(connector.database_type).connection_tester(connector.database_url)
1911
1989
 
1912
1990
 
1913
1991
  def get_datasource_schema_cache(
@@ -1935,7 +2013,12 @@ def introspect_datasource_connector(
1935
2013
  connector: DatasourceConnector,
1936
2014
  actor: str,
1937
2015
  ) -> DatasourceSchemaCache:
1938
- schema = SQLAlchemySchemaIntrospector(connector.database_url).introspect()
2016
+ schema = (
2017
+ get_connector_registry()
2018
+ .get(connector.database_type)
2019
+ .introspector_factory(connector.database_url)
2020
+ .introspect()
2021
+ )
1939
2022
  existing = get_datasource_schema_cache(session, connector.id)
1940
2023
  existing_settings = (
1941
2024
  json_loads(existing.table_settings_json) if existing is not None else {}