glaip-sdk 0.6.10__py3-none-any.whl → 0.7.27__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.
- glaip_sdk/__init__.py +42 -5
- glaip_sdk/agents/base.py +295 -37
- glaip_sdk/agents/component.py +233 -0
- glaip_sdk/branding.py +113 -2
- glaip_sdk/cli/account_store.py +15 -0
- glaip_sdk/cli/auth.py +14 -8
- glaip_sdk/cli/commands/accounts.py +1 -1
- glaip_sdk/cli/commands/agents/__init__.py +116 -0
- glaip_sdk/cli/commands/agents/_common.py +562 -0
- glaip_sdk/cli/commands/agents/create.py +155 -0
- glaip_sdk/cli/commands/agents/delete.py +64 -0
- glaip_sdk/cli/commands/agents/get.py +89 -0
- glaip_sdk/cli/commands/agents/list.py +129 -0
- glaip_sdk/cli/commands/agents/run.py +264 -0
- glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
- glaip_sdk/cli/commands/agents/update.py +112 -0
- glaip_sdk/cli/commands/common_config.py +15 -12
- glaip_sdk/cli/commands/configure.py +1 -2
- glaip_sdk/cli/commands/mcps/__init__.py +94 -0
- glaip_sdk/cli/commands/mcps/_common.py +459 -0
- glaip_sdk/cli/commands/mcps/connect.py +82 -0
- glaip_sdk/cli/commands/mcps/create.py +152 -0
- glaip_sdk/cli/commands/mcps/delete.py +73 -0
- glaip_sdk/cli/commands/mcps/get.py +212 -0
- glaip_sdk/cli/commands/mcps/list.py +69 -0
- glaip_sdk/cli/commands/mcps/tools.py +235 -0
- glaip_sdk/cli/commands/mcps/update.py +190 -0
- glaip_sdk/cli/commands/models.py +2 -4
- glaip_sdk/cli/commands/shared/__init__.py +21 -0
- glaip_sdk/cli/commands/shared/formatters.py +91 -0
- glaip_sdk/cli/commands/tools/__init__.py +69 -0
- glaip_sdk/cli/commands/tools/_common.py +80 -0
- glaip_sdk/cli/commands/tools/create.py +228 -0
- glaip_sdk/cli/commands/tools/delete.py +61 -0
- glaip_sdk/cli/commands/tools/get.py +103 -0
- glaip_sdk/cli/commands/tools/list.py +69 -0
- glaip_sdk/cli/commands/tools/script.py +49 -0
- glaip_sdk/cli/commands/tools/update.py +102 -0
- glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
- glaip_sdk/cli/commands/transcripts/_common.py +9 -0
- glaip_sdk/cli/commands/transcripts/clear.py +5 -0
- glaip_sdk/cli/commands/transcripts/detail.py +5 -0
- glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
- glaip_sdk/cli/commands/update.py +163 -17
- glaip_sdk/cli/config.py +1 -0
- glaip_sdk/cli/core/output.py +12 -7
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/main.py +127 -39
- glaip_sdk/cli/pager.py +3 -3
- glaip_sdk/cli/resolution.py +2 -1
- glaip_sdk/cli/slash/accounts_controller.py +3 -1
- glaip_sdk/cli/slash/agent_session.py +1 -1
- glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
- glaip_sdk/cli/slash/session.py +343 -20
- glaip_sdk/cli/slash/tui/__init__.py +29 -1
- glaip_sdk/cli/slash/tui/accounts.tcss +97 -6
- glaip_sdk/cli/slash/tui/accounts_app.py +1117 -126
- glaip_sdk/cli/slash/tui/clipboard.py +316 -0
- glaip_sdk/cli/slash/tui/context.py +92 -0
- glaip_sdk/cli/slash/tui/indicators.py +341 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
- glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
- glaip_sdk/cli/slash/tui/loading.py +43 -21
- glaip_sdk/cli/slash/tui/remote_runs_app.py +178 -20
- glaip_sdk/cli/slash/tui/terminal.py +407 -0
- glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
- glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
- glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +388 -0
- glaip_sdk/cli/transcript/history.py +1 -1
- glaip_sdk/cli/transcript/viewer.py +1 -1
- glaip_sdk/cli/tui_settings.py +125 -0
- glaip_sdk/cli/update_notifier.py +215 -7
- glaip_sdk/cli/validators.py +1 -1
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agents.py +290 -16
- glaip_sdk/client/base.py +25 -0
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +7 -5
- glaip_sdk/client/mcps.py +44 -13
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +28 -48
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +414 -3
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/tools.py +52 -23
- glaip_sdk/config/constants.py +22 -2
- glaip_sdk/guardrails/__init__.py +80 -0
- glaip_sdk/guardrails/serializer.py +91 -0
- glaip_sdk/hitl/__init__.py +48 -0
- glaip_sdk/hitl/base.py +64 -0
- glaip_sdk/hitl/callback.py +43 -0
- glaip_sdk/hitl/local.py +121 -0
- glaip_sdk/hitl/remote.py +523 -0
- glaip_sdk/models/__init__.py +47 -1
- glaip_sdk/models/_provider_mappings.py +101 -0
- glaip_sdk/models/_validation.py +97 -0
- glaip_sdk/models/agent.py +2 -1
- glaip_sdk/models/agent_runs.py +2 -1
- glaip_sdk/models/constants.py +141 -0
- glaip_sdk/models/model.py +170 -0
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/payload_schemas/agent.py +1 -0
- glaip_sdk/payload_schemas/guardrails.py +34 -0
- glaip_sdk/ptc.py +145 -0
- glaip_sdk/registry/tool.py +270 -57
- glaip_sdk/runner/__init__.py +20 -3
- glaip_sdk/runner/deps.py +6 -6
- glaip_sdk/runner/langgraph.py +427 -39
- glaip_sdk/runner/logging_config.py +77 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +30 -9
- glaip_sdk/runner/ptc_adapter.py +98 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +25 -2
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- glaip_sdk/tools/base.py +67 -14
- glaip_sdk/utils/__init__.py +1 -0
- glaip_sdk/utils/agent_config.py +8 -2
- glaip_sdk/utils/bundler.py +138 -2
- glaip_sdk/utils/import_resolver.py +427 -49
- glaip_sdk/utils/rendering/renderer/base.py +58 -0
- glaip_sdk/utils/runtime_config.py +3 -2
- glaip_sdk/utils/sync.py +31 -11
- glaip_sdk/utils/tool_detection.py +274 -6
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- {glaip_sdk-0.6.10.dist-info → glaip_sdk-0.7.27.dist-info}/METADATA +51 -40
- glaip_sdk-0.7.27.dist-info/RECORD +227 -0
- {glaip_sdk-0.6.10.dist-info → glaip_sdk-0.7.27.dist-info}/WHEEL +2 -1
- glaip_sdk-0.7.27.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.7.27.dist-info/top_level.txt +1 -0
- glaip_sdk/cli/commands/agents.py +0 -1509
- glaip_sdk/cli/commands/mcps.py +0 -1356
- glaip_sdk/cli/commands/tools.py +0 -576
- glaip_sdk/cli/utils.py +0 -263
- glaip_sdk-0.6.10.dist-info/RECORD +0 -159
- glaip_sdk-0.6.10.dist-info/entry_points.txt +0 -3
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Logging configuration for CLI to suppress noisy dependency warnings.
|
|
2
|
+
|
|
3
|
+
This module provides centralized logging suppression for optional dependencies
|
|
4
|
+
that emit noisy warnings during CLI usage. Warnings are suppressed by default
|
|
5
|
+
but can be shown using GLAIP_LOG_LEVEL=DEBUG.
|
|
6
|
+
|
|
7
|
+
Authors:
|
|
8
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import warnings
|
|
14
|
+
|
|
15
|
+
NOISY_LOGGERS = ["transformers", "gllm_privacy", "google.cloud.aiplatform"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NameFilter(logging.Filter):
|
|
19
|
+
"""Filter logs by logger name prefix."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, prefixes: list[str]) -> None:
|
|
22
|
+
"""Initialize filter with logger name prefixes to suppress.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
prefixes: List of logger name prefixes to filter out.
|
|
26
|
+
"""
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.prefixes = prefixes
|
|
29
|
+
|
|
30
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
31
|
+
"""Filter log records by name prefix.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
record: Log record to filter.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
False if record should be suppressed, True otherwise.
|
|
38
|
+
"""
|
|
39
|
+
return not any(record.name.startswith(p) for p in self.prefixes)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def setup_cli_logging() -> None:
|
|
43
|
+
"""Suppress INFO from noisy third-party libraries.
|
|
44
|
+
|
|
45
|
+
Use GLAIP_LOG_LEVEL=DEBUG to see all warnings.
|
|
46
|
+
This function is idempotent - calling it multiple times is safe.
|
|
47
|
+
"""
|
|
48
|
+
# Check env level FIRST before any suppression
|
|
49
|
+
env_level = os.getenv("GLAIP_LOG_LEVEL", "").upper()
|
|
50
|
+
is_debug = env_level == "DEBUG"
|
|
51
|
+
|
|
52
|
+
if is_debug:
|
|
53
|
+
# Debug mode: show everything, no suppression
|
|
54
|
+
if env_level and hasattr(logging, env_level):
|
|
55
|
+
logging.basicConfig(level=getattr(logging, env_level))
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# Default mode: suppress noisy warnings
|
|
59
|
+
if env_level and hasattr(logging, env_level):
|
|
60
|
+
logging.basicConfig(level=getattr(logging, env_level))
|
|
61
|
+
|
|
62
|
+
# Add handler filter to suppress by name prefix (handles child loggers)
|
|
63
|
+
# Check if filter already exists to ensure idempotency
|
|
64
|
+
root_logger = logging.getLogger()
|
|
65
|
+
has_name_filter = any(isinstance(f, NameFilter) for h in root_logger.handlers for f in h.filters)
|
|
66
|
+
|
|
67
|
+
if not has_name_filter:
|
|
68
|
+
handler = logging.StreamHandler()
|
|
69
|
+
handler.addFilter(NameFilter(NOISY_LOGGERS))
|
|
70
|
+
root_logger.addHandler(handler)
|
|
71
|
+
|
|
72
|
+
# Suppress FutureWarning for GCS (idempotent - multiple calls are safe)
|
|
73
|
+
warnings.filterwarnings(
|
|
74
|
+
"ignore",
|
|
75
|
+
category=FutureWarning,
|
|
76
|
+
message=r".*google-cloud-storage.*",
|
|
77
|
+
)
|
|
@@ -60,19 +60,42 @@ class MCPConfigBuilder:
|
|
|
60
60
|
def _handle_custom_header(authentication: dict[str, Any]) -> dict[str, str] | None:
|
|
61
61
|
"""Handle custom-header auth type."""
|
|
62
62
|
headers = authentication.get("headers")
|
|
63
|
-
if isinstance(headers, dict)
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
if isinstance(headers, dict):
|
|
64
|
+
cleaned_headers = MCPConfigBuilder._clean_headers(headers)
|
|
65
|
+
if cleaned_headers:
|
|
66
|
+
return cleaned_headers
|
|
67
|
+
|
|
68
|
+
logger.warning("custom-header auth requires 'headers' dict with at least one non-null key/value")
|
|
66
69
|
return None
|
|
67
70
|
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _clean_headers(headers: dict[str, Any]) -> dict[str, str] | None:
|
|
73
|
+
"""Clean header dict by filtering None keys/values and stringifying values.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
headers: Raw headers dict potentially containing None or non-string values.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Cleaned headers dict with string keys/values, or None if empty after cleaning.
|
|
80
|
+
"""
|
|
81
|
+
cleaned: dict[str, str] = {}
|
|
82
|
+
for key, value in headers.items():
|
|
83
|
+
if key is None:
|
|
84
|
+
logger.warning("Dropping header with null key")
|
|
85
|
+
continue
|
|
86
|
+
if value is None:
|
|
87
|
+
logger.warning("Dropping header '%s' with null value", key)
|
|
88
|
+
continue
|
|
89
|
+
cleaned[str(key)] = str(value)
|
|
90
|
+
|
|
91
|
+
return cleaned if cleaned else None
|
|
92
|
+
|
|
68
93
|
@staticmethod
|
|
69
94
|
def _handle_bearer_token(authentication: dict[str, Any]) -> dict[str, str] | None:
|
|
70
95
|
"""Handle bearer-token auth type."""
|
|
71
|
-
# Check if headers provided directly
|
|
72
96
|
headers = authentication.get("headers")
|
|
73
97
|
if isinstance(headers, dict):
|
|
74
|
-
return headers
|
|
75
|
-
# Otherwise build from token
|
|
98
|
+
return MCPConfigBuilder._clean_headers(headers)
|
|
76
99
|
token = authentication.get("token")
|
|
77
100
|
if token:
|
|
78
101
|
return {"Authorization": f"Bearer {token}"}
|
|
@@ -82,11 +105,9 @@ class MCPConfigBuilder:
|
|
|
82
105
|
@staticmethod
|
|
83
106
|
def _handle_api_key(authentication: dict[str, Any]) -> dict[str, str] | None:
|
|
84
107
|
"""Handle api-key auth type."""
|
|
85
|
-
# Check if headers provided directly
|
|
86
108
|
headers = authentication.get("headers")
|
|
87
109
|
if isinstance(headers, dict):
|
|
88
|
-
return headers
|
|
89
|
-
# Otherwise build from key/value
|
|
110
|
+
return MCPConfigBuilder._clean_headers(headers)
|
|
90
111
|
key = authentication.get("key")
|
|
91
112
|
value = authentication.get("value")
|
|
92
113
|
if key and value:
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""PTC adapter for local runner integration.
|
|
2
|
+
|
|
3
|
+
This module provides validation and normalization of PTC configuration
|
|
4
|
+
for use in the local LangGraph runner. It ensures PTC is configured
|
|
5
|
+
correctly and rejects unsupported configuration sources.
|
|
6
|
+
|
|
7
|
+
Authors:
|
|
8
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from glaip_sdk.exceptions import ValidationError
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from glaip_sdk.ptc import PTC
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_ptc_for_local_run(
|
|
22
|
+
agent_ptc: PTC | None,
|
|
23
|
+
agent_config_ptc: Any | None,
|
|
24
|
+
runtime_config_ptc: Any | None,
|
|
25
|
+
) -> PTC | None:
|
|
26
|
+
"""Validate PTC configuration for local runs.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
agent_ptc: PTC object from Agent.ptc parameter.
|
|
30
|
+
agent_config_ptc: PTC from agent_config (should be None for local).
|
|
31
|
+
runtime_config_ptc: PTC from runtime_config (should be None for v1).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Validated PTC object if enabled, None otherwise.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValidationError: If agent_config.ptc or runtime_config.ptc are provided,
|
|
38
|
+
or if agent_ptc is not a PTC instance when provided.
|
|
39
|
+
"""
|
|
40
|
+
if agent_config_ptc is not None:
|
|
41
|
+
msg = (
|
|
42
|
+
"PTC configuration via agent_config.ptc is not supported for local runs. "
|
|
43
|
+
"Please configure PTC using the Agent.ptc parameter instead: "
|
|
44
|
+
"Agent(name='...', ptc=PTC(enabled=True), ...)"
|
|
45
|
+
)
|
|
46
|
+
raise ValidationError(msg)
|
|
47
|
+
|
|
48
|
+
if runtime_config_ptc is not None:
|
|
49
|
+
msg = (
|
|
50
|
+
"PTC configuration via runtime_config.ptc is not supported in v1. "
|
|
51
|
+
"PTC configuration must be set at Agent initialization time using "
|
|
52
|
+
"the Agent.ptc parameter and cannot be overridden at runtime. "
|
|
53
|
+
"This preserves local/remote parity and prevents ambiguous precedence."
|
|
54
|
+
)
|
|
55
|
+
raise ValidationError(msg)
|
|
56
|
+
|
|
57
|
+
if agent_ptc is None:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
from glaip_sdk.ptc import PTC # noqa: PLC0415
|
|
61
|
+
|
|
62
|
+
if not isinstance(agent_ptc, PTC):
|
|
63
|
+
msg = (
|
|
64
|
+
f"Agent.ptc must be a PTC instance, got {type(agent_ptc).__name__}. "
|
|
65
|
+
"Example: Agent(name='...', ptc=PTC(enabled=True), ...)"
|
|
66
|
+
)
|
|
67
|
+
raise ValidationError(msg)
|
|
68
|
+
|
|
69
|
+
if not agent_ptc.enabled:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
return agent_ptc
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def normalize_ptc_for_aip_agents(ptc: PTC | None) -> Any:
|
|
76
|
+
"""Normalize PTC config for aip-agents LangGraphReactAgent.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
ptc: Validated PTC object or None.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
PTCSandboxConfig for aip-agents or None if PTC disabled.
|
|
83
|
+
"""
|
|
84
|
+
if ptc is None or not ptc.enabled:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
from aip_agents.ptc import PromptConfig, PTCSandboxConfig # noqa: PLC0415
|
|
88
|
+
|
|
89
|
+
# Build PromptConfig if prompt dict is provided.
|
|
90
|
+
prompt_config = None
|
|
91
|
+
if ptc.prompt is not None:
|
|
92
|
+
prompt_config = PromptConfig(**ptc.prompt)
|
|
93
|
+
|
|
94
|
+
return PTCSandboxConfig(
|
|
95
|
+
enabled=ptc.enabled,
|
|
96
|
+
sandbox_timeout=ptc.sandbox_timeout,
|
|
97
|
+
prompt=prompt_config,
|
|
98
|
+
)
|
|
@@ -9,9 +9,10 @@ Authors:
|
|
|
9
9
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
-
from glaip_sdk.runner.tool_adapter.base_tool_adapter import BaseToolAdapter
|
|
13
12
|
from gllm_core.utils import LoggerManager
|
|
14
13
|
|
|
14
|
+
from glaip_sdk.runner.tool_adapter.base_tool_adapter import BaseToolAdapter
|
|
15
|
+
|
|
15
16
|
logger = LoggerManager().get_logger(__name__)
|
|
16
17
|
|
|
17
18
|
# Constant for unknown tool name placeholder
|
|
@@ -73,8 +74,30 @@ class LangChainToolAdapter(BaseToolAdapter):
|
|
|
73
74
|
if self._is_langchain_tool(tool_ref):
|
|
74
75
|
return self._instantiate_langchain_tool(tool_ref)
|
|
75
76
|
|
|
76
|
-
# 3.
|
|
77
|
+
# 3. Native tools with discovered class
|
|
77
78
|
if self._is_platform_tool(tool_ref):
|
|
79
|
+
# Try to discover local implementation for native tool
|
|
80
|
+
from glaip_sdk.utils.tool_detection import ( # noqa: PLC0415
|
|
81
|
+
find_aip_agents_tool_class,
|
|
82
|
+
get_tool_name,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Get tool name from reference
|
|
86
|
+
tool_name = get_tool_name(tool_ref) if not isinstance(tool_ref, str) else tool_ref
|
|
87
|
+
|
|
88
|
+
if tool_name:
|
|
89
|
+
discovered_class = find_aip_agents_tool_class(tool_name)
|
|
90
|
+
if discovered_class:
|
|
91
|
+
logger.info("Instantiating native tool locally: %s", tool_name)
|
|
92
|
+
try:
|
|
93
|
+
return discovered_class()
|
|
94
|
+
except TypeError as exc:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"Could not instantiate native tool '{tool_name}'. "
|
|
97
|
+
"Ensure it has a zero-argument constructor or adjust the instantiation logic."
|
|
98
|
+
) from exc
|
|
99
|
+
|
|
100
|
+
# If no local class found, raise platform tool error
|
|
78
101
|
raise ValueError(self._get_platform_tool_error(tool_ref))
|
|
79
102
|
|
|
80
103
|
# 4. Unknown type
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Schedules runtime package.
|
|
2
|
+
|
|
3
|
+
This package contains runtime schedule resource objects (class-based) that
|
|
4
|
+
encapsulate behavior and API interactions via attached clients.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from glaip_sdk.schedules.base import (
|
|
11
|
+
Schedule,
|
|
12
|
+
ScheduleListResult,
|
|
13
|
+
ScheduleRun,
|
|
14
|
+
ScheduleRunListResult,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Schedule",
|
|
19
|
+
"ScheduleListResult",
|
|
20
|
+
"ScheduleRun",
|
|
21
|
+
"ScheduleRunListResult",
|
|
22
|
+
]
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Schedule runtime resources.
|
|
2
|
+
|
|
3
|
+
This module contains class-based runtime resources for schedules.
|
|
4
|
+
|
|
5
|
+
The runtime resources:
|
|
6
|
+
- Are not Pydantic models.
|
|
7
|
+
- Are returned from public client APIs.
|
|
8
|
+
- Delegate API operations to a bound ScheduleClient.
|
|
9
|
+
|
|
10
|
+
Authors:
|
|
11
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from glaip_sdk.models.agent_runs import RunStatus
|
|
21
|
+
from glaip_sdk.models.schedule import (
|
|
22
|
+
ScheduleConfig,
|
|
23
|
+
ScheduleMetadata,
|
|
24
|
+
ScheduleResponse,
|
|
25
|
+
ScheduleRunResponse,
|
|
26
|
+
ScheduleRunResult,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
30
|
+
from glaip_sdk.client.schedules import ScheduleClient
|
|
31
|
+
|
|
32
|
+
_SCHEDULE_CLIENT_REQUIRED_MSG = "No client available. Use client.schedules.get() to get a client-connected schedule."
|
|
33
|
+
_SCHEDULE_RUN_CLIENT_REQUIRED_MSG = (
|
|
34
|
+
"No client available. Use client.schedules.list_runs() to get a client-connected schedule run."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Schedule:
|
|
39
|
+
"""Runtime schedule resource.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
id (str): The schedule ID.
|
|
43
|
+
next_run_time (str | None): Next run time as returned by the API.
|
|
44
|
+
time_until_next_run (str | None): Human readable duration until next run.
|
|
45
|
+
metadata (ScheduleMetadata | None): Schedule metadata.
|
|
46
|
+
created_at (datetime | None): Creation timestamp.
|
|
47
|
+
updated_at (datetime | None): Update timestamp.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
id: str,
|
|
54
|
+
next_run_time: str | None = None,
|
|
55
|
+
time_until_next_run: str | None = None,
|
|
56
|
+
metadata: ScheduleMetadata | None = None,
|
|
57
|
+
created_at: datetime | None = None,
|
|
58
|
+
updated_at: datetime | None = None,
|
|
59
|
+
_client: ScheduleClient | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Initialize a runtime Schedule."""
|
|
62
|
+
self.id = id
|
|
63
|
+
self.next_run_time = next_run_time
|
|
64
|
+
self.time_until_next_run = time_until_next_run
|
|
65
|
+
self.metadata = metadata
|
|
66
|
+
self.created_at = created_at
|
|
67
|
+
self.updated_at = updated_at
|
|
68
|
+
self._client = _client
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_response(cls, response: ScheduleResponse, *, client: ScheduleClient) -> Schedule:
|
|
72
|
+
"""Build a runtime Schedule from a DTO response.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
response: Parsed schedule response DTO.
|
|
76
|
+
client: ScheduleClient to bind.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Runtime Schedule.
|
|
80
|
+
"""
|
|
81
|
+
return cls(
|
|
82
|
+
id=response.id,
|
|
83
|
+
next_run_time=response.next_run_time,
|
|
84
|
+
time_until_next_run=response.time_until_next_run,
|
|
85
|
+
metadata=response.metadata,
|
|
86
|
+
created_at=response.created_at,
|
|
87
|
+
updated_at=response.updated_at,
|
|
88
|
+
_client=client,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def agent_id(self) -> str | None:
|
|
93
|
+
"""Agent ID derived from metadata."""
|
|
94
|
+
return self.metadata.agent_id if self.metadata else None
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def input(self) -> str | None:
|
|
98
|
+
"""Input text derived from metadata."""
|
|
99
|
+
return self.metadata.input if self.metadata else None
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def schedule_config(self) -> ScheduleConfig | None:
|
|
103
|
+
"""Schedule configuration derived from metadata."""
|
|
104
|
+
return self.metadata.schedule if self.metadata else None
|
|
105
|
+
|
|
106
|
+
def update(
|
|
107
|
+
self,
|
|
108
|
+
*,
|
|
109
|
+
input: str | None = None,
|
|
110
|
+
schedule: ScheduleConfig | dict[str, str] | str | None = None,
|
|
111
|
+
) -> Schedule:
|
|
112
|
+
"""Update this schedule."""
|
|
113
|
+
if self._client is None:
|
|
114
|
+
raise RuntimeError(_SCHEDULE_CLIENT_REQUIRED_MSG)
|
|
115
|
+
return self._client.update(self.id, input=input, schedule=schedule)
|
|
116
|
+
|
|
117
|
+
def delete(self) -> None:
|
|
118
|
+
"""Delete this schedule."""
|
|
119
|
+
if self._client is None:
|
|
120
|
+
raise RuntimeError(_SCHEDULE_CLIENT_REQUIRED_MSG)
|
|
121
|
+
self._client.delete(self.id)
|
|
122
|
+
|
|
123
|
+
def list_runs(
|
|
124
|
+
self,
|
|
125
|
+
*,
|
|
126
|
+
status: RunStatus | None = None,
|
|
127
|
+
limit: int | None = None,
|
|
128
|
+
page: int | None = None,
|
|
129
|
+
) -> ScheduleRunListResult:
|
|
130
|
+
"""List runs for this schedule."""
|
|
131
|
+
if self._client is None:
|
|
132
|
+
raise RuntimeError(_SCHEDULE_CLIENT_REQUIRED_MSG)
|
|
133
|
+
if self.agent_id is None:
|
|
134
|
+
raise ValueError("Schedule has no agent_id")
|
|
135
|
+
return self._client.list_runs(
|
|
136
|
+
self.agent_id,
|
|
137
|
+
schedule_id=self.id,
|
|
138
|
+
status=status,
|
|
139
|
+
limit=limit,
|
|
140
|
+
page=page,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def __repr__(self) -> str:
|
|
144
|
+
"""Return a developer-friendly representation."""
|
|
145
|
+
parts: list[str] = [f"id={self.id!r}"]
|
|
146
|
+
if self.agent_id is not None:
|
|
147
|
+
parts.append(f"agent_id={self.agent_id!r}")
|
|
148
|
+
if self.next_run_time is not None:
|
|
149
|
+
parts.append(f"next_run_time={self.next_run_time!r}")
|
|
150
|
+
if self.time_until_next_run is not None:
|
|
151
|
+
parts.append(f"time_until_next_run={self.time_until_next_run!r}")
|
|
152
|
+
if self.created_at is not None:
|
|
153
|
+
parts.append(f"created_at={self.created_at!r}")
|
|
154
|
+
return f"Schedule({', '.join(parts)})"
|
|
155
|
+
|
|
156
|
+
def __str__(self) -> str:
|
|
157
|
+
"""Return a readable string representation."""
|
|
158
|
+
return self.__repr__()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class ScheduleRun:
|
|
162
|
+
"""Runtime schedule run resource."""
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
*,
|
|
167
|
+
id: str,
|
|
168
|
+
agent_id: str,
|
|
169
|
+
schedule_id: str | None = None,
|
|
170
|
+
status: RunStatus,
|
|
171
|
+
run_type: str | None = None,
|
|
172
|
+
started_at: datetime | None = None,
|
|
173
|
+
completed_at: datetime | None = None,
|
|
174
|
+
input: str | None = None,
|
|
175
|
+
config: ScheduleConfig | dict[str, str] | None = None,
|
|
176
|
+
created_at: datetime | None = None,
|
|
177
|
+
updated_at: datetime | None = None,
|
|
178
|
+
_client: ScheduleClient | None = None,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Initialize a runtime ScheduleRun."""
|
|
181
|
+
self.id = id
|
|
182
|
+
self.agent_id = agent_id
|
|
183
|
+
self.schedule_id = schedule_id
|
|
184
|
+
self.status = status
|
|
185
|
+
self.run_type = run_type
|
|
186
|
+
self.started_at = started_at
|
|
187
|
+
self.completed_at = completed_at
|
|
188
|
+
self.input = input
|
|
189
|
+
self.config = config
|
|
190
|
+
self.created_at = created_at
|
|
191
|
+
self.updated_at = updated_at
|
|
192
|
+
self._client = _client
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def from_response(cls, response: ScheduleRunResponse, *, client: ScheduleClient) -> ScheduleRun:
|
|
196
|
+
"""Build a runtime ScheduleRun from a DTO response."""
|
|
197
|
+
return cls(
|
|
198
|
+
id=response.id,
|
|
199
|
+
agent_id=response.agent_id,
|
|
200
|
+
schedule_id=response.schedule_id,
|
|
201
|
+
status=response.status,
|
|
202
|
+
run_type=response.run_type,
|
|
203
|
+
started_at=response.started_at,
|
|
204
|
+
completed_at=response.completed_at,
|
|
205
|
+
input=response.input,
|
|
206
|
+
config=response.config,
|
|
207
|
+
created_at=response.created_at,
|
|
208
|
+
updated_at=response.updated_at,
|
|
209
|
+
_client=client,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def get_result(self) -> ScheduleRunResult:
|
|
213
|
+
"""Retrieve the full output payload for this run."""
|
|
214
|
+
if self._client is None:
|
|
215
|
+
raise RuntimeError(_SCHEDULE_RUN_CLIENT_REQUIRED_MSG)
|
|
216
|
+
if self.agent_id is None:
|
|
217
|
+
raise ValueError("Schedule run has no agent_id")
|
|
218
|
+
return self._client.get_run_result(self.agent_id, self.id)
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def duration(self) -> str | None:
|
|
222
|
+
"""Formatted duration (HH:MM:SS) when both timestamps are available."""
|
|
223
|
+
if not self.started_at or not self.completed_at:
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
total_seconds = int((self.completed_at - self.started_at).total_seconds())
|
|
227
|
+
minutes, seconds = divmod(total_seconds, 60)
|
|
228
|
+
hours, minutes = divmod(minutes, 60)
|
|
229
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
230
|
+
|
|
231
|
+
def __repr__(self) -> str:
|
|
232
|
+
"""Return a developer-friendly representation."""
|
|
233
|
+
parts: list[str] = [f"id={self.id!r}", f"status={self.status!r}"]
|
|
234
|
+
if self.started_at is not None:
|
|
235
|
+
parts.append(f"started_at={self.started_at.isoformat()!r}")
|
|
236
|
+
duration = self.duration
|
|
237
|
+
if duration is not None:
|
|
238
|
+
parts.append(f"duration={duration!r}")
|
|
239
|
+
return f"ScheduleRun({', '.join(parts)})"
|
|
240
|
+
|
|
241
|
+
def __str__(self) -> str:
|
|
242
|
+
"""Return a readable string representation."""
|
|
243
|
+
return self.__repr__()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass
|
|
247
|
+
class ScheduleListResult:
|
|
248
|
+
"""Paginated list wrapper for runtime schedules."""
|
|
249
|
+
|
|
250
|
+
items: list[Schedule]
|
|
251
|
+
total: int | None = field(default=None)
|
|
252
|
+
page: int | None = field(default=None)
|
|
253
|
+
limit: int | None = field(default=None)
|
|
254
|
+
has_next: bool | None = field(default=None)
|
|
255
|
+
has_prev: bool | None = field(default=None)
|
|
256
|
+
|
|
257
|
+
def __iter__(self):
|
|
258
|
+
"""Iterate over schedules."""
|
|
259
|
+
yield from self.items
|
|
260
|
+
|
|
261
|
+
def __len__(self) -> int:
|
|
262
|
+
"""Return the number of schedules in this page."""
|
|
263
|
+
return self.items.__len__()
|
|
264
|
+
|
|
265
|
+
def __getitem__(self, index: int) -> Schedule:
|
|
266
|
+
"""Return the schedule at the given index."""
|
|
267
|
+
return self.items[index]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass
|
|
271
|
+
class ScheduleRunListResult:
|
|
272
|
+
"""Paginated list wrapper for runtime schedule runs."""
|
|
273
|
+
|
|
274
|
+
items: list[ScheduleRun]
|
|
275
|
+
total: int | None = field(default=None)
|
|
276
|
+
page: int | None = field(default=None)
|
|
277
|
+
limit: int | None = field(default=None)
|
|
278
|
+
has_next: bool | None = field(default=None)
|
|
279
|
+
has_prev: bool | None = field(default=None)
|
|
280
|
+
|
|
281
|
+
def __iter__(self):
|
|
282
|
+
"""Iterate over schedule runs."""
|
|
283
|
+
yield from self.items
|
|
284
|
+
|
|
285
|
+
def __len__(self) -> int:
|
|
286
|
+
"""Return the number of runs in this page."""
|
|
287
|
+
return self.items.__len__()
|
|
288
|
+
|
|
289
|
+
def __getitem__(self, index: int) -> ScheduleRun:
|
|
290
|
+
"""Return the run at the given index."""
|
|
291
|
+
return self.items[index]
|