glaip-sdk 0.1.3__py3-none-any.whl → 0.6.10__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 (141) hide show
  1. glaip_sdk/__init__.py +5 -2
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1191 -0
  5. glaip_sdk/branding.py +13 -0
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/auth.py +254 -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.py +213 -73
  11. glaip_sdk/cli/commands/common_config.py +101 -0
  12. glaip_sdk/cli/commands/configure.py +729 -113
  13. glaip_sdk/cli/commands/mcps.py +241 -72
  14. glaip_sdk/cli/commands/models.py +11 -5
  15. glaip_sdk/cli/commands/tools.py +49 -57
  16. glaip_sdk/cli/commands/transcripts.py +755 -0
  17. glaip_sdk/cli/config.py +48 -4
  18. glaip_sdk/cli/constants.py +38 -0
  19. glaip_sdk/cli/context.py +8 -0
  20. glaip_sdk/cli/core/__init__.py +79 -0
  21. glaip_sdk/cli/core/context.py +124 -0
  22. glaip_sdk/cli/core/output.py +846 -0
  23. glaip_sdk/cli/core/prompting.py +649 -0
  24. glaip_sdk/cli/core/rendering.py +187 -0
  25. glaip_sdk/cli/display.py +35 -19
  26. glaip_sdk/cli/hints.py +57 -0
  27. glaip_sdk/cli/io.py +6 -3
  28. glaip_sdk/cli/main.py +228 -119
  29. glaip_sdk/cli/masking.py +21 -33
  30. glaip_sdk/cli/pager.py +9 -10
  31. glaip_sdk/cli/parsers/__init__.py +1 -3
  32. glaip_sdk/cli/slash/__init__.py +0 -9
  33. glaip_sdk/cli/slash/accounts_controller.py +578 -0
  34. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  35. glaip_sdk/cli/slash/agent_session.py +62 -21
  36. glaip_sdk/cli/slash/prompt.py +21 -0
  37. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  38. glaip_sdk/cli/slash/session.py +771 -140
  39. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  40. glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
  41. glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
  42. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  43. glaip_sdk/cli/slash/tui/loading.py +58 -0
  44. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  45. glaip_sdk/cli/transcript/__init__.py +12 -52
  46. glaip_sdk/cli/transcript/cache.py +255 -44
  47. glaip_sdk/cli/transcript/capture.py +27 -1
  48. glaip_sdk/cli/transcript/history.py +815 -0
  49. glaip_sdk/cli/transcript/viewer.py +72 -499
  50. glaip_sdk/cli/update_notifier.py +14 -5
  51. glaip_sdk/cli/utils.py +243 -1252
  52. glaip_sdk/cli/validators.py +5 -6
  53. glaip_sdk/client/__init__.py +2 -1
  54. glaip_sdk/client/_agent_payloads.py +45 -9
  55. glaip_sdk/client/agent_runs.py +147 -0
  56. glaip_sdk/client/agents.py +287 -29
  57. glaip_sdk/client/base.py +1 -0
  58. glaip_sdk/client/main.py +19 -10
  59. glaip_sdk/client/mcps.py +122 -12
  60. glaip_sdk/client/run_rendering.py +133 -88
  61. glaip_sdk/client/shared.py +21 -0
  62. glaip_sdk/client/tools.py +155 -10
  63. glaip_sdk/config/constants.py +11 -0
  64. glaip_sdk/mcps/__init__.py +21 -0
  65. glaip_sdk/mcps/base.py +345 -0
  66. glaip_sdk/models/__init__.py +90 -0
  67. glaip_sdk/models/agent.py +47 -0
  68. glaip_sdk/models/agent_runs.py +116 -0
  69. glaip_sdk/models/common.py +42 -0
  70. glaip_sdk/models/mcp.py +33 -0
  71. glaip_sdk/models/tool.py +33 -0
  72. glaip_sdk/payload_schemas/__init__.py +1 -13
  73. glaip_sdk/registry/__init__.py +55 -0
  74. glaip_sdk/registry/agent.py +164 -0
  75. glaip_sdk/registry/base.py +139 -0
  76. glaip_sdk/registry/mcp.py +253 -0
  77. glaip_sdk/registry/tool.py +232 -0
  78. glaip_sdk/rich_components.py +58 -2
  79. glaip_sdk/runner/__init__.py +59 -0
  80. glaip_sdk/runner/base.py +84 -0
  81. glaip_sdk/runner/deps.py +115 -0
  82. glaip_sdk/runner/langgraph.py +706 -0
  83. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  84. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  85. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  86. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  87. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  88. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  89. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
  90. glaip_sdk/tools/__init__.py +22 -0
  91. glaip_sdk/tools/base.py +435 -0
  92. glaip_sdk/utils/__init__.py +58 -12
  93. glaip_sdk/utils/a2a/__init__.py +34 -0
  94. glaip_sdk/utils/a2a/event_processor.py +188 -0
  95. glaip_sdk/utils/bundler.py +267 -0
  96. glaip_sdk/utils/client.py +111 -0
  97. glaip_sdk/utils/client_utils.py +39 -7
  98. glaip_sdk/utils/datetime_helpers.py +58 -0
  99. glaip_sdk/utils/discovery.py +78 -0
  100. glaip_sdk/utils/display.py +23 -15
  101. glaip_sdk/utils/export.py +143 -0
  102. glaip_sdk/utils/general.py +0 -33
  103. glaip_sdk/utils/import_export.py +12 -7
  104. glaip_sdk/utils/import_resolver.py +492 -0
  105. glaip_sdk/utils/instructions.py +101 -0
  106. glaip_sdk/utils/rendering/__init__.py +115 -1
  107. glaip_sdk/utils/rendering/formatting.py +5 -30
  108. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  109. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  110. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  111. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  112. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  113. glaip_sdk/utils/rendering/models.py +1 -0
  114. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  115. glaip_sdk/utils/rendering/renderer/base.py +217 -1476
  116. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  117. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  118. glaip_sdk/utils/rendering/renderer/stream.py +4 -12
  119. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  120. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  121. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  122. glaip_sdk/utils/rendering/state.py +204 -0
  123. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  124. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  125. glaip_sdk/utils/rendering/steps/format.py +176 -0
  126. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  127. glaip_sdk/utils/rendering/timing.py +36 -0
  128. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  129. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  130. glaip_sdk/utils/resource_refs.py +25 -13
  131. glaip_sdk/utils/runtime_config.py +425 -0
  132. glaip_sdk/utils/serialization.py +18 -0
  133. glaip_sdk/utils/sync.py +142 -0
  134. glaip_sdk/utils/tool_detection.py +33 -0
  135. glaip_sdk/utils/validation.py +16 -24
  136. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/METADATA +42 -4
  137. glaip_sdk-0.6.10.dist-info/RECORD +159 -0
  138. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/WHEEL +1 -1
  139. glaip_sdk/models.py +0 -240
  140. glaip_sdk-0.1.3.dist-info/RECORD +0 -83
  141. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/entry_points.txt +0 -0
@@ -3,31 +3,37 @@
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
 
9
+ import asyncio
8
10
  import json
9
11
  import logging
10
12
  from collections.abc import AsyncGenerator, Callable, Iterator, Mapping
13
+ from contextlib import asynccontextmanager
11
14
  from os import PathLike
12
15
  from pathlib import Path
13
16
  from typing import Any, BinaryIO
14
17
 
15
18
  import httpx
16
-
19
+ from glaip_sdk.agents import Agent
17
20
  from glaip_sdk.client._agent_payloads import (
18
21
  AgentCreateRequest,
19
22
  AgentListParams,
20
23
  AgentListResult,
21
24
  AgentUpdateRequest,
22
25
  )
26
+ from glaip_sdk.client.agent_runs import AgentRunsClient
23
27
  from glaip_sdk.client.base import BaseClient
24
28
  from glaip_sdk.client.mcps import MCPClient
25
29
  from glaip_sdk.client.run_rendering import (
26
30
  AgentRunRenderingManager,
27
31
  compute_timeout_seconds,
28
32
  )
33
+ from glaip_sdk.client.shared import build_shared_config
29
34
  from glaip_sdk.client.tools import ToolClient
30
35
  from glaip_sdk.config.constants import (
36
+ AGENT_CONFIG_FIELDS,
31
37
  DEFAULT_AGENT_FRAMEWORK,
32
38
  DEFAULT_AGENT_RUN_TIMEOUT,
33
39
  DEFAULT_AGENT_TYPE,
@@ -35,7 +41,7 @@ from glaip_sdk.config.constants import (
35
41
  DEFAULT_MODEL,
36
42
  )
37
43
  from glaip_sdk.exceptions import NotFoundError, ValidationError
38
- from glaip_sdk.models import Agent
44
+ from glaip_sdk.models import AgentResponse
39
45
  from glaip_sdk.payload_schemas.agent import list_server_only_fields
40
46
  from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
41
47
  from glaip_sdk.utils.client_utils import (
@@ -67,6 +73,21 @@ _MERGED_SEQUENCE_FIELDS = ("tools", "agents", "mcps")
67
73
  _DEFAULT_METADATA_TYPE = "custom"
68
74
 
69
75
 
76
+ @asynccontextmanager
77
+ async def _async_timeout_guard(
78
+ timeout_seconds: float | None,
79
+ ) -> AsyncGenerator[None, None]:
80
+ """Apply an asyncio timeout when a custom timeout is provided."""
81
+ if timeout_seconds is None:
82
+ yield
83
+ return
84
+ try:
85
+ async with asyncio.timeout(timeout_seconds):
86
+ yield
87
+ except asyncio.TimeoutError as exc:
88
+ raise httpx.TimeoutException(f"Request timed out after {timeout_seconds}s") from exc
89
+
90
+
70
91
  def _normalise_sequence(value: Any) -> list[Any] | None:
71
92
  """Normalise optional sequence inputs to plain lists."""
72
93
  if value is None:
@@ -193,19 +214,7 @@ def _extract_original_refs(raw_definition: dict) -> dict[str, list]:
193
214
 
194
215
  def _build_cli_args(overrides_dict: dict) -> dict[str, Any]:
195
216
  """Build CLI args from overrides, filtering out None values."""
196
- cli_args = {
197
- key: overrides_dict.get(key)
198
- for key in (
199
- "name",
200
- "instruction",
201
- "model",
202
- "tools",
203
- "agents",
204
- "mcps",
205
- "timeout",
206
- )
207
- if overrides_dict.get(key) is not None
208
- }
217
+ cli_args = {key: overrides_dict.get(key) for key in AGENT_CONFIG_FIELDS if overrides_dict.get(key) is not None}
209
218
 
210
219
  # Normalize sequence fields
211
220
  for field in _MERGED_SEQUENCE_FIELDS:
@@ -254,6 +263,7 @@ class AgentClient(BaseClient):
254
263
  self._renderer_manager = AgentRunRenderingManager(logger)
255
264
  self._tool_client: ToolClient | None = None
256
265
  self._mcp_client: MCPClient | None = None
266
+ self._runs_client: AgentRunsClient | None = None
257
267
 
258
268
  def list_agents(
259
269
  self,
@@ -352,7 +362,8 @@ class AgentClient(BaseClient):
352
362
  status_code=404,
353
363
  )
354
364
 
355
- return Agent(**data)._set_client(self)
365
+ response = AgentResponse(**data)
366
+ return Agent.from_response(response, client=self)
356
367
 
357
368
  def find_agents(self, name: str | None = None) -> list[Agent]:
358
369
  """Find agents by name."""
@@ -366,6 +377,11 @@ class AgentClient(BaseClient):
366
377
  # Renderer delegation helpers
367
378
  # ------------------------------------------------------------------ #
368
379
  def _get_renderer_manager(self) -> AgentRunRenderingManager:
380
+ """Get or create the renderer manager instance.
381
+
382
+ Returns:
383
+ AgentRunRenderingManager instance.
384
+ """
369
385
  manager = getattr(self, "_renderer_manager", None)
370
386
  if manager is None:
371
387
  manager = AgentRunRenderingManager(logger)
@@ -373,6 +389,15 @@ class AgentClient(BaseClient):
373
389
  return manager
374
390
 
375
391
  def _create_renderer(self, renderer: RichStreamRenderer | str | None, **kwargs: Any) -> RichStreamRenderer:
392
+ """Create or return a renderer instance.
393
+
394
+ Args:
395
+ renderer: Renderer instance, string identifier, or None.
396
+ **kwargs: Additional keyword arguments (e.g., verbose).
397
+
398
+ Returns:
399
+ RichStreamRenderer instance.
400
+ """
376
401
  manager = self._get_renderer_manager()
377
402
  verbose = kwargs.get("verbose", False)
378
403
  if isinstance(renderer, RichStreamRenderer) or hasattr(renderer, "on_start"):
@@ -387,6 +412,18 @@ class AgentClient(BaseClient):
387
412
  agent_name: str | None,
388
413
  meta: dict[str, Any],
389
414
  ) -> tuple[str, dict[str, Any], float | None, float | None]:
415
+ """Process stream events from an HTTP response.
416
+
417
+ Args:
418
+ stream_response: HTTP response stream.
419
+ renderer: Renderer to use for displaying events.
420
+ timeout_seconds: Timeout in seconds.
421
+ agent_name: Optional agent name.
422
+ meta: Metadata dictionary.
423
+
424
+ Returns:
425
+ Tuple of (final_text, stats_usage, started_monotonic, finished_monotonic).
426
+ """
390
427
  manager = self._get_renderer_manager()
391
428
  return manager.process_stream_events(
392
429
  stream_response,
@@ -404,6 +441,18 @@ class AgentClient(BaseClient):
404
441
  started_monotonic: float | None,
405
442
  finished_monotonic: float | None,
406
443
  ) -> str:
444
+ """Finalize the renderer and return the final response text.
445
+
446
+ Args:
447
+ renderer: Renderer to finalize.
448
+ final_text: Final text content.
449
+ stats_usage: Usage statistics dictionary.
450
+ started_monotonic: Start time (monotonic).
451
+ finished_monotonic: Finish time (monotonic).
452
+
453
+ Returns:
454
+ Final text string.
455
+ """
407
456
  manager = self._get_renderer_manager()
408
457
  return manager.finalize_renderer(
409
458
  renderer,
@@ -414,11 +463,21 @@ class AgentClient(BaseClient):
414
463
  )
415
464
 
416
465
  def _get_tool_client(self) -> ToolClient:
466
+ """Get or create the tool client instance.
467
+
468
+ Returns:
469
+ ToolClient instance.
470
+ """
417
471
  if self._tool_client is None:
418
472
  self._tool_client = ToolClient(parent_client=self)
419
473
  return self._tool_client
420
474
 
421
475
  def _get_mcp_client(self) -> MCPClient:
476
+ """Get or create the MCP client instance.
477
+
478
+ Returns:
479
+ MCPClient instance.
480
+ """
422
481
  if self._mcp_client is None:
423
482
  self._mcp_client = MCPClient(parent_client=self)
424
483
  return self._mcp_client
@@ -428,6 +487,15 @@ class AgentClient(BaseClient):
428
487
  entry: Any,
429
488
  fallback_iter: Iterator[Any] | None,
430
489
  ) -> tuple[str | None, str | None]:
490
+ """Normalize a reference entry to extract ID and name.
491
+
492
+ Args:
493
+ entry: Reference entry (string, dict, or other).
494
+ fallback_iter: Optional iterator for fallback values.
495
+
496
+ Returns:
497
+ Tuple of (entry_id, entry_name).
498
+ """
431
499
  entry_id: str | None = None
432
500
  entry_name: str | None = None
433
501
 
@@ -464,6 +532,19 @@ class AgentClient(BaseClient):
464
532
  label: str,
465
533
  plural_label: str | None = None,
466
534
  ) -> list[str] | None:
535
+ """Resolve a list of resource references to IDs.
536
+
537
+ Args:
538
+ items: List of resource references to resolve.
539
+ references: Optional list of reference objects for fallback.
540
+ fetch_by_id: Function to fetch resource by ID.
541
+ find_by_name: Function to find resources by name.
542
+ label: Singular label for error messages.
543
+ plural_label: Plural label for error messages.
544
+
545
+ Returns:
546
+ List of resolved resource IDs, or None if items is empty.
547
+ """
467
548
  if not items:
468
549
  return None
469
550
 
@@ -495,6 +576,22 @@ class AgentClient(BaseClient):
495
576
  singular: str,
496
577
  plural: str,
497
578
  ) -> str:
579
+ """Resolve a single resource reference to an ID.
580
+
581
+ Args:
582
+ entry: Resource reference to resolve.
583
+ fallback_iter: Optional iterator for fallback values.
584
+ fetch_by_id: Function to fetch resource by ID.
585
+ find_by_name: Function to find resources by name.
586
+ singular: Singular label for error messages.
587
+ plural: Plural label for error messages.
588
+
589
+ Returns:
590
+ Resolved resource ID string.
591
+
592
+ Raises:
593
+ ValueError: If the resource cannot be resolved.
594
+ """
498
595
  entry_id, entry_name = self._normalise_reference_entry(entry, fallback_iter)
499
596
 
500
597
  validated_id = self._validate_resource_id(fetch_by_id, entry_id)
@@ -511,6 +608,14 @@ class AgentClient(BaseClient):
511
608
 
512
609
  @staticmethod
513
610
  def _coerce_reference_value(entry: Any) -> str:
611
+ """Coerce a reference entry to a string value.
612
+
613
+ Args:
614
+ entry: Reference entry (dict, string, or other).
615
+
616
+ Returns:
617
+ String representation of the reference.
618
+ """
514
619
  if isinstance(entry, dict):
515
620
  if entry.get("id"):
516
621
  return str(entry["id"])
@@ -520,6 +625,15 @@ class AgentClient(BaseClient):
520
625
 
521
626
  @staticmethod
522
627
  def _validate_resource_id(fetch_by_id: Callable[[str], Any], candidate_id: str | None) -> str | None:
628
+ """Validate a resource ID by attempting to fetch it.
629
+
630
+ Args:
631
+ fetch_by_id: Function to fetch resource by ID.
632
+ candidate_id: Candidate ID to validate.
633
+
634
+ Returns:
635
+ Validated ID if found, None otherwise.
636
+ """
523
637
  if not candidate_id:
524
638
  return None
525
639
  try:
@@ -535,6 +649,20 @@ class AgentClient(BaseClient):
535
649
  singular: str,
536
650
  plural: str,
537
651
  ) -> tuple[str, bool]:
652
+ """Resolve a resource by name to an ID.
653
+
654
+ Args:
655
+ find_by_name: Function to find resources by name.
656
+ entry_name: Name of the resource to find.
657
+ singular: Singular label for error messages.
658
+ plural: Plural label for error messages.
659
+
660
+ Returns:
661
+ Tuple of (resolved_id, success).
662
+
663
+ Raises:
664
+ ValueError: If resource not found or multiple matches exist.
665
+ """
538
666
  try:
539
667
  matches = find_by_name(entry_name)
540
668
  except Exception:
@@ -555,6 +683,15 @@ class AgentClient(BaseClient):
555
683
  tools: list[Any] | None,
556
684
  references: list[Any] | None = None,
557
685
  ) -> list[str] | None:
686
+ """Resolve tool references to IDs.
687
+
688
+ Args:
689
+ tools: List of tool references to resolve.
690
+ references: Optional list of reference objects for fallback.
691
+
692
+ Returns:
693
+ List of resolved tool IDs, or None if tools is empty.
694
+ """
558
695
  tool_client = self._get_tool_client()
559
696
  return self._resolve_resource_ids(
560
697
  tools,
@@ -570,6 +707,15 @@ class AgentClient(BaseClient):
570
707
  agents: list[Any] | None,
571
708
  references: list[Any] | None = None,
572
709
  ) -> list[str] | None:
710
+ """Resolve agent references to IDs.
711
+
712
+ Args:
713
+ agents: List of agent references to resolve.
714
+ references: Optional list of reference objects for fallback.
715
+
716
+ Returns:
717
+ List of resolved agent IDs, or None if agents is empty.
718
+ """
573
719
  return self._resolve_resource_ids(
574
720
  agents,
575
721
  references,
@@ -584,6 +730,15 @@ class AgentClient(BaseClient):
584
730
  mcps: list[Any] | None,
585
731
  references: list[Any] | None = None,
586
732
  ) -> list[str] | None:
733
+ """Resolve MCP references to IDs.
734
+
735
+ Args:
736
+ mcps: List of MCP references to resolve.
737
+ references: Optional list of reference objects for fallback.
738
+
739
+ Returns:
740
+ List of resolved MCP IDs, or None if mcps is empty.
741
+ """
587
742
  mcp_client = self._get_mcp_client()
588
743
  return self._resolve_resource_ids(
589
744
  mcps,
@@ -675,7 +830,8 @@ class AgentClient(BaseClient):
675
830
  get_endpoint_fmt=f"{AGENTS_ENDPOINT}{{id}}",
676
831
  json=payload_dict,
677
832
  )
678
- return Agent(**full_agent_data)._set_client(self)
833
+ response = AgentResponse(**full_agent_data)
834
+ return Agent.from_response(response, client=self)
679
835
 
680
836
  def create_agent(
681
837
  self,
@@ -770,8 +926,9 @@ class AgentClient(BaseClient):
770
926
 
771
927
  payload_dict = request.to_payload(current_agent)
772
928
 
773
- response = self._request("PUT", f"/agents/{agent_id}", json=payload_dict)
774
- return Agent(**response)._set_client(self)
929
+ api_response = self._request("PUT", f"/agents/{agent_id}", json=payload_dict)
930
+ response = AgentResponse(**api_response)
931
+ return Agent.from_response(response, client=self)
775
932
 
776
933
  def update_agent(
777
934
  self,
@@ -818,6 +975,62 @@ class AgentClient(BaseClient):
818
975
  """Delete an agent."""
819
976
  self._request("DELETE", f"/agents/{agent_id}")
820
977
 
978
+ def upsert_agent(self, identifier: str | Agent, **kwargs) -> Agent:
979
+ """Create or update an agent by instance, ID, or name.
980
+
981
+ Args:
982
+ identifier: Agent instance, ID (UUID string), or name
983
+ **kwargs: Agent configuration (instruction, description, tools, etc.)
984
+
985
+ Returns:
986
+ The created or updated agent.
987
+
988
+ Example:
989
+ >>> # By name (creates if not exists)
990
+ >>> agent = client.agents.upsert_agent(
991
+ ... "hello_agent",
992
+ ... instruction="You are a helpful assistant.",
993
+ ... description="A friendly agent",
994
+ ... )
995
+ >>> # By instance
996
+ >>> agent = client.agents.upsert_agent(existing_agent, description="Updated")
997
+ >>> # By ID
998
+ >>> agent = client.agents.upsert_agent("uuid-here", description="Updated")
999
+ """
1000
+ # Handle Agent instance
1001
+ if isinstance(identifier, Agent):
1002
+ if identifier.id:
1003
+ logger.info("Updating agent by instance: %s", identifier.name)
1004
+ return self.update_agent(identifier.id, name=identifier.name, **kwargs)
1005
+ identifier = identifier.name
1006
+
1007
+ # Handle string (ID or name)
1008
+ if isinstance(identifier, str):
1009
+ # Check if it's a UUID
1010
+ if is_uuid(identifier):
1011
+ logger.info("Updating agent by ID: %s", identifier)
1012
+ return self.update_agent(identifier, **kwargs)
1013
+
1014
+ # It's a name - find or create
1015
+ return self._upsert_agent_by_name(identifier, **kwargs)
1016
+
1017
+ raise ValueError(f"Invalid identifier type: {type(identifier)}")
1018
+
1019
+ def _upsert_agent_by_name(self, name: str, **kwargs) -> Agent:
1020
+ """Find agent by name and update, or create if not found."""
1021
+ existing = self.find_agents(name)
1022
+
1023
+ if len(existing) == 1:
1024
+ logger.info("Updating existing agent: %s", name)
1025
+ return self.update_agent(existing[0].id, name=name, **kwargs)
1026
+
1027
+ if len(existing) > 1:
1028
+ raise ValueError(f"Multiple agents found with name '{name}'")
1029
+
1030
+ # Create new agent
1031
+ logger.info("Creating new agent: %s", name)
1032
+ return self.create_agent(name=name, **kwargs)
1033
+
821
1034
  def _prepare_sync_request_data(
822
1035
  self,
823
1036
  message: str,
@@ -882,9 +1095,32 @@ class AgentClient(BaseClient):
882
1095
  tty: bool = False,
883
1096
  *,
884
1097
  renderer: RichStreamRenderer | str | None = "auto",
1098
+ runtime_config: dict[str, Any] | None = None,
885
1099
  **kwargs,
886
1100
  ) -> str:
887
- """Run an agent with a message, streaming via a renderer."""
1101
+ """Run an agent with a message, streaming via a renderer.
1102
+
1103
+ Args:
1104
+ agent_id: The ID of the agent to run.
1105
+ message: The message to send to the agent.
1106
+ files: Optional list of files to include with the request.
1107
+ tty: Whether to enable TTY mode.
1108
+ renderer: Renderer for streaming output.
1109
+ runtime_config: Optional runtime configuration for tools, MCPs, and agents.
1110
+ Keys should be platform IDs. Example:
1111
+ {
1112
+ "tool_configs": {"tool-id": {"param": "value"}},
1113
+ "mcp_configs": {"mcp-id": {"setting": "on"}},
1114
+ "agent_config": {"planning": True},
1115
+ }
1116
+ **kwargs: Additional arguments to pass to the run API.
1117
+
1118
+ Returns:
1119
+ The agent's response as a string.
1120
+ """
1121
+ # Include runtime_config in kwargs only when caller hasn't already provided it
1122
+ if runtime_config is not None and "runtime_config" not in kwargs:
1123
+ kwargs["runtime_config"] = runtime_config
888
1124
  (
889
1125
  payload,
890
1126
  data_payload,
@@ -1028,7 +1264,8 @@ class AgentClient(BaseClient):
1028
1264
  message: str,
1029
1265
  files: list[str | BinaryIO] | None = None,
1030
1266
  *,
1031
- timeout: float | None = None,
1267
+ request_timeout: float | None = None,
1268
+ runtime_config: dict[str, Any] | None = None,
1032
1269
  **kwargs,
1033
1270
  ) -> AsyncGenerator[dict, None]:
1034
1271
  """Async run an agent with a message, yielding streaming JSON chunks.
@@ -1037,7 +1274,14 @@ class AgentClient(BaseClient):
1037
1274
  agent_id: ID of the agent to run
1038
1275
  message: Message to send to the agent
1039
1276
  files: Optional list of files to include
1040
- timeout: Request timeout in seconds
1277
+ request_timeout: Optional request timeout in seconds (defaults to client timeout)
1278
+ runtime_config: Optional runtime configuration for tools, MCPs, and agents.
1279
+ Keys should be platform IDs. Example:
1280
+ {
1281
+ "tool_configs": {"tool-id": {"param": "value"}},
1282
+ "mcp_configs": {"mcp-id": {"setting": "on"}},
1283
+ "agent_config": {"planning": True},
1284
+ }
1041
1285
  **kwargs: Additional arguments (chat_history, pii_mapping, etc.)
1042
1286
 
1043
1287
  Yields:
@@ -1048,18 +1292,25 @@ class AgentClient(BaseClient):
1048
1292
  httpx.TimeoutException: When general timeout occurs
1049
1293
  Exception: For other unexpected errors
1050
1294
  """
1295
+ # Include runtime_config in kwargs only when caller hasn't already provided it
1296
+ if runtime_config is not None and "runtime_config" not in kwargs:
1297
+ kwargs["runtime_config"] = runtime_config
1298
+ # Derive timeout values for request/control flow
1299
+ legacy_timeout = kwargs.get("timeout")
1300
+ http_timeout_override = request_timeout if request_timeout is not None else legacy_timeout
1301
+ http_timeout = http_timeout_override or self.timeout
1302
+
1051
1303
  # Prepare request data
1052
1304
  payload, data_payload, files_payload, headers = self._prepare_request_data(message, files, **kwargs)
1053
1305
 
1054
1306
  # Create async client configuration
1055
- async_client_config = self._create_async_client_config(timeout, headers)
1307
+ async_client_config = self._create_async_client_config(http_timeout_override, headers)
1056
1308
 
1057
1309
  # Get execution timeout for streaming control
1058
1310
  timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
1059
1311
  agent_name = kwargs.get("agent_name")
1060
1312
 
1061
- try:
1062
- # Create async client and stream response
1313
+ async def _chunk_stream() -> AsyncGenerator[dict, None]:
1063
1314
  async with httpx.AsyncClient(**async_client_config) as async_client:
1064
1315
  async for chunk in self._stream_agent_response(
1065
1316
  async_client,
@@ -1073,7 +1324,14 @@ class AgentClient(BaseClient):
1073
1324
  ):
1074
1325
  yield chunk
1075
1326
 
1076
- finally:
1077
- # Ensure cleanup - this is handled by the calling context
1078
- # but we keep this for safety in case of future changes
1079
- pass
1327
+ async with _async_timeout_guard(http_timeout):
1328
+ async for chunk in _chunk_stream():
1329
+ yield chunk
1330
+
1331
+ @property
1332
+ def runs(self) -> "AgentRunsClient":
1333
+ """Get the agent runs client."""
1334
+ if self._runs_client is None:
1335
+ shared_config = build_shared_config(self)
1336
+ self._runs_client = AgentRunsClient(**shared_config)
1337
+ return self._runs_client
glaip_sdk/client/base.py CHANGED
@@ -3,6 +3,7 @@
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
glaip_sdk/client/main.py CHANGED
@@ -3,15 +3,24 @@
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 typing import Any
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
- from glaip_sdk.models import MCP, Agent, Tool
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):
@@ -25,12 +34,7 @@ class Client(BaseClient):
25
34
  """
26
35
  super().__init__(**kwargs)
27
36
  # Share the single httpx.Client + config with sub-clients
28
- shared_config = {
29
- "parent_client": self,
30
- "api_url": self.api_url,
31
- "api_key": self.api_key,
32
- "timeout": self._timeout,
33
- }
37
+ shared_config = build_shared_config(self)
34
38
  self.agents = AgentClient(**shared_config)
35
39
  self.tools = ToolClient(**shared_config)
36
40
  self.mcps = MCPClient(**shared_config)
@@ -53,7 +57,7 @@ class Client(BaseClient):
53
57
  name: str | None = None,
54
58
  version: str | None = None,
55
59
  sync_langflow_agents: bool = False,
56
- ) -> list[Agent]:
60
+ ) -> AgentListResult:
57
61
  """List agents with optional filtering.
58
62
 
59
63
  Args:
@@ -64,7 +68,7 @@ class Client(BaseClient):
64
68
  sync_langflow_agents: Sync with LangFlow server before listing (only applies when agent_type=langflow)
65
69
 
66
70
  Returns:
67
- List of agents matching the filters
71
+ AgentListResult with agents and pagination metadata. Supports iteration and indexing.
68
72
  """
69
73
  return self.agents.list_agents(
70
74
  agent_type=agent_type,
@@ -217,6 +221,11 @@ class Client(BaseClient):
217
221
 
218
222
  @timeout.setter
219
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
+ """
220
229
  # Rebuild the root http client
221
230
  BaseClient.timeout.fset(self, value) # call parent setter
222
231
  # Propagate the new session to sub-clients so they don't hold a closed client