glaip-sdk 0.6.5b3__py3-none-any.whl → 0.7.17__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.
Files changed (145) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. glaip_sdk/agents/base.py +362 -39
  3. glaip_sdk/branding.py +113 -2
  4. glaip_sdk/cli/account_store.py +15 -0
  5. glaip_sdk/cli/auth.py +14 -8
  6. glaip_sdk/cli/commands/accounts.py +1 -1
  7. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  8. glaip_sdk/cli/commands/agents/_common.py +562 -0
  9. glaip_sdk/cli/commands/agents/create.py +155 -0
  10. glaip_sdk/cli/commands/agents/delete.py +64 -0
  11. glaip_sdk/cli/commands/agents/get.py +89 -0
  12. glaip_sdk/cli/commands/agents/list.py +129 -0
  13. glaip_sdk/cli/commands/agents/run.py +264 -0
  14. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  15. glaip_sdk/cli/commands/agents/update.py +112 -0
  16. glaip_sdk/cli/commands/common_config.py +15 -12
  17. glaip_sdk/cli/commands/configure.py +2 -3
  18. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  19. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  20. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  21. glaip_sdk/cli/commands/mcps/create.py +152 -0
  22. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  23. glaip_sdk/cli/commands/mcps/get.py +212 -0
  24. glaip_sdk/cli/commands/mcps/list.py +69 -0
  25. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  26. glaip_sdk/cli/commands/mcps/update.py +190 -0
  27. glaip_sdk/cli/commands/models.py +2 -4
  28. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  29. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  30. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  31. glaip_sdk/cli/commands/tools/_common.py +80 -0
  32. glaip_sdk/cli/commands/tools/create.py +228 -0
  33. glaip_sdk/cli/commands/tools/delete.py +61 -0
  34. glaip_sdk/cli/commands/tools/get.py +103 -0
  35. glaip_sdk/cli/commands/tools/list.py +69 -0
  36. glaip_sdk/cli/commands/tools/script.py +49 -0
  37. glaip_sdk/cli/commands/tools/update.py +102 -0
  38. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  39. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  40. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  41. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  42. glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
  43. glaip_sdk/cli/commands/update.py +163 -17
  44. glaip_sdk/cli/config.py +1 -0
  45. glaip_sdk/cli/core/output.py +12 -7
  46. glaip_sdk/cli/entrypoint.py +20 -0
  47. glaip_sdk/cli/main.py +127 -39
  48. glaip_sdk/cli/pager.py +3 -3
  49. glaip_sdk/cli/resolution.py +2 -1
  50. glaip_sdk/cli/slash/accounts_controller.py +112 -32
  51. glaip_sdk/cli/slash/agent_session.py +5 -2
  52. glaip_sdk/cli/slash/prompt.py +11 -0
  53. glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
  54. glaip_sdk/cli/slash/session.py +375 -25
  55. glaip_sdk/cli/slash/tui/__init__.py +28 -1
  56. glaip_sdk/cli/slash/tui/accounts.tcss +97 -6
  57. glaip_sdk/cli/slash/tui/accounts_app.py +1107 -126
  58. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  59. glaip_sdk/cli/slash/tui/context.py +92 -0
  60. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  61. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  62. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  63. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  64. glaip_sdk/cli/slash/tui/loading.py +43 -21
  65. glaip_sdk/cli/slash/tui/remote_runs_app.py +152 -20
  66. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  67. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  68. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  69. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  70. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  71. glaip_sdk/cli/slash/tui/toast.py +388 -0
  72. glaip_sdk/cli/transcript/history.py +1 -1
  73. glaip_sdk/cli/transcript/viewer.py +5 -3
  74. glaip_sdk/cli/tui_settings.py +125 -0
  75. glaip_sdk/cli/update_notifier.py +215 -7
  76. glaip_sdk/cli/validators.py +1 -1
  77. glaip_sdk/client/__init__.py +2 -1
  78. glaip_sdk/client/_schedule_payloads.py +89 -0
  79. glaip_sdk/client/agents.py +290 -16
  80. glaip_sdk/client/base.py +25 -0
  81. glaip_sdk/client/hitl.py +136 -0
  82. glaip_sdk/client/main.py +7 -5
  83. glaip_sdk/client/mcps.py +44 -13
  84. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  85. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +28 -48
  86. glaip_sdk/client/payloads/agent/responses.py +43 -0
  87. glaip_sdk/client/run_rendering.py +414 -3
  88. glaip_sdk/client/schedules.py +439 -0
  89. glaip_sdk/client/tools.py +57 -26
  90. glaip_sdk/config/constants.py +22 -2
  91. glaip_sdk/guardrails/__init__.py +80 -0
  92. glaip_sdk/guardrails/serializer.py +89 -0
  93. glaip_sdk/hitl/__init__.py +48 -0
  94. glaip_sdk/hitl/base.py +64 -0
  95. glaip_sdk/hitl/callback.py +43 -0
  96. glaip_sdk/hitl/local.py +121 -0
  97. glaip_sdk/hitl/remote.py +523 -0
  98. glaip_sdk/models/__init__.py +47 -1
  99. glaip_sdk/models/_provider_mappings.py +101 -0
  100. glaip_sdk/models/_validation.py +97 -0
  101. glaip_sdk/models/agent.py +2 -1
  102. glaip_sdk/models/agent_runs.py +2 -1
  103. glaip_sdk/models/constants.py +141 -0
  104. glaip_sdk/models/model.py +170 -0
  105. glaip_sdk/models/schedule.py +224 -0
  106. glaip_sdk/payload_schemas/agent.py +1 -0
  107. glaip_sdk/payload_schemas/guardrails.py +34 -0
  108. glaip_sdk/registry/tool.py +273 -66
  109. glaip_sdk/runner/__init__.py +76 -0
  110. glaip_sdk/runner/base.py +84 -0
  111. glaip_sdk/runner/deps.py +115 -0
  112. glaip_sdk/runner/langgraph.py +1055 -0
  113. glaip_sdk/runner/logging_config.py +77 -0
  114. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  115. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  116. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  117. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  118. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  119. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  120. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  121. glaip_sdk/schedules/__init__.py +22 -0
  122. glaip_sdk/schedules/base.py +291 -0
  123. glaip_sdk/tools/base.py +67 -14
  124. glaip_sdk/utils/__init__.py +1 -0
  125. glaip_sdk/utils/a2a/__init__.py +34 -0
  126. glaip_sdk/utils/a2a/event_processor.py +188 -0
  127. glaip_sdk/utils/agent_config.py +8 -2
  128. glaip_sdk/utils/bundler.py +138 -2
  129. glaip_sdk/utils/import_resolver.py +43 -11
  130. glaip_sdk/utils/rendering/renderer/base.py +58 -0
  131. glaip_sdk/utils/runtime_config.py +120 -0
  132. glaip_sdk/utils/sync.py +31 -11
  133. glaip_sdk/utils/tool_detection.py +301 -0
  134. glaip_sdk/utils/tool_storage_provider.py +140 -0
  135. {glaip_sdk-0.6.5b3.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +49 -38
  136. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  137. {glaip_sdk-0.6.5b3.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  138. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  139. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  140. glaip_sdk/cli/commands/agents.py +0 -1509
  141. glaip_sdk/cli/commands/mcps.py +0 -1356
  142. glaip_sdk/cli/commands/tools.py +0 -576
  143. glaip_sdk/cli/utils.py +0 -263
  144. glaip_sdk-0.6.5b3.dist-info/RECORD +0 -145
  145. glaip_sdk-0.6.5b3.dist-info/entry_points.txt +0 -3
@@ -0,0 +1,242 @@
1
+ """LangChain tool adapter for local agent runtime.
2
+
3
+ This module handles adaptation of glaip-sdk tool references to LangChain
4
+ BaseTool instances for local execution with aip-agents (LangGraph backend).
5
+
6
+ Authors:
7
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
8
+ """
9
+
10
+ from typing import Any
11
+
12
+ from gllm_core.utils import LoggerManager
13
+
14
+ from glaip_sdk.runner.tool_adapter.base_tool_adapter import BaseToolAdapter
15
+
16
+ logger = LoggerManager().get_logger(__name__)
17
+
18
+ # Constant for unknown tool name placeholder
19
+ _UNKNOWN_TOOL_NAME = "<unknown>"
20
+
21
+
22
+ class LangChainToolAdapter(BaseToolAdapter):
23
+ """Adapts glaip-sdk tools to LangChain BaseTool format for aip-agents.
24
+
25
+ Handles:
26
+ - LangChain BaseTool classes → instantiate
27
+ - LangChain BaseTool instances → return as-is
28
+ - Tool.from_langchain() → extract underlying tool
29
+ - @tool_plugin decorator → ignore (just metadata)
30
+
31
+ Rejects:
32
+ - Tool.from_native() → platform-specific
33
+ - String tool names → platform-specific
34
+ """
35
+
36
+ def adapt_tools(self, tool_refs: list[Any]) -> list[Any]:
37
+ """Adapt tool references to LangChain BaseTool instances.
38
+
39
+ Args:
40
+ tool_refs: List of tool references from Agent definition.
41
+
42
+ Returns:
43
+ List of LangChain BaseTool instances.
44
+
45
+ Raises:
46
+ ValueError: If tool is not supported in local mode.
47
+ """
48
+ langchain_tools = []
49
+
50
+ for tool_ref in tool_refs:
51
+ langchain_tool = self._adapt_single_tool(tool_ref)
52
+ langchain_tools.append(langchain_tool)
53
+
54
+ logger.debug("Adapted %d tools to LangChain format", len(langchain_tools))
55
+ return langchain_tools
56
+
57
+ def _adapt_single_tool(self, tool_ref: Any) -> Any:
58
+ """Adapt a single tool reference.
59
+
60
+ Args:
61
+ tool_ref: Single tool reference to adapt.
62
+
63
+ Returns:
64
+ LangChain BaseTool instance.
65
+
66
+ Raises:
67
+ ValueError: If tool is not supported.
68
+ """
69
+ # 1. Tool.from_langchain() wrapper
70
+ if self._is_tool_wrapper(tool_ref):
71
+ return self._extract_from_wrapper(tool_ref)
72
+
73
+ # 2. Direct LangChain BaseTool
74
+ if self._is_langchain_tool(tool_ref):
75
+ return self._instantiate_langchain_tool(tool_ref)
76
+
77
+ # 3. Native tools with discovered class
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
101
+ raise ValueError(self._get_platform_tool_error(tool_ref))
102
+
103
+ # 4. Unknown type
104
+ raise ValueError(
105
+ f"Unsupported tool type for local mode: {type(tool_ref)}. "
106
+ "Local mode only supports LangChain BaseTool classes/instances."
107
+ )
108
+
109
+ def _has_explicit_attr(self, ref: Any, attr: str) -> bool:
110
+ """Check if attribute is explicitly set on the object.
111
+
112
+ This avoids false positives from objects like MagicMock, where hasattr()
113
+ can return True even if the attribute was never set.
114
+ """
115
+ ref_dict = getattr(ref, "__dict__", None)
116
+ return isinstance(ref_dict, dict) and attr in ref_dict
117
+
118
+ def _is_tool_wrapper(self, ref: Any) -> bool:
119
+ """Check if ref is a Tool.from_langchain() wrapper.
120
+
121
+ Args:
122
+ ref: Object to check.
123
+
124
+ Returns:
125
+ True if ref is a Tool.from_langchain() wrapper.
126
+ """
127
+ if self._has_explicit_attr(ref, "langchain_tool") and hasattr(ref, "id") and hasattr(ref, "name"):
128
+ return True
129
+
130
+ if self._has_explicit_attr(ref, "tool_class"):
131
+ return getattr(ref, "tool_class", None) is not None
132
+
133
+ return False
134
+
135
+ def _extract_from_wrapper(self, wrapper: Any) -> Any:
136
+ """Extract underlying LangChain tool from Tool.from_langchain().
137
+
138
+ Args:
139
+ wrapper: Tool.from_langchain() wrapper object.
140
+
141
+ Returns:
142
+ LangChain BaseTool instance.
143
+
144
+ Raises:
145
+ ValueError: If the wrapper's underlying tool is not a valid LangChain tool.
146
+ """
147
+ langchain_tool = getattr(wrapper, "langchain_tool", None)
148
+ if langchain_tool is None:
149
+ langchain_tool = getattr(wrapper, "tool_class", None)
150
+
151
+ # Validate the extracted object is a valid LangChain tool
152
+ if langchain_tool is None:
153
+ wrapper_name = getattr(wrapper, "name", _UNKNOWN_TOOL_NAME)
154
+ raise ValueError(
155
+ f"Tool wrapper '{wrapper_name}' does not contain a valid LangChain tool. "
156
+ "Ensure Tool.from_langchain() was called with a LangChain BaseTool class or instance."
157
+ )
158
+
159
+ # Validate it's actually a LangChain tool (class or instance)
160
+ if not self._is_langchain_tool(langchain_tool):
161
+ wrapper_name = getattr(wrapper, "name", _UNKNOWN_TOOL_NAME)
162
+ raise ValueError(
163
+ f"Tool wrapper '{wrapper_name}' contains an invalid tool type: {type(langchain_tool)}. "
164
+ "Expected a LangChain BaseTool class or instance."
165
+ )
166
+
167
+ # If it's a class, instantiate it
168
+ if isinstance(langchain_tool, type):
169
+ langchain_tool = langchain_tool()
170
+
171
+ logger.debug(
172
+ "Extracted LangChain tool from wrapper: %s",
173
+ getattr(langchain_tool, "name", _UNKNOWN_TOOL_NAME),
174
+ )
175
+ return langchain_tool
176
+
177
+ def _is_langchain_tool(self, ref: Any) -> bool:
178
+ """Check if ref is a LangChain BaseTool class or instance.
179
+
180
+ Args:
181
+ ref: Object to check.
182
+
183
+ Returns:
184
+ True if ref is a LangChain BaseTool.
185
+ """
186
+ from glaip_sdk.utils.tool_detection import is_langchain_tool # noqa: PLC0415
187
+
188
+ return is_langchain_tool(ref)
189
+
190
+ def _instantiate_langchain_tool(self, ref: Any) -> Any:
191
+ """Instantiate LangChain tool if class, return as-is if instance.
192
+
193
+ Args:
194
+ ref: LangChain BaseTool class or instance.
195
+
196
+ Returns:
197
+ LangChain BaseTool instance.
198
+ """
199
+ if isinstance(ref, type):
200
+ # It's a class, instantiate it
201
+ # Note: @tool_plugin decorator doesn't affect instantiation
202
+ return ref()
203
+ return ref
204
+
205
+ def _is_platform_tool(self, ref: Any) -> bool:
206
+ """Check if ref is platform-specific (not supported locally).
207
+
208
+ Args:
209
+ ref: Object to check.
210
+
211
+ Returns:
212
+ True if ref is a platform-specific tool.
213
+ """
214
+ # String tool names
215
+ if isinstance(ref, str):
216
+ return True
217
+
218
+ # Tool.from_native() instances
219
+ if hasattr(ref, "id") and hasattr(ref, "name") and not self._has_explicit_attr(ref, "langchain_tool"):
220
+ tool_class = getattr(ref, "tool_class", None) if self._has_explicit_attr(ref, "tool_class") else None
221
+ if tool_class is None:
222
+ return True
223
+
224
+ return False
225
+
226
+ def _get_platform_tool_error(self, ref: Any) -> str:
227
+ """Get error message for platform tools.
228
+
229
+ Args:
230
+ ref: Platform tool reference.
231
+
232
+ Returns:
233
+ Error message string.
234
+ """
235
+ from glaip_sdk.runner.deps import ( # noqa: PLC0415
236
+ get_local_mode_not_supported_for_tool_message,
237
+ )
238
+
239
+ tool_name = ref if isinstance(ref, str) else getattr(ref, "name", None)
240
+ if tool_name is None:
241
+ tool_name = getattr(getattr(ref, "tool_class", None), "__name__", _UNKNOWN_TOOL_NAME)
242
+ return get_local_mode_not_supported_for_tool_message(tool_name)
@@ -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]
glaip_sdk/tools/base.py CHANGED
@@ -231,6 +231,9 @@ class Tool:
231
231
  Native tools are pre-existing tools on the GL AIP platform
232
232
  that don't require uploading (e.g., "time_tool", "web_search").
233
233
 
234
+ For local execution, automatically discovers the corresponding aip_agents.tools
235
+ class if available. If not found, tool can only be used after deployment.
236
+
234
237
  Args:
235
238
  name: The name of the native tool on the platform.
236
239
 
@@ -241,7 +244,14 @@ class Tool:
241
244
  >>> time_tool = Tool.from_native("time_tool")
242
245
  >>> web_search = Tool.from_native("web_search")
243
246
  """
244
- return cls(name=name, type=ToolType.NATIVE)
247
+ # Try to discover local implementation for native execution
248
+ from glaip_sdk.utils.tool_detection import ( # noqa: PLC0415
249
+ find_aip_agents_tool_class,
250
+ )
251
+
252
+ tool_class = find_aip_agents_tool_class(name)
253
+
254
+ return cls(name=name, type=ToolType.NATIVE, tool_class=tool_class)
245
255
 
246
256
  @classmethod
247
257
  def from_langchain(cls, tool_class: type) -> Tool:
@@ -256,6 +266,9 @@ class Tool:
256
266
  Returns:
257
267
  A Tool reference that will be uploaded during Agent.deploy().
258
268
 
269
+ Raises:
270
+ ValueError: If the tool class has no valid string 'name' attribute or field.
271
+
259
272
  Example:
260
273
  >>> from langchain_core.tools import BaseTool
261
274
  >>>
@@ -267,7 +280,34 @@ class Tool:
267
280
  >>>
268
281
  >>> greeting_tool = Tool.from_langchain(GreetingTool)
269
282
  """
270
- return cls(tool_class=tool_class, type=ToolType.CUSTOM)
283
+ # Extract name from tool_class to populate the name attribute
284
+ tool_name = cls._extract_tool_name(tool_class)
285
+ return cls(name=tool_name, tool_class=tool_class, type=ToolType.CUSTOM)
286
+
287
+ @staticmethod
288
+ def _extract_tool_name(tool_class: type) -> str:
289
+ """Extract tool name from a LangChain tool class.
290
+
291
+ Args:
292
+ tool_class: A LangChain BaseTool subclass.
293
+
294
+ Returns:
295
+ The extracted tool name.
296
+
297
+ Raises:
298
+ ValueError: If name cannot be extracted or is not a valid string.
299
+ """
300
+ from glaip_sdk.utils.tool_detection import get_tool_name # noqa: PLC0415
301
+
302
+ name = get_tool_name(tool_class)
303
+ if name:
304
+ return name
305
+
306
+ # If we can't extract the name, raise an error
307
+ raise ValueError(
308
+ f"Cannot extract name from tool class {tool_class.__name__}. "
309
+ f"Ensure the tool class has a 'name' attribute or field with a valid string value."
310
+ )
271
311
 
272
312
  def get_import_path(self) -> str | None:
273
313
  """Get the import path for custom tools.
@@ -292,15 +332,8 @@ class Tool:
292
332
  return self.name
293
333
 
294
334
  if self.tool_class is not None:
295
- # LangChain BaseTool - get name from model_fields
296
- if hasattr(self.tool_class, "model_fields"):
297
- name_field = self.tool_class.model_fields.get("name")
298
- if name_field and name_field.default:
299
- return name_field.default
300
-
301
- # Direct name attribute
302
- if hasattr(self.tool_class, "name"):
303
- return self.tool_class.name
335
+ # Reuse extraction logic for consistency
336
+ return self._extract_tool_name(self.tool_class)
304
337
 
305
338
  raise ValueError(f"Cannot determine name for tool: {self}")
306
339
 
@@ -354,12 +387,22 @@ class Tool:
354
387
  if not self._client:
355
388
  raise RuntimeError(_CLIENT_NOT_AVAILABLE_MSG)
356
389
 
390
+ # Handle both Client (has .tools) and ToolClient (direct methods)
391
+ # Priority: Check if client has a 'tools' attribute (Client instance)
392
+ # Otherwise, use client directly (ToolClient instance)
393
+ if hasattr(self._client, "tools") and self._client.tools is not None:
394
+ # Main Client instance - use the tools sub-client
395
+ tools_client = self._client.tools
396
+ else:
397
+ # ToolClient instance - use directly
398
+ tools_client = self._client
399
+
357
400
  # Check if file upload is requested
358
401
  if "file" in kwargs:
359
402
  file_path = kwargs.pop("file")
360
- response = self._client.tools.update_via_file(self._id, file_path, **kwargs)
403
+ response = tools_client.update_tool_via_file(self._id, file_path, **kwargs)
361
404
  else:
362
- response = self._client.tools.update(tool_id=self._id, **kwargs)
405
+ response = tools_client.update_tool(tool_id=self._id, **kwargs)
363
406
 
364
407
  # Update local properties from response
365
408
  if hasattr(response, "name") and response.name:
@@ -383,7 +426,17 @@ class Tool:
383
426
  if not self._client:
384
427
  raise RuntimeError(_CLIENT_NOT_AVAILABLE_MSG)
385
428
 
386
- self._client.tools.delete(tool_id=self._id)
429
+ # Handle both Client (has .tools) and ToolClient (direct methods)
430
+ # Priority: Check if client has a 'tools' attribute (Client instance)
431
+ # Otherwise, use client directly (ToolClient instance)
432
+ if hasattr(self._client, "tools") and self._client.tools is not None:
433
+ # Main Client instance - use the tools sub-client
434
+ tools_client = self._client.tools
435
+ else:
436
+ # ToolClient instance - use directly
437
+ tools_client = self._client
438
+
439
+ tools_client.delete_tool(self._id)
387
440
  self._id = None
388
441
  self._client = None
389
442
 
@@ -77,6 +77,7 @@ def __getattr__(name: str) -> type:
77
77
  "get_client": _client_module,
78
78
  "set_client": _client_module,
79
79
  "reset_client": _client_module,
80
+ "tool_detection": "glaip_sdk.utils.tool_detection",
80
81
  }
81
82
 
82
83
  if name in lazy_imports: