glaip-sdk 0.2.1__py3-none-any.whl → 0.3.0__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 (50) hide show
  1. glaip_sdk/_version.py +8 -0
  2. glaip_sdk/branding.py +13 -0
  3. glaip_sdk/cli/commands/agents.py +180 -39
  4. glaip_sdk/cli/commands/mcps.py +44 -18
  5. glaip_sdk/cli/commands/models.py +11 -5
  6. glaip_sdk/cli/commands/tools.py +35 -16
  7. glaip_sdk/cli/commands/transcripts.py +8 -0
  8. glaip_sdk/cli/constants.py +38 -0
  9. glaip_sdk/cli/context.py +8 -0
  10. glaip_sdk/cli/display.py +34 -19
  11. glaip_sdk/cli/main.py +14 -7
  12. glaip_sdk/cli/masking.py +8 -33
  13. glaip_sdk/cli/pager.py +9 -10
  14. glaip_sdk/cli/slash/agent_session.py +57 -20
  15. glaip_sdk/cli/slash/prompt.py +8 -0
  16. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  17. glaip_sdk/cli/slash/session.py +341 -46
  18. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  19. glaip_sdk/cli/slash/tui/remote_runs_app.py +632 -0
  20. glaip_sdk/cli/transcript/viewer.py +232 -32
  21. glaip_sdk/cli/update_notifier.py +2 -2
  22. glaip_sdk/cli/utils.py +266 -35
  23. glaip_sdk/cli/validators.py +5 -6
  24. glaip_sdk/client/__init__.py +2 -1
  25. glaip_sdk/client/_agent_payloads.py +30 -0
  26. glaip_sdk/client/agent_runs.py +147 -0
  27. glaip_sdk/client/agents.py +186 -22
  28. glaip_sdk/client/main.py +23 -6
  29. glaip_sdk/client/mcps.py +2 -4
  30. glaip_sdk/client/run_rendering.py +66 -0
  31. glaip_sdk/client/tools.py +2 -3
  32. glaip_sdk/config/constants.py +11 -0
  33. glaip_sdk/models/__init__.py +56 -0
  34. glaip_sdk/models/agent_runs.py +117 -0
  35. glaip_sdk/rich_components.py +58 -2
  36. glaip_sdk/utils/client_utils.py +13 -0
  37. glaip_sdk/utils/export.py +143 -0
  38. glaip_sdk/utils/import_export.py +6 -9
  39. glaip_sdk/utils/rendering/__init__.py +122 -1
  40. glaip_sdk/utils/rendering/renderer/base.py +3 -7
  41. glaip_sdk/utils/rendering/renderer/debug.py +0 -1
  42. glaip_sdk/utils/rendering/renderer/stream.py +4 -12
  43. glaip_sdk/utils/rendering/steps.py +1 -0
  44. glaip_sdk/utils/resource_refs.py +26 -15
  45. glaip_sdk/utils/serialization.py +16 -0
  46. {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/METADATA +24 -2
  47. glaip_sdk-0.3.0.dist-info/RECORD +94 -0
  48. glaip_sdk-0.2.1.dist-info/RECORD +0 -86
  49. {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/WHEEL +0 -0
  50. {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/entry_points.txt +0 -0
@@ -5,9 +5,11 @@ Authors:
5
5
  Raymond Christopher (raymond.christopher@gdplabs.id)
6
6
  """
7
7
 
8
+ import asyncio
8
9
  import json
9
10
  import logging
10
11
  from collections.abc import AsyncGenerator, Callable, Iterator, Mapping
12
+ from contextlib import asynccontextmanager
11
13
  from os import PathLike
12
14
  from pathlib import Path
13
15
  from typing import Any, BinaryIO
@@ -20,6 +22,7 @@ from glaip_sdk.client._agent_payloads import (
20
22
  AgentListResult,
21
23
  AgentUpdateRequest,
22
24
  )
25
+ from glaip_sdk.client.agent_runs import AgentRunsClient
23
26
  from glaip_sdk.client.base import BaseClient
24
27
  from glaip_sdk.client.mcps import MCPClient
25
28
  from glaip_sdk.client.run_rendering import (
@@ -28,6 +31,7 @@ from glaip_sdk.client.run_rendering import (
28
31
  )
29
32
  from glaip_sdk.client.tools import ToolClient
30
33
  from glaip_sdk.config.constants import (
34
+ AGENT_CONFIG_FIELDS,
31
35
  DEFAULT_AGENT_FRAMEWORK,
32
36
  DEFAULT_AGENT_RUN_TIMEOUT,
33
37
  DEFAULT_AGENT_TYPE,
@@ -67,6 +71,19 @@ _MERGED_SEQUENCE_FIELDS = ("tools", "agents", "mcps")
67
71
  _DEFAULT_METADATA_TYPE = "custom"
68
72
 
69
73
 
74
+ @asynccontextmanager
75
+ async def _async_timeout_guard(timeout_seconds: float | None) -> AsyncGenerator[None, None]:
76
+ """Apply an asyncio timeout when a custom timeout is provided."""
77
+ if timeout_seconds is None:
78
+ yield
79
+ return
80
+ try:
81
+ async with asyncio.timeout(timeout_seconds):
82
+ yield
83
+ except asyncio.TimeoutError as exc:
84
+ raise httpx.TimeoutException(f"Request timed out after {timeout_seconds}s") from exc
85
+
86
+
70
87
  def _normalise_sequence(value: Any) -> list[Any] | None:
71
88
  """Normalise optional sequence inputs to plain lists."""
72
89
  if value is None:
@@ -193,19 +210,7 @@ def _extract_original_refs(raw_definition: dict) -> dict[str, list]:
193
210
 
194
211
  def _build_cli_args(overrides_dict: dict) -> dict[str, Any]:
195
212
  """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
- }
213
+ cli_args = {key: overrides_dict.get(key) for key in AGENT_CONFIG_FIELDS if overrides_dict.get(key) is not None}
209
214
 
210
215
  # Normalize sequence fields
211
216
  for field in _MERGED_SEQUENCE_FIELDS:
@@ -254,6 +259,7 @@ class AgentClient(BaseClient):
254
259
  self._renderer_manager = AgentRunRenderingManager(logger)
255
260
  self._tool_client: ToolClient | None = None
256
261
  self._mcp_client: MCPClient | None = None
262
+ self._runs_client: "AgentRunsClient | None" = None
257
263
 
258
264
  def list_agents(
259
265
  self,
@@ -366,6 +372,11 @@ class AgentClient(BaseClient):
366
372
  # Renderer delegation helpers
367
373
  # ------------------------------------------------------------------ #
368
374
  def _get_renderer_manager(self) -> AgentRunRenderingManager:
375
+ """Get or create the renderer manager instance.
376
+
377
+ Returns:
378
+ AgentRunRenderingManager instance.
379
+ """
369
380
  manager = getattr(self, "_renderer_manager", None)
370
381
  if manager is None:
371
382
  manager = AgentRunRenderingManager(logger)
@@ -373,6 +384,15 @@ class AgentClient(BaseClient):
373
384
  return manager
374
385
 
375
386
  def _create_renderer(self, renderer: RichStreamRenderer | str | None, **kwargs: Any) -> RichStreamRenderer:
387
+ """Create or return a renderer instance.
388
+
389
+ Args:
390
+ renderer: Renderer instance, string identifier, or None.
391
+ **kwargs: Additional keyword arguments (e.g., verbose).
392
+
393
+ Returns:
394
+ RichStreamRenderer instance.
395
+ """
376
396
  manager = self._get_renderer_manager()
377
397
  verbose = kwargs.get("verbose", False)
378
398
  if isinstance(renderer, RichStreamRenderer) or hasattr(renderer, "on_start"):
@@ -387,6 +407,18 @@ class AgentClient(BaseClient):
387
407
  agent_name: str | None,
388
408
  meta: dict[str, Any],
389
409
  ) -> tuple[str, dict[str, Any], float | None, float | None]:
410
+ """Process stream events from an HTTP response.
411
+
412
+ Args:
413
+ stream_response: HTTP response stream.
414
+ renderer: Renderer to use for displaying events.
415
+ timeout_seconds: Timeout in seconds.
416
+ agent_name: Optional agent name.
417
+ meta: Metadata dictionary.
418
+
419
+ Returns:
420
+ Tuple of (final_text, stats_usage, started_monotonic, finished_monotonic).
421
+ """
390
422
  manager = self._get_renderer_manager()
391
423
  return manager.process_stream_events(
392
424
  stream_response,
@@ -404,6 +436,18 @@ class AgentClient(BaseClient):
404
436
  started_monotonic: float | None,
405
437
  finished_monotonic: float | None,
406
438
  ) -> str:
439
+ """Finalize the renderer and return the final text.
440
+
441
+ Args:
442
+ renderer: Renderer to finalize.
443
+ final_text: Final text content.
444
+ stats_usage: Usage statistics dictionary.
445
+ started_monotonic: Start time (monotonic).
446
+ finished_monotonic: Finish time (monotonic).
447
+
448
+ Returns:
449
+ Final text string.
450
+ """
407
451
  manager = self._get_renderer_manager()
408
452
  return manager.finalize_renderer(
409
453
  renderer,
@@ -414,11 +458,21 @@ class AgentClient(BaseClient):
414
458
  )
415
459
 
416
460
  def _get_tool_client(self) -> ToolClient:
461
+ """Get or create the tool client instance.
462
+
463
+ Returns:
464
+ ToolClient instance.
465
+ """
417
466
  if self._tool_client is None:
418
467
  self._tool_client = ToolClient(parent_client=self)
419
468
  return self._tool_client
420
469
 
421
470
  def _get_mcp_client(self) -> MCPClient:
471
+ """Get or create the MCP client instance.
472
+
473
+ Returns:
474
+ MCPClient instance.
475
+ """
422
476
  if self._mcp_client is None:
423
477
  self._mcp_client = MCPClient(parent_client=self)
424
478
  return self._mcp_client
@@ -428,6 +482,15 @@ class AgentClient(BaseClient):
428
482
  entry: Any,
429
483
  fallback_iter: Iterator[Any] | None,
430
484
  ) -> tuple[str | None, str | None]:
485
+ """Normalize a reference entry to extract ID and name.
486
+
487
+ Args:
488
+ entry: Reference entry (string, dict, or other).
489
+ fallback_iter: Optional iterator for fallback values.
490
+
491
+ Returns:
492
+ Tuple of (entry_id, entry_name).
493
+ """
431
494
  entry_id: str | None = None
432
495
  entry_name: str | None = None
433
496
 
@@ -464,6 +527,19 @@ class AgentClient(BaseClient):
464
527
  label: str,
465
528
  plural_label: str | None = None,
466
529
  ) -> list[str] | None:
530
+ """Resolve a list of resource references to IDs.
531
+
532
+ Args:
533
+ items: List of resource references to resolve.
534
+ references: Optional list of reference objects for fallback.
535
+ fetch_by_id: Function to fetch resource by ID.
536
+ find_by_name: Function to find resources by name.
537
+ label: Singular label for error messages.
538
+ plural_label: Plural label for error messages.
539
+
540
+ Returns:
541
+ List of resolved resource IDs, or None if items is empty.
542
+ """
467
543
  if not items:
468
544
  return None
469
545
 
@@ -495,6 +571,22 @@ class AgentClient(BaseClient):
495
571
  singular: str,
496
572
  plural: str,
497
573
  ) -> str:
574
+ """Resolve a single resource reference to an ID.
575
+
576
+ Args:
577
+ entry: Resource reference to resolve.
578
+ fallback_iter: Optional iterator for fallback values.
579
+ fetch_by_id: Function to fetch resource by ID.
580
+ find_by_name: Function to find resources by name.
581
+ singular: Singular label for error messages.
582
+ plural: Plural label for error messages.
583
+
584
+ Returns:
585
+ Resolved resource ID string.
586
+
587
+ Raises:
588
+ ValueError: If the resource cannot be resolved.
589
+ """
498
590
  entry_id, entry_name = self._normalise_reference_entry(entry, fallback_iter)
499
591
 
500
592
  validated_id = self._validate_resource_id(fetch_by_id, entry_id)
@@ -511,6 +603,14 @@ class AgentClient(BaseClient):
511
603
 
512
604
  @staticmethod
513
605
  def _coerce_reference_value(entry: Any) -> str:
606
+ """Coerce a reference entry to a string value.
607
+
608
+ Args:
609
+ entry: Reference entry (dict, string, or other).
610
+
611
+ Returns:
612
+ String representation of the reference.
613
+ """
514
614
  if isinstance(entry, dict):
515
615
  if entry.get("id"):
516
616
  return str(entry["id"])
@@ -520,6 +620,15 @@ class AgentClient(BaseClient):
520
620
 
521
621
  @staticmethod
522
622
  def _validate_resource_id(fetch_by_id: Callable[[str], Any], candidate_id: str | None) -> str | None:
623
+ """Validate a resource ID by attempting to fetch it.
624
+
625
+ Args:
626
+ fetch_by_id: Function to fetch resource by ID.
627
+ candidate_id: Candidate ID to validate.
628
+
629
+ Returns:
630
+ Validated ID if found, None otherwise.
631
+ """
523
632
  if not candidate_id:
524
633
  return None
525
634
  try:
@@ -535,6 +644,20 @@ class AgentClient(BaseClient):
535
644
  singular: str,
536
645
  plural: str,
537
646
  ) -> tuple[str, bool]:
647
+ """Resolve a resource by name to an ID.
648
+
649
+ Args:
650
+ find_by_name: Function to find resources by name.
651
+ entry_name: Name of the resource to find.
652
+ singular: Singular label for error messages.
653
+ plural: Plural label for error messages.
654
+
655
+ Returns:
656
+ Tuple of (resolved_id, success).
657
+
658
+ Raises:
659
+ ValueError: If resource not found or multiple matches exist.
660
+ """
538
661
  try:
539
662
  matches = find_by_name(entry_name)
540
663
  except Exception:
@@ -555,6 +678,15 @@ class AgentClient(BaseClient):
555
678
  tools: list[Any] | None,
556
679
  references: list[Any] | None = None,
557
680
  ) -> list[str] | None:
681
+ """Resolve tool references to IDs.
682
+
683
+ Args:
684
+ tools: List of tool references to resolve.
685
+ references: Optional list of reference objects for fallback.
686
+
687
+ Returns:
688
+ List of resolved tool IDs, or None if tools is empty.
689
+ """
558
690
  tool_client = self._get_tool_client()
559
691
  return self._resolve_resource_ids(
560
692
  tools,
@@ -570,6 +702,15 @@ class AgentClient(BaseClient):
570
702
  agents: list[Any] | None,
571
703
  references: list[Any] | None = None,
572
704
  ) -> list[str] | None:
705
+ """Resolve agent references to IDs.
706
+
707
+ Args:
708
+ agents: List of agent references to resolve.
709
+ references: Optional list of reference objects for fallback.
710
+
711
+ Returns:
712
+ List of resolved agent IDs, or None if agents is empty.
713
+ """
573
714
  return self._resolve_resource_ids(
574
715
  agents,
575
716
  references,
@@ -584,6 +725,15 @@ class AgentClient(BaseClient):
584
725
  mcps: list[Any] | None,
585
726
  references: list[Any] | None = None,
586
727
  ) -> list[str] | None:
728
+ """Resolve MCP references to IDs.
729
+
730
+ Args:
731
+ mcps: List of MCP references to resolve.
732
+ references: Optional list of reference objects for fallback.
733
+
734
+ Returns:
735
+ List of resolved MCP IDs, or None if mcps is empty.
736
+ """
587
737
  mcp_client = self._get_mcp_client()
588
738
  return self._resolve_resource_ids(
589
739
  mcps,
@@ -1028,7 +1178,7 @@ class AgentClient(BaseClient):
1028
1178
  message: str,
1029
1179
  files: list[str | BinaryIO] | None = None,
1030
1180
  *,
1031
- timeout: float | None = None,
1181
+ request_timeout: float | None = None,
1032
1182
  **kwargs,
1033
1183
  ) -> AsyncGenerator[dict, None]:
1034
1184
  """Async run an agent with a message, yielding streaming JSON chunks.
@@ -1037,7 +1187,7 @@ class AgentClient(BaseClient):
1037
1187
  agent_id: ID of the agent to run
1038
1188
  message: Message to send to the agent
1039
1189
  files: Optional list of files to include
1040
- timeout: Request timeout in seconds
1190
+ request_timeout: Optional request timeout in seconds (defaults to client timeout)
1041
1191
  **kwargs: Additional arguments (chat_history, pii_mapping, etc.)
1042
1192
 
1043
1193
  Yields:
@@ -1048,18 +1198,22 @@ class AgentClient(BaseClient):
1048
1198
  httpx.TimeoutException: When general timeout occurs
1049
1199
  Exception: For other unexpected errors
1050
1200
  """
1201
+ # Derive timeout values for request/control flow
1202
+ legacy_timeout = kwargs.get("timeout")
1203
+ http_timeout_override = request_timeout if request_timeout is not None else legacy_timeout
1204
+ http_timeout = http_timeout_override or self.timeout
1205
+
1051
1206
  # Prepare request data
1052
1207
  payload, data_payload, files_payload, headers = self._prepare_request_data(message, files, **kwargs)
1053
1208
 
1054
1209
  # Create async client configuration
1055
- async_client_config = self._create_async_client_config(timeout, headers)
1210
+ async_client_config = self._create_async_client_config(http_timeout_override, headers)
1056
1211
 
1057
1212
  # Get execution timeout for streaming control
1058
1213
  timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
1059
1214
  agent_name = kwargs.get("agent_name")
1060
1215
 
1061
- try:
1062
- # Create async client and stream response
1216
+ async def _chunk_stream() -> AsyncGenerator[dict, None]:
1063
1217
  async with httpx.AsyncClient(**async_client_config) as async_client:
1064
1218
  async for chunk in self._stream_agent_response(
1065
1219
  async_client,
@@ -1073,7 +1227,17 @@ class AgentClient(BaseClient):
1073
1227
  ):
1074
1228
  yield chunk
1075
1229
 
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
1230
+ async with _async_timeout_guard(http_timeout):
1231
+ async for chunk in _chunk_stream():
1232
+ yield chunk
1233
+
1234
+ @property
1235
+ def runs(self) -> "AgentRunsClient":
1236
+ """Get the agent runs client."""
1237
+ if self._runs_client is None:
1238
+ # Import here to avoid circular dependency with client.main
1239
+ from glaip_sdk.client.main import _build_shared_config
1240
+
1241
+ shared_config = _build_shared_config(self)
1242
+ self._runs_client = AgentRunsClient(**shared_config)
1243
+ return self._runs_client
glaip_sdk/client/main.py CHANGED
@@ -14,6 +14,23 @@ from glaip_sdk.client.tools import ToolClient
14
14
  from glaip_sdk.models import MCP, Agent, Tool
15
15
 
16
16
 
17
+ def _build_shared_config(client: BaseClient) -> dict[str, Any]:
18
+ """Build shared configuration dictionary for sub-clients.
19
+
20
+ Args:
21
+ client: Base client instance.
22
+
23
+ Returns:
24
+ Dictionary with shared configuration.
25
+ """
26
+ return {
27
+ "parent_client": client,
28
+ "api_url": client.api_url,
29
+ "api_key": client.api_key,
30
+ "timeout": client._timeout,
31
+ }
32
+
33
+
17
34
  class Client(BaseClient):
18
35
  """Main client that composes all specialized clients and shares one HTTP session."""
19
36
 
@@ -25,12 +42,7 @@ class Client(BaseClient):
25
42
  """
26
43
  super().__init__(**kwargs)
27
44
  # 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
- }
45
+ shared_config = _build_shared_config(self)
34
46
  self.agents = AgentClient(**shared_config)
35
47
  self.tools = ToolClient(**shared_config)
36
48
  self.mcps = MCPClient(**shared_config)
@@ -217,6 +229,11 @@ class Client(BaseClient):
217
229
 
218
230
  @timeout.setter
219
231
  def timeout(self, value: float) -> None: # type: ignore[override]
232
+ """Set the client timeout and propagate to sub-clients.
233
+
234
+ Args:
235
+ value: Timeout value in seconds.
236
+ """
220
237
  # Rebuild the root http client
221
238
  BaseClient.timeout.fset(self, value) # call parent setter
222
239
  # Propagate the new session to sub-clients so they don't hold a closed client
glaip_sdk/client/mcps.py CHANGED
@@ -14,7 +14,7 @@ from glaip_sdk.config.constants import (
14
14
  DEFAULT_MCP_TYPE,
15
15
  )
16
16
  from glaip_sdk.models import MCP
17
- from glaip_sdk.utils.client_utils import create_model_instances, find_by_name
17
+ from glaip_sdk.utils.client_utils import add_kwargs_to_payload, create_model_instances, find_by_name
18
18
 
19
19
  # API endpoints
20
20
  MCPS_ENDPOINT = "/mcps/"
@@ -147,9 +147,7 @@ class MCPClient(BaseClient):
147
147
 
148
148
  # Add any other kwargs (excluding already handled ones)
149
149
  excluded_keys = {"type"} # type is handled above
150
- for key, value in kwargs.items():
151
- if key not in excluded_keys:
152
- payload[key] = value
150
+ add_kwargs_to_payload(payload, kwargs, excluded_keys)
153
151
 
154
152
  return payload
155
153
 
@@ -124,6 +124,11 @@ class AgentRunRenderingManager:
124
124
  renderer.on_start(meta)
125
125
 
126
126
  def _create_silent_renderer(self) -> RichStreamRenderer:
127
+ """Create a silent renderer that outputs to a string buffer.
128
+
129
+ Returns:
130
+ RichStreamRenderer configured for silent output.
131
+ """
127
132
  silent_config = RendererConfig(
128
133
  live=False,
129
134
  persist_live=False,
@@ -136,6 +141,11 @@ class AgentRunRenderingManager:
136
141
  )
137
142
 
138
143
  def _create_minimal_renderer(self) -> RichStreamRenderer:
144
+ """Create a minimal renderer with reduced output.
145
+
146
+ Returns:
147
+ RichStreamRenderer configured for minimal output.
148
+ """
139
149
  minimal_config = RendererConfig(
140
150
  live=False,
141
151
  persist_live=False,
@@ -148,6 +158,11 @@ class AgentRunRenderingManager:
148
158
  )
149
159
 
150
160
  def _create_verbose_renderer(self) -> RichStreamRenderer:
161
+ """Create a verbose renderer with detailed output.
162
+
163
+ Returns:
164
+ RichStreamRenderer configured for verbose output.
165
+ """
151
166
  verbose_config = RendererConfig(
152
167
  live=False,
153
168
  append_finished_snapshots=False,
@@ -159,6 +174,14 @@ class AgentRunRenderingManager:
159
174
  )
160
175
 
161
176
  def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
177
+ """Create the default renderer based on verbosity.
178
+
179
+ Args:
180
+ verbose: Whether to create a verbose renderer.
181
+
182
+ Returns:
183
+ RichStreamRenderer instance.
184
+ """
162
185
  if verbose:
163
186
  return self._create_verbose_renderer()
164
187
  default_config = RendererConfig()
@@ -214,12 +237,27 @@ class AgentRunRenderingManager:
214
237
  meta: dict[str, Any],
215
238
  renderer: RichStreamRenderer,
216
239
  ) -> None:
240
+ """Capture request ID from response headers and update metadata.
241
+
242
+ Args:
243
+ stream_response: HTTP response stream.
244
+ meta: Metadata dictionary to update.
245
+ renderer: Renderer instance.
246
+ """
217
247
  req_id = stream_response.headers.get("x-request-id") or stream_response.headers.get("x-run-id")
218
248
  if req_id:
219
249
  meta["run_id"] = req_id
220
250
  renderer.on_start(meta)
221
251
 
222
252
  def _maybe_start_timer(self, event: dict[str, Any]) -> float | None:
253
+ """Start timing if this is a content-bearing event.
254
+
255
+ Args:
256
+ event: Event dictionary.
257
+
258
+ Returns:
259
+ Monotonic time if timer should start, None otherwise.
260
+ """
223
261
  try:
224
262
  ev = json.loads(event["data"])
225
263
  except json.JSONDecodeError:
@@ -237,6 +275,18 @@ class AgentRunRenderingManager:
237
275
  stats_usage: dict[str, Any],
238
276
  meta: dict[str, Any],
239
277
  ) -> tuple[str, dict[str, Any]]:
278
+ """Process a single streaming event.
279
+
280
+ Args:
281
+ event: Event dictionary.
282
+ renderer: Renderer instance.
283
+ final_text: Accumulated text so far.
284
+ stats_usage: Usage statistics dictionary.
285
+ meta: Metadata dictionary.
286
+
287
+ Returns:
288
+ Tuple of (updated_final_text, updated_stats_usage).
289
+ """
240
290
  try:
241
291
  ev = json.loads(event["data"])
242
292
  except json.JSONDecodeError:
@@ -292,6 +342,15 @@ class AgentRunRenderingManager:
292
342
  return None
293
343
 
294
344
  def _handle_content_event(self, ev: dict[str, Any], final_text: str) -> str:
345
+ """Handle a content event and update final text.
346
+
347
+ Args:
348
+ ev: Event dictionary.
349
+ final_text: Current accumulated text.
350
+
351
+ Returns:
352
+ Updated final text.
353
+ """
295
354
  content = ev.get("content", "")
296
355
  if not content.startswith("Artifact received:"):
297
356
  return content
@@ -303,6 +362,13 @@ class AgentRunRenderingManager:
303
362
  meta: dict[str, Any],
304
363
  renderer: RichStreamRenderer,
305
364
  ) -> None:
365
+ """Handle a run_info event and update metadata.
366
+
367
+ Args:
368
+ ev: Event dictionary.
369
+ meta: Metadata dictionary to update.
370
+ renderer: Renderer instance.
371
+ """
306
372
  if ev.get("model"):
307
373
  meta["model"] = ev["model"]
308
374
  renderer.on_start(meta)
glaip_sdk/client/tools.py CHANGED
@@ -18,6 +18,7 @@ from glaip_sdk.config.constants import (
18
18
  )
19
19
  from glaip_sdk.models import Tool
20
20
  from glaip_sdk.utils.client_utils import (
21
+ add_kwargs_to_payload,
21
22
  create_model_instances,
22
23
  find_by_name,
23
24
  )
@@ -200,9 +201,7 @@ class ToolClient(BaseClient):
200
201
 
201
202
  # Add any other kwargs (excluding already handled ones)
202
203
  excluded_keys = {"tags", "version"}
203
- for key, value in kwargs.items():
204
- if key not in excluded_keys:
205
- payload[key] = value
204
+ add_kwargs_to_payload(payload, kwargs, excluded_keys)
206
205
 
207
206
  return payload
208
207
 
@@ -39,3 +39,14 @@ DEFAULT_MCP_TRANSPORT = "stdio"
39
39
 
40
40
  # Default error messages
41
41
  DEFAULT_ERROR_MESSAGE = "Unknown error"
42
+
43
+ # Agent configuration fields used for CLI args and payload building
44
+ AGENT_CONFIG_FIELDS = (
45
+ "name",
46
+ "instruction",
47
+ "model",
48
+ "tools",
49
+ "agents",
50
+ "mcps",
51
+ "timeout",
52
+ )
@@ -0,0 +1,56 @@
1
+ """Models package for AIP SDK.
2
+
3
+ This package re-exports models from the legacy models.py file for backward compatibility.
4
+
5
+ Authors:
6
+ Raymond Christopher (raymond.christopher@gdplabs.id)
7
+ """
8
+
9
+ import importlib.util
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ # Export new agent runs models first (no dependencies on legacy models)
14
+ from glaip_sdk.models.agent_runs import ( # noqa: F401
15
+ RunOutputChunk,
16
+ RunSummary,
17
+ RunsPage,
18
+ RunWithOutput,
19
+ )
20
+
21
+ # Import from the parent models.py file
22
+ _parent_file = Path(__file__).parent.parent / "models.py"
23
+ if _parent_file.exists():
24
+ spec = importlib.util.spec_from_file_location("glaip_sdk.models_legacy", _parent_file)
25
+ models_legacy = importlib.util.module_from_spec(spec)
26
+ sys.modules["glaip_sdk.models_legacy"] = models_legacy
27
+ spec.loader.exec_module(models_legacy)
28
+
29
+ # Re-export all models
30
+ Agent = models_legacy.Agent
31
+ Tool = models_legacy.Tool
32
+ MCP = models_legacy.MCP
33
+ LanguageModelResponse = models_legacy.LanguageModelResponse
34
+ TTYRenderer = models_legacy.TTYRenderer
35
+ else:
36
+ # Fallback: try direct import (won't work if models/ exists)
37
+ # pragma: no cover - defensive fallback path, unlikely to execute
38
+ from glaip_sdk import models as models_legacy # type: ignore # pragma: no cover
39
+
40
+ Agent = models_legacy.Agent # pragma: no cover
41
+ Tool = models_legacy.Tool # pragma: no cover
42
+ MCP = models_legacy.MCP # pragma: no cover
43
+ LanguageModelResponse = models_legacy.LanguageModelResponse # pragma: no cover
44
+ TTYRenderer = models_legacy.TTYRenderer # pragma: no cover
45
+
46
+ __all__ = [
47
+ "Agent",
48
+ "Tool",
49
+ "MCP",
50
+ "LanguageModelResponse",
51
+ "TTYRenderer",
52
+ "RunSummary",
53
+ "RunsPage",
54
+ "RunWithOutput",
55
+ "RunOutputChunk",
56
+ ]