glaip-sdk 0.1.2__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 (217) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1413 -0
  5. glaip_sdk/branding.py +126 -2
  6. glaip_sdk/cli/account_store.py +555 -0
  7. glaip_sdk/cli/auth.py +260 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  11. glaip_sdk/cli/commands/agents/_common.py +562 -0
  12. glaip_sdk/cli/commands/agents/create.py +155 -0
  13. glaip_sdk/cli/commands/agents/delete.py +64 -0
  14. glaip_sdk/cli/commands/agents/get.py +89 -0
  15. glaip_sdk/cli/commands/agents/list.py +129 -0
  16. glaip_sdk/cli/commands/agents/run.py +264 -0
  17. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  18. glaip_sdk/cli/commands/agents/update.py +112 -0
  19. glaip_sdk/cli/commands/common_config.py +104 -0
  20. glaip_sdk/cli/commands/configure.py +728 -113
  21. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  22. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  23. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  24. glaip_sdk/cli/commands/mcps/create.py +152 -0
  25. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  26. glaip_sdk/cli/commands/mcps/get.py +212 -0
  27. glaip_sdk/cli/commands/mcps/list.py +69 -0
  28. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  29. glaip_sdk/cli/commands/mcps/update.py +190 -0
  30. glaip_sdk/cli/commands/models.py +12 -8
  31. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  32. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  33. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  34. glaip_sdk/cli/commands/tools/_common.py +80 -0
  35. glaip_sdk/cli/commands/tools/create.py +228 -0
  36. glaip_sdk/cli/commands/tools/delete.py +61 -0
  37. glaip_sdk/cli/commands/tools/get.py +103 -0
  38. glaip_sdk/cli/commands/tools/list.py +69 -0
  39. glaip_sdk/cli/commands/tools/script.py +49 -0
  40. glaip_sdk/cli/commands/tools/update.py +102 -0
  41. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  42. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  43. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  44. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  45. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  46. glaip_sdk/cli/commands/update.py +163 -17
  47. glaip_sdk/cli/config.py +49 -4
  48. glaip_sdk/cli/constants.py +38 -0
  49. glaip_sdk/cli/context.py +8 -0
  50. glaip_sdk/cli/core/__init__.py +79 -0
  51. glaip_sdk/cli/core/context.py +124 -0
  52. glaip_sdk/cli/core/output.py +851 -0
  53. glaip_sdk/cli/core/prompting.py +649 -0
  54. glaip_sdk/cli/core/rendering.py +187 -0
  55. glaip_sdk/cli/display.py +41 -20
  56. glaip_sdk/cli/entrypoint.py +20 -0
  57. glaip_sdk/cli/hints.py +57 -0
  58. glaip_sdk/cli/io.py +6 -3
  59. glaip_sdk/cli/main.py +340 -143
  60. glaip_sdk/cli/masking.py +21 -33
  61. glaip_sdk/cli/pager.py +12 -13
  62. glaip_sdk/cli/parsers/__init__.py +1 -3
  63. glaip_sdk/cli/resolution.py +2 -1
  64. glaip_sdk/cli/slash/__init__.py +0 -9
  65. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  66. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  67. glaip_sdk/cli/slash/agent_session.py +62 -21
  68. glaip_sdk/cli/slash/prompt.py +21 -0
  69. glaip_sdk/cli/slash/remote_runs_controller.py +568 -0
  70. glaip_sdk/cli/slash/session.py +1105 -153
  71. glaip_sdk/cli/slash/tui/__init__.py +36 -0
  72. glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
  73. glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
  74. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  75. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  76. glaip_sdk/cli/slash/tui/context.py +92 -0
  77. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  78. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  79. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  80. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  81. glaip_sdk/cli/slash/tui/loading.py +80 -0
  82. glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
  83. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  84. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  85. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  86. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  87. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  88. glaip_sdk/cli/slash/tui/toast.py +388 -0
  89. glaip_sdk/cli/transcript/__init__.py +12 -52
  90. glaip_sdk/cli/transcript/cache.py +255 -44
  91. glaip_sdk/cli/transcript/capture.py +66 -1
  92. glaip_sdk/cli/transcript/history.py +815 -0
  93. glaip_sdk/cli/transcript/viewer.py +72 -463
  94. glaip_sdk/cli/tui_settings.py +125 -0
  95. glaip_sdk/cli/update_notifier.py +227 -10
  96. glaip_sdk/cli/validators.py +5 -6
  97. glaip_sdk/client/__init__.py +3 -1
  98. glaip_sdk/client/_schedule_payloads.py +89 -0
  99. glaip_sdk/client/agent_runs.py +147 -0
  100. glaip_sdk/client/agents.py +576 -44
  101. glaip_sdk/client/base.py +26 -0
  102. glaip_sdk/client/hitl.py +136 -0
  103. glaip_sdk/client/main.py +25 -14
  104. glaip_sdk/client/mcps.py +165 -24
  105. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  106. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
  107. glaip_sdk/client/payloads/agent/responses.py +43 -0
  108. glaip_sdk/client/run_rendering.py +546 -92
  109. glaip_sdk/client/schedules.py +439 -0
  110. glaip_sdk/client/shared.py +21 -0
  111. glaip_sdk/client/tools.py +206 -32
  112. glaip_sdk/config/constants.py +33 -2
  113. glaip_sdk/guardrails/__init__.py +80 -0
  114. glaip_sdk/guardrails/serializer.py +89 -0
  115. glaip_sdk/hitl/__init__.py +48 -0
  116. glaip_sdk/hitl/base.py +64 -0
  117. glaip_sdk/hitl/callback.py +43 -0
  118. glaip_sdk/hitl/local.py +121 -0
  119. glaip_sdk/hitl/remote.py +523 -0
  120. glaip_sdk/mcps/__init__.py +21 -0
  121. glaip_sdk/mcps/base.py +345 -0
  122. glaip_sdk/models/__init__.py +136 -0
  123. glaip_sdk/models/_provider_mappings.py +101 -0
  124. glaip_sdk/models/_validation.py +97 -0
  125. glaip_sdk/models/agent.py +48 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/constants.py +141 -0
  129. glaip_sdk/models/mcp.py +33 -0
  130. glaip_sdk/models/model.py +170 -0
  131. glaip_sdk/models/schedule.py +224 -0
  132. glaip_sdk/models/tool.py +33 -0
  133. glaip_sdk/payload_schemas/__init__.py +1 -13
  134. glaip_sdk/payload_schemas/agent.py +1 -0
  135. glaip_sdk/payload_schemas/guardrails.py +34 -0
  136. glaip_sdk/registry/__init__.py +55 -0
  137. glaip_sdk/registry/agent.py +164 -0
  138. glaip_sdk/registry/base.py +139 -0
  139. glaip_sdk/registry/mcp.py +253 -0
  140. glaip_sdk/registry/tool.py +445 -0
  141. glaip_sdk/rich_components.py +58 -2
  142. glaip_sdk/runner/__init__.py +76 -0
  143. glaip_sdk/runner/base.py +84 -0
  144. glaip_sdk/runner/deps.py +115 -0
  145. glaip_sdk/runner/langgraph.py +1055 -0
  146. glaip_sdk/runner/logging_config.py +77 -0
  147. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  148. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  149. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  150. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  151. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  152. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  153. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  154. glaip_sdk/schedules/__init__.py +22 -0
  155. glaip_sdk/schedules/base.py +291 -0
  156. glaip_sdk/tools/__init__.py +22 -0
  157. glaip_sdk/tools/base.py +488 -0
  158. glaip_sdk/utils/__init__.py +59 -12
  159. glaip_sdk/utils/a2a/__init__.py +34 -0
  160. glaip_sdk/utils/a2a/event_processor.py +188 -0
  161. glaip_sdk/utils/agent_config.py +8 -2
  162. glaip_sdk/utils/bundler.py +403 -0
  163. glaip_sdk/utils/client.py +111 -0
  164. glaip_sdk/utils/client_utils.py +39 -7
  165. glaip_sdk/utils/datetime_helpers.py +58 -0
  166. glaip_sdk/utils/discovery.py +78 -0
  167. glaip_sdk/utils/display.py +23 -15
  168. glaip_sdk/utils/export.py +143 -0
  169. glaip_sdk/utils/general.py +0 -33
  170. glaip_sdk/utils/import_export.py +12 -7
  171. glaip_sdk/utils/import_resolver.py +524 -0
  172. glaip_sdk/utils/instructions.py +101 -0
  173. glaip_sdk/utils/rendering/__init__.py +115 -1
  174. glaip_sdk/utils/rendering/formatting.py +5 -30
  175. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  176. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  177. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  178. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  179. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  180. glaip_sdk/utils/rendering/models.py +1 -0
  181. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  182. glaip_sdk/utils/rendering/renderer/base.py +299 -1434
  183. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  184. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  185. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  186. glaip_sdk/utils/rendering/renderer/stream.py +4 -33
  187. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  188. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  189. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  190. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  191. glaip_sdk/utils/rendering/state.py +204 -0
  192. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  193. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  194. glaip_sdk/utils/rendering/steps/format.py +176 -0
  195. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  196. glaip_sdk/utils/rendering/timing.py +36 -0
  197. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  198. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  199. glaip_sdk/utils/resource_refs.py +25 -13
  200. glaip_sdk/utils/runtime_config.py +426 -0
  201. glaip_sdk/utils/serialization.py +18 -0
  202. glaip_sdk/utils/sync.py +162 -0
  203. glaip_sdk/utils/tool_detection.py +301 -0
  204. glaip_sdk/utils/tool_storage_provider.py +140 -0
  205. glaip_sdk/utils/validation.py +16 -24
  206. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
  207. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  208. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  209. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  210. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  211. glaip_sdk/cli/commands/agents.py +0 -1369
  212. glaip_sdk/cli/commands/mcps.py +0 -1187
  213. glaip_sdk/cli/commands/tools.py +0 -584
  214. glaip_sdk/cli/utils.py +0 -1278
  215. glaip_sdk/models.py +0 -240
  216. glaip_sdk-0.1.2.dist-info/RECORD +0 -82
  217. glaip_sdk-0.1.2.dist-info/entry_points.txt +0 -3
@@ -0,0 +1,253 @@
1
+ """MCP registry for glaip_sdk.
2
+
3
+ This module provides the MCPRegistry that caches MCPs (Model Context Protocols)
4
+ to avoid redundant API calls when deploying agents with MCPs.
5
+
6
+ Authors:
7
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ from glaip_sdk.registry.base import BaseRegistry
16
+ from glaip_sdk.utils.resource_refs import is_uuid
17
+
18
+ if TYPE_CHECKING:
19
+ from glaip_sdk.mcps import MCP
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class MCPRegistry(BaseRegistry["MCP"]):
25
+ """Registry for MCPs (Model Context Protocols).
26
+
27
+ Resolves MCP references to glaip_sdk.models.MCP objects.
28
+ Caches results to avoid redundant API calls.
29
+
30
+ Handles:
31
+ - glaip_sdk.models.MCP → return as-is (uses mcp.id)
32
+ - String names/IDs → lookup on platform, cache, return MCP
33
+
34
+ Attributes:
35
+ _cache: Internal cache mapping names to MCP objects.
36
+
37
+ Example:
38
+ >>> registry = get_mcp_registry()
39
+ >>> mcp = registry.resolve("arxiv-mcp")
40
+ >>> print(mcp.id)
41
+ """
42
+
43
+ def _extract_name(self, ref: Any) -> str:
44
+ """Extract MCP name from a reference.
45
+
46
+ Args:
47
+ ref: An MCP object, dict, or string name.
48
+
49
+ Returns:
50
+ The extracted MCP name.
51
+
52
+ Raises:
53
+ ValueError: If name cannot be extracted from the reference.
54
+ """
55
+ # String name
56
+ if isinstance(ref, str):
57
+ return ref
58
+
59
+ # Dict from API response - extract name or id
60
+ if isinstance(ref, dict):
61
+ return ref.get("name") or ref.get("id") or ""
62
+
63
+ # Already resolved MCP (glaip_sdk.models.MCP)
64
+ if hasattr(ref, "id") and hasattr(ref, "name") and not isinstance(ref, type):
65
+ return ref.name or ref.id
66
+
67
+ raise ValueError(f"Cannot extract name from: {ref}")
68
+
69
+ def _resolve_and_cache(self, ref: Any, name: str) -> MCP:
70
+ """Resolve MCP reference - find by name/ID or create if needed.
71
+
72
+ Args:
73
+ ref: The MCP reference to resolve.
74
+ name: The extracted MCP name.
75
+
76
+ Returns:
77
+ The resolved glaip_sdk.models.MCP object.
78
+
79
+ Raises:
80
+ ValueError: If the MCP cannot be resolved.
81
+ """
82
+ # MCP object (check if already has ID)
83
+ if hasattr(ref, "id") and hasattr(ref, "name") and not isinstance(ref, type):
84
+ if ref.id is not None:
85
+ # Already resolved MCP with ID - just cache and return
86
+ logger.debug("Caching already resolved MCP: %s", name)
87
+ self._cache[name] = ref
88
+ return ref
89
+
90
+ # MCP without ID - need to look up or create
91
+ return self._lookup_or_create_mcp(ref, name)
92
+
93
+ # Dict from API response - use ID directly if available
94
+ if isinstance(ref, dict):
95
+ mcp_id = ref.get("id")
96
+ if mcp_id:
97
+ from glaip_sdk.mcps.base import MCP # noqa: PLC0415
98
+
99
+ mcp = MCP(id=mcp_id, name=ref.get("name", ""))
100
+ self._cache[name] = mcp
101
+ return mcp
102
+ raise ValueError(f"MCP dict missing 'id': {ref}")
103
+
104
+ # String name - look up on platform
105
+ if isinstance(ref, str):
106
+ return self._lookup_mcp_by_name(name)
107
+
108
+ raise ValueError(f"Could not resolve MCP reference: {ref}")
109
+
110
+ def _lookup_or_create_mcp(self, ref: Any, name: str) -> MCP:
111
+ """Look up or create an MCP from a reference.
112
+
113
+ Args:
114
+ ref: The MCP reference with config details.
115
+ name: The extracted MCP name.
116
+
117
+ Returns:
118
+ The resolved or created glaip_sdk.models.MCP object.
119
+ """
120
+ # Check if this MCP is lookup-only (e.g., from MCP.from_native)
121
+ if getattr(ref, "_lookup_only", False):
122
+ return self._lookup_native_mcp(name)
123
+
124
+ return self._upsert_mcp_from_ref(ref, name)
125
+
126
+ def _lookup_native_mcp(self, name: str) -> MCP:
127
+ """Look up a native MCP that must exist on the platform.
128
+
129
+ Used for MCP.from_native() references that should not be created.
130
+
131
+ Args:
132
+ name: The MCP name to look up.
133
+
134
+ Returns:
135
+ The found MCP.
136
+
137
+ Raises:
138
+ ValueError: If MCP not found or multiple found.
139
+ """
140
+ from glaip_sdk.utils.client import get_client # noqa: PLC0415
141
+
142
+ client = get_client()
143
+ logger.info("Looking up native MCP: %s", name)
144
+
145
+ results = client.find_mcps(name)
146
+ exact_matches = [mcp for mcp in results if getattr(mcp, "name", None) == name]
147
+ if len(exact_matches) == 1:
148
+ mcp = exact_matches[0]
149
+ self._cache[name] = mcp
150
+ return mcp
151
+ if len(exact_matches) > 1:
152
+ raise ValueError(f"Multiple MCPs found with name '{name}'")
153
+ raise ValueError(f"MCP not found on platform: {name}")
154
+
155
+ def _upsert_mcp_from_ref(self, ref: Any, name: str) -> MCP:
156
+ """Create or update an MCP from a reference with config.
157
+
158
+ Args:
159
+ ref: The MCP reference with config details.
160
+ name: The extracted MCP name.
161
+
162
+ Returns:
163
+ The created or updated MCP.
164
+ """
165
+ from glaip_sdk.utils.client import get_client # noqa: PLC0415
166
+
167
+ client = get_client()
168
+ logger.info("Upserting MCP: %s", name)
169
+
170
+ mcp = client.mcps.upsert_mcp(
171
+ name,
172
+ description=getattr(ref, "description", None),
173
+ config=getattr(ref, "config", None),
174
+ transport=getattr(ref, "transport", None),
175
+ metadata=getattr(ref, "metadata", None),
176
+ authentication=getattr(ref, "authentication", None),
177
+ )
178
+ self._cache[name] = mcp
179
+ return mcp
180
+
181
+ def _lookup_mcp_by_name(self, name: str) -> MCP:
182
+ """Look up MCP by name or ID on the platform.
183
+
184
+ Args:
185
+ name: The MCP name or ID to look up.
186
+
187
+ Returns:
188
+ The resolved glaip_sdk.models.MCP object.
189
+
190
+ Raises:
191
+ ValueError: If the MCP cannot be found.
192
+ """
193
+ # Lazy imports to avoid circular dependency
194
+ from glaip_sdk.utils.client import get_client # noqa: PLC0415
195
+
196
+ client = get_client()
197
+ logger.info("Looking up MCP by name: %s", name)
198
+
199
+ # Check if it's a valid UUID
200
+ if is_uuid(name):
201
+ mcp = client.get_mcp_by_id(name)
202
+ if mcp:
203
+ self._cache[name] = mcp
204
+ return mcp
205
+ else:
206
+ results = client.find_mcps(name)
207
+ exact_matches = [mcp for mcp in results if getattr(mcp, "name", None) == name]
208
+ if len(exact_matches) == 1:
209
+ mcp = exact_matches[0]
210
+ self._cache[name] = mcp
211
+ return mcp
212
+ if len(exact_matches) > 1:
213
+ raise ValueError(f"Multiple MCPs found with name '{name}'")
214
+
215
+ raise ValueError(f"MCP not found on platform: {name}")
216
+
217
+
218
+ class _MCPRegistrySingleton:
219
+ """Singleton holder for MCPRegistry to avoid global statement."""
220
+
221
+ _instance: MCPRegistry | None = None
222
+
223
+ @classmethod
224
+ def get_instance(cls) -> MCPRegistry:
225
+ """Get or create the singleton instance.
226
+
227
+ Returns:
228
+ The global MCPRegistry instance.
229
+ """
230
+ if cls._instance is None:
231
+ cls._instance = MCPRegistry()
232
+ return cls._instance
233
+
234
+ @classmethod
235
+ def reset(cls) -> None:
236
+ """Reset the singleton instance (for testing)."""
237
+ cls._instance = None
238
+
239
+
240
+ def get_mcp_registry() -> MCPRegistry:
241
+ """Get the singleton MCPRegistry instance.
242
+
243
+ Returns a global MCPRegistry that caches MCPs across the session.
244
+
245
+ Returns:
246
+ The global MCPRegistry instance.
247
+
248
+ Example:
249
+ >>> from glaip_sdk.registry import get_mcp_registry
250
+ >>> registry = get_mcp_registry()
251
+ >>> mcp = registry.resolve("arxiv-mcp")
252
+ """
253
+ return _MCPRegistrySingleton.get_instance()
@@ -0,0 +1,445 @@
1
+ """Tool registry for glaip_sdk.
2
+
3
+ This module provides a ToolRegistry that caches deployed tools
4
+ to avoid redundant API calls when deploying agents with tools.
5
+
6
+ Authors:
7
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ from glaip_sdk.registry.base import BaseRegistry
16
+
17
+ if TYPE_CHECKING:
18
+ from glaip_sdk.tools import Tool
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class ToolRegistry(BaseRegistry["Tool"]):
24
+ """Registry for tools.
25
+
26
+ Resolves tool references to glaip_sdk.models.Tool objects.
27
+ Caches results to avoid redundant API calls and duplicate uploads.
28
+
29
+ Handles:
30
+ - Tool classes (LangChain BaseTool subclasses) → upload, cache, return Tool
31
+ - glaip_sdk.models.Tool → return as-is (uses tool.id)
32
+ - String names → lookup on platform, cache, return Tool
33
+
34
+ Attributes:
35
+ _cache: Internal cache mapping names to Tool objects.
36
+
37
+ Example:
38
+ >>> registry = get_tool_registry()
39
+ >>> tool = registry.resolve(WebSearchTool)
40
+ >>> print(tool.id)
41
+ """
42
+
43
+ def _get_name_from_model_fields(self, ref: type) -> str | None:
44
+ """Extract name from Pydantic model_fields if available."""
45
+ model_fields = getattr(ref, "model_fields", {})
46
+ if "name" not in model_fields:
47
+ return None
48
+ field_info = model_fields["name"]
49
+ default = getattr(field_info, "default", None)
50
+ return default if isinstance(default, str) else None
51
+
52
+ def _get_string_attr(self, obj: Any, attr: str) -> str | None:
53
+ """Get attribute if it's a string, otherwise None."""
54
+ value = getattr(obj, attr, None)
55
+ return value if isinstance(value, str) else None
56
+
57
+ def _extract_name_from_instance(self, ref: Any) -> str | None:
58
+ """Extract name from a non-type instance.
59
+
60
+ Args:
61
+ ref: The instance to extract name from.
62
+
63
+ Returns:
64
+ The extracted name, or None if not found.
65
+ """
66
+ if isinstance(ref, type):
67
+ return None
68
+ return self._get_string_attr(ref, "name")
69
+
70
+ def _extract_name_from_class(self, ref: Any) -> str | None:
71
+ """Extract name from a class.
72
+
73
+ Args:
74
+ ref: The class to extract name from.
75
+
76
+ Returns:
77
+ The extracted name, or None if not found.
78
+ """
79
+ if not isinstance(ref, type):
80
+ return None
81
+ return self._get_string_attr(ref, "name") or self._get_name_from_model_fields(ref)
82
+
83
+ def _extract_name(self, ref: Any) -> str:
84
+ """Extract tool name from a reference.
85
+
86
+ Args:
87
+ ref: A tool class, instance, dict, or string name.
88
+
89
+ Returns:
90
+ The extracted tool name.
91
+
92
+ Raises:
93
+ ValueError: If name cannot be extracted from the reference.
94
+ """
95
+ # Lazy import to avoid circular dependency: Tool -> ToolRegistry -> Tool
96
+ from glaip_sdk.tools.base import Tool # noqa: PLC0415
97
+
98
+ if isinstance(ref, str):
99
+ return ref
100
+
101
+ # Tool instance (from Tool.from_langchain() or Tool.from_native())
102
+ if isinstance(ref, Tool):
103
+ return ref.get_name()
104
+
105
+ # Dict from API response - extract name or id
106
+ if isinstance(ref, dict):
107
+ return ref.get("name") or ref.get("id") or ""
108
+
109
+ # Tool instance (not a class) with name attribute
110
+ name = self._extract_name_from_instance(ref)
111
+ if name:
112
+ return name
113
+
114
+ # Tool class - try direct attribute first, then model_fields
115
+ name = self._extract_name_from_class(ref)
116
+ if name:
117
+ return name
118
+
119
+ raise ValueError(f"Cannot extract name from: {ref}")
120
+
121
+ def _cache_tool(self, tool: Tool, name: str) -> None:
122
+ """Cache a tool by name and ID if available.
123
+
124
+ Args:
125
+ tool: The tool to cache.
126
+ name: The tool name.
127
+ """
128
+ self._cache[name] = tool
129
+ if hasattr(tool, "id") and tool.id:
130
+ self._cache[tool.id] = tool
131
+
132
+ def _resolve_native_platform_tool(self, name: str, tool_class: type | None = None) -> Tool:
133
+ """Find a native tool on the platform and cache it.
134
+
135
+ Args:
136
+ name: The tool name to look up.
137
+ tool_class: Optional local implementation to preserve.
138
+
139
+ Returns:
140
+ The resolved Tool object.
141
+
142
+ Raises:
143
+ ValueError: If the tool is not found on the platform.
144
+ """
145
+ from glaip_sdk.utils.discovery import find_tool # noqa: PLC0415
146
+
147
+ logger.info("Looking up native tool: %s", name)
148
+ tool = find_tool(name)
149
+ if tool:
150
+ # Preserve local implementation if provided
151
+ if tool_class:
152
+ tool.tool_class = tool_class
153
+ self._cache_tool(tool, name)
154
+ return tool
155
+
156
+ raise ValueError(
157
+ f"Native tool '{name}' not found on platform. Ensure the tool is deployed or check for name mismatches."
158
+ )
159
+
160
+ def _resolve_tool_instance(self, ref: Any, name: str) -> Tool | None:
161
+ """Resolve a ToolClass instance.
162
+
163
+ Args:
164
+ ref: The ToolClass instance to resolve.
165
+ name: The extracted tool name.
166
+
167
+ Returns:
168
+ The resolved tool, or None if not a ToolClass instance.
169
+ """
170
+ # Lazy imports to avoid circular dependency: Tool -> ToolRegistry -> Tool
171
+ from glaip_sdk.tools.base import Tool as ToolClass # noqa: PLC0415
172
+ from glaip_sdk.tools.base import ToolType # noqa: PLC0415
173
+
174
+ # Use try/except to handle mocked Tool class in tests
175
+ try:
176
+ is_tool_instance = isinstance(ref, ToolClass)
177
+ except TypeError:
178
+ return None
179
+
180
+ if not is_tool_instance:
181
+ return None
182
+
183
+ # If Tool has an ID, it's already deployed - return as-is
184
+ if ref.id is not None:
185
+ logger.debug("Caching already deployed tool: %s", name)
186
+ self._cache_tool(ref, name)
187
+ return ref
188
+
189
+ # Tool.from_native() - look up on platform
190
+ if ref.tool_type == ToolType.NATIVE:
191
+ return self._resolve_native_platform_tool(name, tool_class=getattr(ref, "tool_class", None))
192
+
193
+ # Tool.from_langchain() - resolve the inner tool_class (promoted or uploaded)
194
+ if ref.tool_class is not None:
195
+ return self._resolve_custom_tool(ref.tool_class, name)
196
+
197
+ # Unresolvable Tool instance - neither native nor has tool_class
198
+ raise ValueError(
199
+ f"Cannot resolve Tool instance: {ref}. "
200
+ f"Tool has no id, is not NATIVE type, and has no tool_class. "
201
+ f"Ensure Tool is created via Tool.from_native() or Tool.from_langchain()."
202
+ )
203
+
204
+ def _resolve_deployed_tool(self, ref: Any, name: str) -> Tool | None:
205
+ """Resolve an already deployed tool (has id/name attributes).
206
+
207
+ Args:
208
+ ref: The tool reference to resolve.
209
+ name: The extracted tool name.
210
+
211
+ Returns:
212
+ The resolved tool, or None if not a deployed tool.
213
+ """
214
+ # Already deployed tool (not a ToolClass, but has id/name)
215
+ # This handles API response objects and backward compatibility
216
+ if not (hasattr(ref, "id") and hasattr(ref, "name") and not isinstance(ref, type)):
217
+ return None
218
+
219
+ if ref.id is not None:
220
+ logger.debug("Caching already deployed tool: %s", name)
221
+ # Use _cache_tool to cache by both name and ID for consistency
222
+ self._cache_tool(ref, name)
223
+ return ref
224
+
225
+ # Tool without ID (backward compatibility) - look up on platform
226
+ return self._resolve_native_platform_tool(name)
227
+
228
+ def _resolve_custom_tool(self, ref: Any, name: str) -> Tool | None:
229
+ """Resolve a custom tool class, promoting aip_agents.tools classes to NATIVE.
230
+
231
+ This method handles two main paths:
232
+ 1. **Promotion**: If the tool class is from `aip_agents.tools`, it is automatically
233
+ promoted to a `NATIVE` tool type. It then performs a platform lookup to link it
234
+ with the deployed native tool while preserving the local `tool_class` for local execution.
235
+ 2. **Upload**: If it is a standard LangChain tool, it is uploaded to the platform
236
+ as a custom tool.
237
+
238
+ Args:
239
+ ref: The tool reference (usually a class) to resolve.
240
+ name: The extracted tool name.
241
+
242
+ Returns:
243
+ The resolved tool, or None if not a custom tool.
244
+ """
245
+ # aip_agents tools are automatically promoted to NATIVE
246
+ if self._is_aip_agents_tool(ref):
247
+ from glaip_sdk.utils.tool_detection import get_tool_name # noqa: PLC0415
248
+
249
+ # Get name from class attribute or field
250
+ tool_name = get_tool_name(ref)
251
+ if tool_name is None:
252
+ raise ValueError(f"Tool class {ref.__name__} has no 'name' attribute")
253
+
254
+ return self._resolve_native_platform_tool(tool_name, tool_class=ref)
255
+
256
+ if not self._is_custom_tool(ref):
257
+ return None
258
+
259
+ # Regular custom tools - upload to platform
260
+ from glaip_sdk.utils.sync import update_or_create_tool # noqa: PLC0415
261
+
262
+ logger.info("Uploading custom tool: %s", name)
263
+ tool = update_or_create_tool(ref)
264
+
265
+ # Cache the resolved tool
266
+ self._cache_tool(tool, name)
267
+ if hasattr(tool, "id") and tool.id:
268
+ self._cache[tool.id] = tool
269
+
270
+ return tool
271
+
272
+ def _resolve_dict_tool(self, ref: Any, name: str) -> Tool | None:
273
+ """Resolve a tool from a dict (API response).
274
+
275
+ Args:
276
+ ref: The dict to resolve.
277
+ name: The extracted tool name.
278
+
279
+ Returns:
280
+ The resolved tool, or None if not a dict.
281
+ """
282
+ # Lazy imports to avoid circular dependency
283
+ from glaip_sdk.tools.base import Tool as ToolClass # noqa: PLC0415
284
+
285
+ if not isinstance(ref, dict):
286
+ return None
287
+
288
+ tool_id = ref.get("id")
289
+ if tool_id:
290
+ tool = ToolClass(id=tool_id, name=ref.get("name", ""))
291
+ # Use _cache_tool to cache by both name and ID for consistency
292
+ self._cache_tool(tool, name)
293
+ return tool
294
+ raise ValueError(f"Tool dict missing 'id': {ref}")
295
+
296
+ def _resolve_string_tool(self, ref: Any, name: str) -> Tool | None:
297
+ """Resolve a tool from a string name.
298
+
299
+ Args:
300
+ ref: The string to resolve.
301
+ name: The extracted tool name.
302
+
303
+ Returns:
304
+ The resolved tool, or None if not a string.
305
+ """
306
+ if not isinstance(ref, str):
307
+ return None
308
+
309
+ return self._resolve_native_platform_tool(name)
310
+
311
+ def _resolve_and_cache(self, ref: Any, name: str) -> Tool:
312
+ """Resolve tool reference - upload if class, find if string/native.
313
+
314
+ Args:
315
+ ref: The tool reference to resolve.
316
+ name: The extracted tool name.
317
+
318
+ Returns:
319
+ The resolved glaip_sdk.models.Tool object.
320
+
321
+ Raises:
322
+ ValueError: If tool cannot be resolved.
323
+ """
324
+ # Try each resolution strategy in order
325
+ resolvers = [
326
+ self._resolve_tool_instance,
327
+ self._resolve_deployed_tool,
328
+ self._resolve_custom_tool,
329
+ self._resolve_dict_tool,
330
+ self._resolve_string_tool,
331
+ ]
332
+
333
+ for resolver in resolvers:
334
+ result = resolver(ref, name)
335
+ if result is not None:
336
+ return result
337
+
338
+ raise ValueError(f"Could not resolve tool reference: {ref}")
339
+
340
+ def _is_aip_agents_tool(self, ref: Any) -> bool:
341
+ """Check if reference is an aip-agents tool.
342
+
343
+ Args:
344
+ ref: The reference to check.
345
+
346
+ Returns:
347
+ True if ref is from aip_agents.tools package.
348
+ """
349
+ try:
350
+ from glaip_sdk.utils.tool_detection import ( # noqa: PLC0415
351
+ is_aip_agents_tool,
352
+ )
353
+ except ImportError:
354
+ return False
355
+
356
+ return is_aip_agents_tool(ref)
357
+
358
+ def _is_custom_tool(self, ref: Any) -> bool:
359
+ """Check if reference is a custom tool class/instance.
360
+
361
+ Args:
362
+ ref: The reference to check.
363
+
364
+ Returns:
365
+ True if ref is a custom tool that needs uploading.
366
+ """
367
+ try:
368
+ from glaip_sdk.utils.tool_detection import ( # noqa: PLC0415
369
+ is_langchain_tool,
370
+ )
371
+
372
+ is_tool = is_langchain_tool(ref)
373
+ except ImportError:
374
+ is_tool = hasattr(ref, "args_schema") or hasattr(ref, "_run")
375
+ if is_tool:
376
+ logger.warning("tool_detection module missing; identifying tool via fallback attributes.")
377
+
378
+ # aip_agents tools are NOT custom - they're native
379
+ if is_tool and self._is_aip_agents_tool(ref):
380
+ return False
381
+
382
+ return is_tool
383
+
384
+ def resolve(self, ref: Any) -> Tool:
385
+ """Resolve a tool reference to a platform Tool object.
386
+
387
+ Overrides base resolve to handle SDK tools differently.
388
+
389
+ Args:
390
+ ref: The tool reference to resolve.
391
+
392
+ Returns:
393
+ The resolved glaip_sdk.models.Tool object.
394
+ """
395
+ # Check if it's a Tool instance (not a class)
396
+ if hasattr(ref, "id") and hasattr(ref, "name") and not isinstance(ref, type):
397
+ # If Tool has an ID, it's already deployed - return as-is
398
+ if ref.id is not None:
399
+ name = self._extract_name(ref)
400
+ if name not in self._cache:
401
+ # Use _cache_tool to cache by both name and ID for consistency
402
+ self._cache_tool(ref, name)
403
+ return ref
404
+ # Tool without ID (e.g., from Tool.from_native()) - needs platform lookup
405
+ # Fall through to normal resolution
406
+
407
+ return super().resolve(ref)
408
+
409
+
410
+ class _ToolRegistrySingleton:
411
+ """Singleton holder for ToolRegistry to avoid global statement."""
412
+
413
+ _instance: ToolRegistry | None = None
414
+
415
+ @classmethod
416
+ def get_instance(cls) -> ToolRegistry:
417
+ """Get or create the singleton instance.
418
+
419
+ Returns:
420
+ The global ToolRegistry instance.
421
+ """
422
+ if cls._instance is None:
423
+ cls._instance = ToolRegistry()
424
+ return cls._instance
425
+
426
+ @classmethod
427
+ def reset(cls) -> None:
428
+ """Reset the singleton instance (for testing)."""
429
+ cls._instance = None
430
+
431
+
432
+ def get_tool_registry() -> ToolRegistry:
433
+ """Get the singleton ToolRegistry instance.
434
+
435
+ Returns a global ToolRegistry that caches tools across the session.
436
+
437
+ Returns:
438
+ The global ToolRegistry instance.
439
+
440
+ Example:
441
+ >>> from glaip_sdk.registry import get_tool_registry
442
+ >>> registry = get_tool_registry()
443
+ >>> tool = registry.resolve("web_search")
444
+ """
445
+ return _ToolRegistrySingleton.get_instance()