glaip-sdk 0.0.7__py3-none-any.whl → 0.6.5b6__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 +6 -3
- glaip_sdk/_version.py +12 -5
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1126 -0
- glaip_sdk/branding.py +79 -15
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +699 -0
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +503 -183
- glaip_sdk/cli/commands/common_config.py +101 -0
- glaip_sdk/cli/commands/configure.py +774 -137
- glaip_sdk/cli/commands/mcps.py +1124 -181
- glaip_sdk/cli/commands/models.py +25 -10
- glaip_sdk/cli/commands/tools.py +144 -92
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/commands/update.py +61 -0
- glaip_sdk/cli/config.py +95 -0
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +150 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +846 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +143 -53
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +24 -18
- glaip_sdk/cli/main.py +420 -145
- glaip_sdk/cli/masking.py +136 -0
- glaip_sdk/cli/mcp_validators.py +287 -0
- glaip_sdk/cli/pager.py +266 -0
- glaip_sdk/cli/parsers/__init__.py +7 -0
- glaip_sdk/cli/parsers/json_input.py +177 -0
- glaip_sdk/cli/resolution.py +28 -21
- glaip_sdk/cli/rich_helpers.py +27 -0
- glaip_sdk/cli/slash/__init__.py +15 -0
- glaip_sdk/cli/slash/accounts_controller.py +500 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +282 -0
- glaip_sdk/cli/slash/prompt.py +245 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +1679 -0
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +872 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/transcript/__init__.py +31 -0
- glaip_sdk/cli/transcript/cache.py +536 -0
- glaip_sdk/cli/transcript/capture.py +329 -0
- glaip_sdk/cli/transcript/export.py +38 -0
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +77 -0
- glaip_sdk/cli/transcript/viewer.py +372 -0
- glaip_sdk/cli/update_notifier.py +290 -0
- glaip_sdk/cli/utils.py +247 -1238
- glaip_sdk/cli/validators.py +16 -18
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +520 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +940 -574
- glaip_sdk/client/base.py +163 -48
- glaip_sdk/client/main.py +35 -12
- glaip_sdk/client/mcps.py +126 -18
- glaip_sdk/client/run_rendering.py +415 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +195 -37
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/config/constants.py +15 -5
- glaip_sdk/exceptions.py +16 -9
- glaip_sdk/icons.py +25 -0
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +90 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +116 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +7 -0
- glaip_sdk/payload_schemas/agent.py +85 -0
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +231 -0
- glaip_sdk/rich_components.py +98 -2
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +115 -0
- glaip_sdk/runner/langgraph.py +597 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +158 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +177 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +59 -13
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +53 -40
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +58 -26
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +65 -32
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +1 -36
- glaip_sdk/utils/import_export.py +20 -25
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +85 -43
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +51 -19
- glaip_sdk/utils/rendering/layout/progress.py +202 -0
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +39 -7
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
- glaip_sdk/utils/rendering/renderer/base.py +672 -759
- glaip_sdk/utils/rendering/renderer/config.py +4 -10
- glaip_sdk/utils/rendering/renderer/debug.py +75 -22
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +13 -54
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/step_tree_state.py +100 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/resource_refs.py +29 -26
- glaip_sdk/utils/runtime_config.py +422 -0
- glaip_sdk/utils/serialization.py +184 -51
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +21 -30
- {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/METADATA +58 -12
- glaip_sdk-0.6.5b6.dist-info/RECORD +159 -0
- {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/WHEEL +1 -1
- glaip_sdk/models.py +0 -250
- glaip_sdk/utils/rendering/renderer/progress.py +0 -118
- glaip_sdk/utils/rendering/steps.py +0 -232
- glaip_sdk/utils/rich_utils.py +0 -29
- glaip_sdk-0.0.7.dist-info/RECORD +0 -55
- {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/entry_points.txt +0 -0
glaip_sdk/client/base.py
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
Authors:
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import logging
|
|
9
10
|
import os
|
|
11
|
+
from collections.abc import Iterable, Mapping
|
|
10
12
|
from typing import Any, NoReturn, Union
|
|
11
13
|
|
|
12
14
|
import httpx
|
|
@@ -14,7 +16,7 @@ from dotenv import load_dotenv
|
|
|
14
16
|
|
|
15
17
|
import glaip_sdk
|
|
16
18
|
from glaip_sdk._version import __version__ as SDK_VERSION
|
|
17
|
-
from glaip_sdk.config.constants import SDK_NAME
|
|
19
|
+
from glaip_sdk.config.constants import DEFAULT_ERROR_MESSAGE, SDK_NAME
|
|
18
20
|
from glaip_sdk.exceptions import (
|
|
19
21
|
AuthenticationError,
|
|
20
22
|
ConflictError,
|
|
@@ -151,12 +153,7 @@ class BaseClient:
|
|
|
151
153
|
def timeout(self, value: float) -> None:
|
|
152
154
|
"""Set timeout and rebuild client."""
|
|
153
155
|
self._timeout = value
|
|
154
|
-
if (
|
|
155
|
-
hasattr(self, "http_client")
|
|
156
|
-
and self.http_client
|
|
157
|
-
and not self._session_scoped
|
|
158
|
-
and not self._parent_client
|
|
159
|
-
):
|
|
156
|
+
if hasattr(self, "http_client") and self.http_client and not self._session_scoped and not self._parent_client:
|
|
160
157
|
self.http_client.close()
|
|
161
158
|
self.http_client = self._build_client(value)
|
|
162
159
|
|
|
@@ -230,8 +227,13 @@ class BaseClient:
|
|
|
230
227
|
else:
|
|
231
228
|
raise
|
|
232
229
|
|
|
233
|
-
def
|
|
234
|
-
|
|
230
|
+
def _perform_request(
|
|
231
|
+
self,
|
|
232
|
+
method: str,
|
|
233
|
+
endpoint: str,
|
|
234
|
+
**kwargs: Any,
|
|
235
|
+
) -> httpx.Response:
|
|
236
|
+
"""Execute a raw HTTP request with retry handling."""
|
|
235
237
|
# Ensure client is alive before making request
|
|
236
238
|
self._ensure_client_alive()
|
|
237
239
|
|
|
@@ -239,20 +241,31 @@ class BaseClient:
|
|
|
239
241
|
try:
|
|
240
242
|
response = self.http_client.request(method, endpoint, **kwargs)
|
|
241
243
|
client_log.debug(f"Response status: {response.status_code}")
|
|
242
|
-
return
|
|
244
|
+
return response
|
|
243
245
|
except httpx.ConnectError as e:
|
|
244
|
-
client_log.warning(
|
|
245
|
-
f"Connection error on {method} {endpoint}, retrying once: {e}"
|
|
246
|
-
)
|
|
246
|
+
client_log.warning(f"Connection error on {method} {endpoint}, retrying once: {e}")
|
|
247
247
|
try:
|
|
248
248
|
response = self.http_client.request(method, endpoint, **kwargs)
|
|
249
|
-
client_log.debug(
|
|
250
|
-
|
|
251
|
-
)
|
|
252
|
-
return self._handle_response(response)
|
|
249
|
+
client_log.debug(f"Retry successful, response status: {response.status_code}")
|
|
250
|
+
return response
|
|
253
251
|
except httpx.ConnectError:
|
|
254
252
|
client_log.error(f"Retry failed for {method} {endpoint}: {e}")
|
|
255
|
-
raise
|
|
253
|
+
raise
|
|
254
|
+
|
|
255
|
+
def _request(self, method: str, endpoint: str, **kwargs) -> Any:
|
|
256
|
+
"""Make HTTP request with error handling and unwrap success envelopes."""
|
|
257
|
+
response = self._perform_request(method, endpoint, **kwargs)
|
|
258
|
+
return self._handle_response(response, unwrap=True)
|
|
259
|
+
|
|
260
|
+
def _request_with_envelope(
|
|
261
|
+
self,
|
|
262
|
+
method: str,
|
|
263
|
+
endpoint: str,
|
|
264
|
+
**kwargs: Any,
|
|
265
|
+
) -> Any:
|
|
266
|
+
"""Make HTTP request but return the full success envelope."""
|
|
267
|
+
response = self._perform_request(method, endpoint, **kwargs)
|
|
268
|
+
return self._handle_response(response, unwrap=False)
|
|
256
269
|
|
|
257
270
|
def _parse_response_content(self, response: httpx.Response) -> Any | None:
|
|
258
271
|
"""Parse response content based on content type."""
|
|
@@ -271,14 +284,14 @@ class BaseClient:
|
|
|
271
284
|
else:
|
|
272
285
|
return None # Let _handle_response deal with error status codes
|
|
273
286
|
|
|
274
|
-
def _handle_success_response(self, parsed: Any) -> Any:
|
|
287
|
+
def _handle_success_response(self, parsed: Any, *, unwrap: bool) -> Any:
|
|
275
288
|
"""Handle successful response with success flag."""
|
|
276
289
|
if isinstance(parsed, dict) and "success" in parsed:
|
|
277
290
|
if parsed.get("success"):
|
|
278
|
-
return parsed.get("data", parsed)
|
|
291
|
+
return parsed.get("data", parsed) if unwrap else parsed
|
|
279
292
|
else:
|
|
280
293
|
error_type = parsed.get("error", "UnknownError")
|
|
281
|
-
message =
|
|
294
|
+
message = self._format_error_dict({key: value for key, value in parsed.items() if key != "success"})
|
|
282
295
|
self._raise_api_error(
|
|
283
296
|
400,
|
|
284
297
|
message,
|
|
@@ -290,35 +303,142 @@ class BaseClient:
|
|
|
290
303
|
|
|
291
304
|
def _get_error_message(self, response: httpx.Response) -> str:
|
|
292
305
|
"""Extract error message from response, preferring parsed content."""
|
|
293
|
-
|
|
294
|
-
|
|
306
|
+
parsed = self._parse_error_json(response)
|
|
307
|
+
if parsed is None:
|
|
308
|
+
return response.text
|
|
309
|
+
|
|
310
|
+
formatted = self._format_parsed_error(parsed)
|
|
311
|
+
return formatted if formatted is not None else response.text
|
|
312
|
+
|
|
313
|
+
def _parse_error_json(self, response: httpx.Response) -> Any | None:
|
|
314
|
+
"""Safely parse JSON from an error response."""
|
|
295
315
|
try:
|
|
296
|
-
|
|
297
|
-
if isinstance(parsed, dict) and "message" in parsed:
|
|
298
|
-
error_message = parsed["message"]
|
|
299
|
-
elif isinstance(parsed, str):
|
|
300
|
-
error_message = parsed
|
|
316
|
+
return response.json()
|
|
301
317
|
except (ValueError, TypeError):
|
|
302
|
-
|
|
303
|
-
return error_message
|
|
318
|
+
return None
|
|
304
319
|
|
|
305
|
-
def
|
|
306
|
-
"""
|
|
307
|
-
|
|
308
|
-
|
|
320
|
+
def _format_parsed_error(self, parsed: Any) -> str | None:
|
|
321
|
+
"""Build a readable error message from parsed JSON payloads."""
|
|
322
|
+
if isinstance(parsed, dict):
|
|
323
|
+
return self._format_error_dict(parsed)
|
|
324
|
+
if isinstance(parsed, str):
|
|
325
|
+
return parsed
|
|
326
|
+
return str(parsed) if parsed else None
|
|
327
|
+
|
|
328
|
+
def _format_error_dict(self, parsed: dict[str, Any]) -> str:
|
|
329
|
+
"""Format structured API error payloads."""
|
|
330
|
+
detail = parsed.get("detail")
|
|
331
|
+
if isinstance(detail, list):
|
|
332
|
+
validation_message = self._format_validation_errors(detail)
|
|
333
|
+
if validation_message:
|
|
334
|
+
return validation_message
|
|
335
|
+
return f"Validation error: {parsed}"
|
|
336
|
+
|
|
337
|
+
formatted_details = None
|
|
338
|
+
if "details" in parsed:
|
|
339
|
+
formatted_details = self._format_error_details(parsed["details"])
|
|
340
|
+
|
|
341
|
+
message = parsed.get("message")
|
|
342
|
+
if message:
|
|
343
|
+
if formatted_details:
|
|
344
|
+
return f"{message}\n{formatted_details}"
|
|
345
|
+
return message
|
|
346
|
+
|
|
347
|
+
if formatted_details:
|
|
348
|
+
return formatted_details
|
|
349
|
+
|
|
350
|
+
return str(parsed) if parsed else DEFAULT_ERROR_MESSAGE
|
|
351
|
+
|
|
352
|
+
def _format_error_details(self, details: Any) -> str | None:
|
|
353
|
+
"""Render generic error details into a human-readable string."""
|
|
354
|
+
if details is None:
|
|
309
355
|
return None
|
|
310
356
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
error_message = self._get_error_message(response)
|
|
314
|
-
self._raise_api_error(response.status_code, error_message)
|
|
315
|
-
return None # Won't be reached but helps with type checking
|
|
357
|
+
if isinstance(details, dict):
|
|
358
|
+
return self._format_detail_mapping(details)
|
|
316
359
|
|
|
317
|
-
|
|
318
|
-
|
|
360
|
+
if isinstance(details, (list, tuple, set)):
|
|
361
|
+
return self._format_detail_iterable(details)
|
|
362
|
+
|
|
363
|
+
return f"Details: {details}"
|
|
364
|
+
|
|
365
|
+
@staticmethod
|
|
366
|
+
def _format_detail_mapping(details: Mapping[str, Any]) -> str | None:
|
|
367
|
+
"""Format details provided as a mapping."""
|
|
368
|
+
entries = [f" {key}: {value}" for key, value in details.items()]
|
|
369
|
+
if not entries:
|
|
370
|
+
return None
|
|
371
|
+
return "Details:\n" + "\n".join(entries)
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def _format_detail_iterable(details: Iterable[Any]) -> str | None:
|
|
375
|
+
"""Format details provided as an iterable collection."""
|
|
376
|
+
entries: list[str] = []
|
|
377
|
+
for item in details:
|
|
378
|
+
if isinstance(item, Mapping):
|
|
379
|
+
inner = ", ".join(f"{k}={v}" for k, v in item.items())
|
|
380
|
+
entries.append(f" - {inner if inner else '{}'}")
|
|
381
|
+
else:
|
|
382
|
+
entries.append(f" - {item}")
|
|
383
|
+
|
|
384
|
+
if not entries:
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
return "Details:\n" + "\n".join(entries)
|
|
388
|
+
|
|
389
|
+
def _format_validation_errors(self, errors: list[Any]) -> str | None:
|
|
390
|
+
"""Render validation errors into a human-readable string."""
|
|
391
|
+
entries: list[str] = []
|
|
392
|
+
for error in errors:
|
|
393
|
+
if isinstance(error, dict):
|
|
394
|
+
loc = " -> ".join(str(x) for x in error.get("loc", []))
|
|
395
|
+
msg = error.get("msg", DEFAULT_ERROR_MESSAGE)
|
|
396
|
+
error_type = error.get("type", "unknown")
|
|
397
|
+
prefix = loc if loc else "Field"
|
|
398
|
+
entries.append(f" {prefix}: {msg} ({error_type})")
|
|
399
|
+
else:
|
|
400
|
+
entries.append(f" {error}")
|
|
401
|
+
|
|
402
|
+
if not entries:
|
|
403
|
+
return None
|
|
404
|
+
|
|
405
|
+
return "Validation errors:\n" + "\n".join(entries)
|
|
406
|
+
|
|
407
|
+
@staticmethod
|
|
408
|
+
def _is_no_content_response(response: httpx.Response) -> bool:
|
|
409
|
+
"""Return True when the response contains no content."""
|
|
410
|
+
return response.status_code == 204
|
|
411
|
+
|
|
412
|
+
@staticmethod
|
|
413
|
+
def _is_success_status(response: httpx.Response) -> bool:
|
|
414
|
+
"""Return True for successful HTTP status codes."""
|
|
415
|
+
return 200 <= response.status_code < 300
|
|
416
|
+
|
|
417
|
+
def _handle_error_response(self, response: httpx.Response) -> None:
|
|
418
|
+
"""Raise an API error for non-success responses."""
|
|
419
|
+
error_message = self._get_error_message(response)
|
|
420
|
+
parsed_content = self._parse_response_content(response)
|
|
421
|
+
self._raise_api_error(response.status_code, error_message, payload=parsed_content)
|
|
422
|
+
|
|
423
|
+
def _handle_response(
|
|
424
|
+
self,
|
|
425
|
+
response: httpx.Response,
|
|
426
|
+
*,
|
|
427
|
+
unwrap: bool = True,
|
|
428
|
+
) -> Any:
|
|
429
|
+
"""Handle HTTP response with proper error handling."""
|
|
430
|
+
# Handle no-content success before general error handling
|
|
431
|
+
if self._is_no_content_response(response):
|
|
319
432
|
return None
|
|
320
433
|
|
|
321
|
-
|
|
434
|
+
if self._is_success_status(response):
|
|
435
|
+
parsed = self._parse_response_content(response)
|
|
436
|
+
if parsed is None:
|
|
437
|
+
return None
|
|
438
|
+
return self._handle_success_response(parsed, unwrap=unwrap)
|
|
439
|
+
|
|
440
|
+
self._handle_error_response(response)
|
|
441
|
+
return None
|
|
322
442
|
|
|
323
443
|
def _raise_api_error(
|
|
324
444
|
self,
|
|
@@ -363,12 +483,7 @@ class BaseClient:
|
|
|
363
483
|
|
|
364
484
|
def close(self) -> None:
|
|
365
485
|
"""Close the HTTP client."""
|
|
366
|
-
if (
|
|
367
|
-
hasattr(self, "http_client")
|
|
368
|
-
and self.http_client
|
|
369
|
-
and not self._session_scoped
|
|
370
|
-
and not self._parent_client
|
|
371
|
-
):
|
|
486
|
+
if hasattr(self, "http_client") and self.http_client and not self._session_scoped and not self._parent_client:
|
|
372
487
|
self.http_client.close()
|
|
373
488
|
|
|
374
489
|
def __enter__(self) -> "BaseClient":
|
glaip_sdk/client/main.py
CHANGED
|
@@ -3,29 +3,38 @@
|
|
|
3
3
|
|
|
4
4
|
Authors:
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
|
-
from
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
9
12
|
|
|
10
13
|
from glaip_sdk.client.agents import AgentClient
|
|
11
14
|
from glaip_sdk.client.base import BaseClient
|
|
12
15
|
from glaip_sdk.client.mcps import MCPClient
|
|
16
|
+
from glaip_sdk.client.shared import build_shared_config
|
|
13
17
|
from glaip_sdk.client.tools import ToolClient
|
|
14
|
-
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
20
|
+
from glaip_sdk.agents import Agent
|
|
21
|
+
from glaip_sdk.client._agent_payloads import AgentListResult
|
|
22
|
+
from glaip_sdk.mcps import MCP
|
|
23
|
+
from glaip_sdk.tools import Tool
|
|
15
24
|
|
|
16
25
|
|
|
17
26
|
class Client(BaseClient):
|
|
18
27
|
"""Main client that composes all specialized clients and shares one HTTP session."""
|
|
19
28
|
|
|
20
29
|
def __init__(self, **kwargs):
|
|
30
|
+
"""Initialize the main client.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
**kwargs: Client configuration arguments (api_url, api_key, timeout, etc.)
|
|
34
|
+
"""
|
|
21
35
|
super().__init__(**kwargs)
|
|
22
36
|
# Share the single httpx.Client + config with sub-clients
|
|
23
|
-
shared_config =
|
|
24
|
-
"parent_client": self,
|
|
25
|
-
"api_url": self.api_url,
|
|
26
|
-
"api_key": self.api_key,
|
|
27
|
-
"timeout": self._timeout,
|
|
28
|
-
}
|
|
37
|
+
shared_config = build_shared_config(self)
|
|
29
38
|
self.agents = AgentClient(**shared_config)
|
|
30
39
|
self.tools = ToolClient(**shared_config)
|
|
31
40
|
self.mcps = MCPClient(**shared_config)
|
|
@@ -37,6 +46,10 @@ class Client(BaseClient):
|
|
|
37
46
|
"""Create a new agent."""
|
|
38
47
|
return self.agents.create_agent(**kwargs)
|
|
39
48
|
|
|
49
|
+
def create_agent_from_file(self, *args, **kwargs) -> Agent:
|
|
50
|
+
"""Create a new agent from a JSON or YAML configuration file."""
|
|
51
|
+
return self.agents.create_agent_from_file(*args, **kwargs)
|
|
52
|
+
|
|
40
53
|
def list_agents(
|
|
41
54
|
self,
|
|
42
55
|
agent_type: str | None = None,
|
|
@@ -44,7 +57,7 @@ class Client(BaseClient):
|
|
|
44
57
|
name: str | None = None,
|
|
45
58
|
version: str | None = None,
|
|
46
59
|
sync_langflow_agents: bool = False,
|
|
47
|
-
) ->
|
|
60
|
+
) -> AgentListResult:
|
|
48
61
|
"""List agents with optional filtering.
|
|
49
62
|
|
|
50
63
|
Args:
|
|
@@ -55,7 +68,7 @@ class Client(BaseClient):
|
|
|
55
68
|
sync_langflow_agents: Sync with LangFlow server before listing (only applies when agent_type=langflow)
|
|
56
69
|
|
|
57
70
|
Returns:
|
|
58
|
-
|
|
71
|
+
AgentListResult with agents and pagination metadata. Supports iteration and indexing.
|
|
59
72
|
"""
|
|
60
73
|
return self.agents.list_agents(
|
|
61
74
|
agent_type=agent_type,
|
|
@@ -81,6 +94,10 @@ class Client(BaseClient):
|
|
|
81
94
|
"""Update an existing agent."""
|
|
82
95
|
return self.agents.update_agent(agent_id, **kwargs)
|
|
83
96
|
|
|
97
|
+
def update_agent_from_file(self, agent_id: str, *args, **kwargs) -> Agent:
|
|
98
|
+
"""Update an existing agent using a JSON or YAML configuration file."""
|
|
99
|
+
return self.agents.update_agent_from_file(agent_id, *args, **kwargs)
|
|
100
|
+
|
|
84
101
|
def delete_agent(self, agent_id: str) -> bool:
|
|
85
102
|
"""Delete an agent."""
|
|
86
103
|
return self.agents.delete_agent(agent_id)
|
|
@@ -145,9 +162,9 @@ class Client(BaseClient):
|
|
|
145
162
|
"""Get tool script content."""
|
|
146
163
|
return self.tools.get_tool_script(tool_id)
|
|
147
164
|
|
|
148
|
-
def update_tool_via_file(self, tool_id: str, file_path: str) -> Tool:
|
|
165
|
+
def update_tool_via_file(self, tool_id: str, file_path: str, **kwargs) -> Tool:
|
|
149
166
|
"""Update tool via file."""
|
|
150
|
-
return self.tools.update_tool_via_file(tool_id, file_path)
|
|
167
|
+
return self.tools.update_tool_via_file(tool_id, file_path, **kwargs)
|
|
151
168
|
|
|
152
169
|
# MCPs
|
|
153
170
|
def create_mcp(self, **kwargs) -> MCP:
|
|
@@ -199,10 +216,16 @@ class Client(BaseClient):
|
|
|
199
216
|
# ---- Timeout propagation ----
|
|
200
217
|
@property
|
|
201
218
|
def timeout(self) -> float: # type: ignore[override]
|
|
219
|
+
"""Get the client timeout value."""
|
|
202
220
|
return super().timeout
|
|
203
221
|
|
|
204
222
|
@timeout.setter
|
|
205
223
|
def timeout(self, value: float) -> None: # type: ignore[override]
|
|
224
|
+
"""Set the client timeout and propagate to sub-clients.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
value: Timeout value in seconds.
|
|
228
|
+
"""
|
|
206
229
|
# Rebuild the root http client
|
|
207
230
|
BaseClient.timeout.fset(self, value) # call parent setter
|
|
208
231
|
# Propagate the new session to sub-clients so they don't hold a closed client
|
glaip_sdk/client/mcps.py
CHANGED
|
@@ -3,18 +3,22 @@
|
|
|
3
3
|
|
|
4
4
|
Authors:
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import logging
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
11
12
|
from glaip_sdk.client.base import BaseClient
|
|
12
|
-
from glaip_sdk.config.constants import
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
from glaip_sdk.config.constants import DEFAULT_MCP_TRANSPORT, DEFAULT_MCP_TYPE
|
|
14
|
+
from glaip_sdk.mcps import MCP
|
|
15
|
+
from glaip_sdk.models import MCPResponse
|
|
16
|
+
from glaip_sdk.utils.client_utils import (
|
|
17
|
+
add_kwargs_to_payload,
|
|
18
|
+
create_model_instances,
|
|
19
|
+
find_by_name,
|
|
15
20
|
)
|
|
16
|
-
from glaip_sdk.
|
|
17
|
-
from glaip_sdk.utils.client_utils import create_model_instances, find_by_name
|
|
21
|
+
from glaip_sdk.utils.resource_refs import is_uuid
|
|
18
22
|
|
|
19
23
|
# API endpoints
|
|
20
24
|
MCPS_ENDPOINT = "/mcps/"
|
|
@@ -45,7 +49,8 @@ class MCPClient(BaseClient):
|
|
|
45
49
|
def get_mcp_by_id(self, mcp_id: str) -> MCP:
|
|
46
50
|
"""Get MCP by ID."""
|
|
47
51
|
data = self._request("GET", f"{MCPS_ENDPOINT}{mcp_id}")
|
|
48
|
-
|
|
52
|
+
response = MCPResponse(**data)
|
|
53
|
+
return MCP.from_response(response, client=self)
|
|
49
54
|
|
|
50
55
|
def find_mcps(self, name: str | None = None) -> list[MCP]:
|
|
51
56
|
"""Find MCPs by name."""
|
|
@@ -57,7 +62,7 @@ class MCPClient(BaseClient):
|
|
|
57
62
|
def create_mcp(
|
|
58
63
|
self,
|
|
59
64
|
name: str,
|
|
60
|
-
description: str,
|
|
65
|
+
description: str | None = None,
|
|
61
66
|
config: dict[str, Any] | None = None,
|
|
62
67
|
**kwargs,
|
|
63
68
|
) -> MCP:
|
|
@@ -77,7 +82,8 @@ class MCPClient(BaseClient):
|
|
|
77
82
|
get_endpoint_fmt=f"{MCPS_ENDPOINT}{{id}}",
|
|
78
83
|
json=payload,
|
|
79
84
|
)
|
|
80
|
-
|
|
85
|
+
response = MCPResponse(**full_mcp_data)
|
|
86
|
+
return MCP.from_response(response, client=self)
|
|
81
87
|
|
|
82
88
|
def update_mcp(self, mcp_id: str, **kwargs) -> MCP:
|
|
83
89
|
"""Update an existing MCP.
|
|
@@ -99,16 +105,106 @@ class MCPClient(BaseClient):
|
|
|
99
105
|
method = "PATCH"
|
|
100
106
|
|
|
101
107
|
data = self._request(method, f"{MCPS_ENDPOINT}{mcp_id}", json=kwargs)
|
|
102
|
-
|
|
108
|
+
response = MCPResponse(**data)
|
|
109
|
+
return MCP.from_response(response, client=self)
|
|
103
110
|
|
|
104
111
|
def delete_mcp(self, mcp_id: str) -> None:
|
|
105
112
|
"""Delete an MCP."""
|
|
106
113
|
self._request("DELETE", f"{MCPS_ENDPOINT}{mcp_id}")
|
|
107
114
|
|
|
115
|
+
def upsert_mcp(
|
|
116
|
+
self,
|
|
117
|
+
identifier: str | MCP,
|
|
118
|
+
description: str | None = None,
|
|
119
|
+
config: dict[str, Any] | None = None,
|
|
120
|
+
**kwargs,
|
|
121
|
+
) -> MCP:
|
|
122
|
+
"""Create or update an MCP by instance, ID, or name.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
identifier: MCP instance, ID (UUID string), or name
|
|
126
|
+
description: MCP description
|
|
127
|
+
config: MCP configuration dictionary
|
|
128
|
+
**kwargs: Additional parameters (transport, metadata, etc.)
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The created or updated MCP.
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
>>> # By name (creates if not exists)
|
|
135
|
+
>>> mcp = client.mcps.upsert_mcp(
|
|
136
|
+
... "deepwiki",
|
|
137
|
+
... transport="sse",
|
|
138
|
+
... config={"url": "https://mcp.deepwiki.com/sse"},
|
|
139
|
+
... )
|
|
140
|
+
>>> # By instance
|
|
141
|
+
>>> mcp = client.mcps.upsert_mcp(existing_mcp, description="Updated")
|
|
142
|
+
>>> # By ID
|
|
143
|
+
>>> mcp = client.mcps.upsert_mcp("uuid-here", description="Updated")
|
|
144
|
+
"""
|
|
145
|
+
# Handle MCP instance
|
|
146
|
+
if isinstance(identifier, MCP):
|
|
147
|
+
if identifier.id:
|
|
148
|
+
logger.info("Updating MCP by instance: %s", identifier.name)
|
|
149
|
+
return self._do_upsert_update(identifier.id, identifier.name, description, config, **kwargs)
|
|
150
|
+
# MCP without ID - treat name as identifier
|
|
151
|
+
identifier = identifier.name
|
|
152
|
+
|
|
153
|
+
# Handle string (ID or name)
|
|
154
|
+
if isinstance(identifier, str):
|
|
155
|
+
if is_uuid(identifier):
|
|
156
|
+
logger.info("Updating MCP by ID: %s", identifier)
|
|
157
|
+
existing = self.get_mcp_by_id(identifier)
|
|
158
|
+
return self._do_upsert_update(identifier, existing.name, description, config, **kwargs)
|
|
159
|
+
|
|
160
|
+
# It's a name - find or create
|
|
161
|
+
return self._upsert_by_name(identifier, description, config, **kwargs)
|
|
162
|
+
|
|
163
|
+
raise ValueError(f"Invalid identifier type: {type(identifier)}")
|
|
164
|
+
|
|
165
|
+
def _do_upsert_update(
|
|
166
|
+
self,
|
|
167
|
+
mcp_id: str,
|
|
168
|
+
name: str | None,
|
|
169
|
+
description: str | None,
|
|
170
|
+
config: dict[str, Any] | None,
|
|
171
|
+
**kwargs,
|
|
172
|
+
) -> MCP:
|
|
173
|
+
"""Perform the update part of upsert."""
|
|
174
|
+
update_kwargs = {**kwargs}
|
|
175
|
+
if name is not None:
|
|
176
|
+
update_kwargs["name"] = name
|
|
177
|
+
if description is not None:
|
|
178
|
+
update_kwargs["description"] = description
|
|
179
|
+
if config is not None:
|
|
180
|
+
update_kwargs["config"] = config
|
|
181
|
+
return self.update_mcp(mcp_id, **update_kwargs)
|
|
182
|
+
|
|
183
|
+
def _upsert_by_name(
|
|
184
|
+
self,
|
|
185
|
+
name: str,
|
|
186
|
+
description: str | None,
|
|
187
|
+
config: dict[str, Any] | None,
|
|
188
|
+
**kwargs,
|
|
189
|
+
) -> MCP:
|
|
190
|
+
"""Find by name and update, or create if not found."""
|
|
191
|
+
existing = self.find_mcps(name)
|
|
192
|
+
|
|
193
|
+
if len(existing) == 1:
|
|
194
|
+
logger.info("Updating existing MCP: %s", name)
|
|
195
|
+
return self._do_upsert_update(existing[0].id, name, description, config, **kwargs)
|
|
196
|
+
|
|
197
|
+
if len(existing) > 1:
|
|
198
|
+
raise ValueError(f"Multiple MCPs found with name '{name}'")
|
|
199
|
+
|
|
200
|
+
# Create new MCP
|
|
201
|
+
logger.info("Creating new MCP: %s", name)
|
|
202
|
+
return self.create_mcp(name=name, description=description, config=config, **kwargs)
|
|
203
|
+
|
|
108
204
|
def _build_create_payload(
|
|
109
205
|
self,
|
|
110
206
|
name: str,
|
|
111
|
-
description: str,
|
|
207
|
+
description: str | None = None,
|
|
112
208
|
transport: str = DEFAULT_MCP_TRANSPORT,
|
|
113
209
|
config: dict[str, Any] | None = None,
|
|
114
210
|
**kwargs,
|
|
@@ -122,7 +218,7 @@ class MCPClient(BaseClient):
|
|
|
122
218
|
|
|
123
219
|
Args:
|
|
124
220
|
name: MCP name
|
|
125
|
-
description: MCP description
|
|
221
|
+
description: MCP description (optional)
|
|
126
222
|
transport: MCP transport protocol (defaults to stdio)
|
|
127
223
|
config: MCP configuration dictionary
|
|
128
224
|
**kwargs: Additional parameters
|
|
@@ -147,9 +243,7 @@ class MCPClient(BaseClient):
|
|
|
147
243
|
|
|
148
244
|
# Add any other kwargs (excluding already handled ones)
|
|
149
245
|
excluded_keys = {"type"} # type is handled above
|
|
150
|
-
|
|
151
|
-
if key not in excluded_keys:
|
|
152
|
-
payload[key] = value
|
|
246
|
+
add_kwargs_to_payload(payload, kwargs, excluded_keys)
|
|
153
247
|
|
|
154
248
|
return payload
|
|
155
249
|
|
|
@@ -179,9 +273,7 @@ class MCPClient(BaseClient):
|
|
|
179
273
|
update_data = {
|
|
180
274
|
"name": name if name is not None else current_mcp.name,
|
|
181
275
|
"type": DEFAULT_MCP_TYPE, # Required by backend, MCPs are always server type
|
|
182
|
-
"transport": kwargs.get(
|
|
183
|
-
"transport", getattr(current_mcp, "transport", DEFAULT_MCP_TRANSPORT)
|
|
184
|
-
),
|
|
276
|
+
"transport": kwargs.get("transport", getattr(current_mcp, "transport", DEFAULT_MCP_TRANSPORT)),
|
|
185
277
|
}
|
|
186
278
|
|
|
187
279
|
# Handle description with proper None handling
|
|
@@ -208,7 +300,23 @@ class MCPClient(BaseClient):
|
|
|
208
300
|
def get_mcp_tools(self, mcp_id: str) -> list[dict[str, Any]]:
|
|
209
301
|
"""Get tools available from an MCP."""
|
|
210
302
|
data = self._request("GET", f"{MCPS_ENDPOINT}{mcp_id}/tools")
|
|
211
|
-
|
|
303
|
+
if data is None:
|
|
304
|
+
return []
|
|
305
|
+
if isinstance(data, list):
|
|
306
|
+
return data
|
|
307
|
+
if isinstance(data, dict):
|
|
308
|
+
if "tools" in data:
|
|
309
|
+
return data.get("tools", []) or []
|
|
310
|
+
logger.warning(
|
|
311
|
+
"Unexpected MCP tools response keys %s; returning empty list",
|
|
312
|
+
list(data.keys()),
|
|
313
|
+
)
|
|
314
|
+
return []
|
|
315
|
+
logger.warning(
|
|
316
|
+
"Unexpected MCP tools response type %s; returning empty list",
|
|
317
|
+
type(data).__name__,
|
|
318
|
+
)
|
|
319
|
+
return []
|
|
212
320
|
|
|
213
321
|
def test_mcp_connection(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
214
322
|
"""Test MCP connection using configuration.
|