glaip-sdk 0.0.5__py3-none-any.whl → 0.0.6a0__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 (42) hide show
  1. glaip_sdk/__init__.py +1 -1
  2. glaip_sdk/branding.py +3 -2
  3. glaip_sdk/cli/commands/__init__.py +1 -1
  4. glaip_sdk/cli/commands/agents.py +444 -268
  5. glaip_sdk/cli/commands/configure.py +12 -11
  6. glaip_sdk/cli/commands/mcps.py +28 -16
  7. glaip_sdk/cli/commands/models.py +5 -3
  8. glaip_sdk/cli/commands/tools.py +109 -102
  9. glaip_sdk/cli/display.py +38 -16
  10. glaip_sdk/cli/io.py +1 -1
  11. glaip_sdk/cli/main.py +26 -5
  12. glaip_sdk/cli/resolution.py +5 -4
  13. glaip_sdk/cli/utils.py +376 -157
  14. glaip_sdk/cli/validators.py +7 -2
  15. glaip_sdk/client/agents.py +184 -89
  16. glaip_sdk/client/base.py +24 -13
  17. glaip_sdk/client/validators.py +154 -94
  18. glaip_sdk/config/constants.py +0 -2
  19. glaip_sdk/models.py +4 -4
  20. glaip_sdk/utils/__init__.py +7 -7
  21. glaip_sdk/utils/client_utils.py +144 -78
  22. glaip_sdk/utils/display.py +4 -2
  23. glaip_sdk/utils/general.py +8 -6
  24. glaip_sdk/utils/import_export.py +55 -24
  25. glaip_sdk/utils/rendering/formatting.py +12 -6
  26. glaip_sdk/utils/rendering/models.py +1 -1
  27. glaip_sdk/utils/rendering/renderer/base.py +412 -248
  28. glaip_sdk/utils/rendering/renderer/console.py +6 -5
  29. glaip_sdk/utils/rendering/renderer/debug.py +94 -52
  30. glaip_sdk/utils/rendering/renderer/stream.py +93 -48
  31. glaip_sdk/utils/rendering/steps.py +103 -39
  32. glaip_sdk/utils/rich_utils.py +1 -1
  33. glaip_sdk/utils/run_renderer.py +1 -1
  34. glaip_sdk/utils/serialization.py +3 -1
  35. glaip_sdk/utils/validation.py +2 -2
  36. glaip_sdk-0.0.6a0.dist-info/METADATA +183 -0
  37. glaip_sdk-0.0.6a0.dist-info/RECORD +55 -0
  38. {glaip_sdk-0.0.5.dist-info → glaip_sdk-0.0.6a0.dist-info}/WHEEL +1 -1
  39. glaip_sdk-0.0.6a0.dist-info/entry_points.txt +3 -0
  40. glaip_sdk-0.0.5.dist-info/METADATA +0 -645
  41. glaip_sdk-0.0.5.dist-info/RECORD +0 -55
  42. glaip_sdk-0.0.5.dist-info/entry_points.txt +0 -2
@@ -7,7 +7,9 @@ Authors:
7
7
  Raymond Christopher (raymond.christopher@gdplabs.id)
8
8
  """
9
9
 
10
+ from collections.abc import Callable
10
11
  from pathlib import Path
12
+ from typing import Any
11
13
 
12
14
  import click
13
15
 
@@ -189,7 +191,7 @@ def validate_api_key_cli(api_key: str) -> str:
189
191
  raise click.ClickException(str(e))
190
192
 
191
193
 
192
- def coerce_timeout_cli(value) -> int:
194
+ def coerce_timeout_cli(value: int | float | str) -> int:
193
195
  """Coerce timeout value to integer with CLI-friendly error handling.
194
196
 
195
197
  Args:
@@ -208,7 +210,10 @@ def coerce_timeout_cli(value) -> int:
208
210
 
209
211
 
210
212
  def validate_name_uniqueness_cli(
211
- _client, name: str, resource_type: str, finder_func
213
+ _client: Any,
214
+ name: str,
215
+ resource_type: str,
216
+ finder_func: Callable[..., list[Any]],
212
217
  ) -> None:
213
218
  """Validate that a resource name is unique.
214
219
 
@@ -49,7 +49,12 @@ logger = logging.getLogger("glaip_sdk.agents")
49
49
  class AgentClient(BaseClient):
50
50
  """Client for agent operations."""
51
51
 
52
- def __init__(self, *, parent_client: BaseClient | None = None, **kwargs):
52
+ def __init__(
53
+ self,
54
+ *,
55
+ parent_client: BaseClient | None = None,
56
+ **kwargs: Any,
57
+ ) -> None:
53
58
  """Initialize the agent client.
54
59
 
55
60
  Args:
@@ -161,7 +166,7 @@ class AgentClient(BaseClient):
161
166
  tools: list[str | Any] | None = None,
162
167
  agents: list[str | Any] | None = None,
163
168
  timeout: int = DEFAULT_AGENT_RUN_TIMEOUT,
164
- **kwargs,
169
+ **kwargs: Any,
165
170
  ) -> dict[str, Any]:
166
171
  """Build payload for agent creation with proper LM selection and metadata handling.
167
172
 
@@ -365,7 +370,10 @@ class AgentClient(BaseClient):
365
370
  agent_config.pop(key, None)
366
371
 
367
372
  def _finalize_update_payload(
368
- self, update_data: dict[str, Any], current_agent: "Agent", **kwargs
373
+ self,
374
+ update_data: dict[str, Any],
375
+ current_agent: "Agent",
376
+ **kwargs: Any,
369
377
  ) -> dict[str, Any]:
370
378
  """Finalize the update payload with metadata and additional kwargs."""
371
379
  # Handle metadata preservation
@@ -386,7 +394,7 @@ class AgentClient(BaseClient):
386
394
  name: str | None = None,
387
395
  instruction: str | None = None,
388
396
  model: str | None = None,
389
- **kwargs,
397
+ **kwargs: Any,
390
398
  ) -> dict[str, Any]:
391
399
  """Build payload for agent update with proper LM selection and current state preservation.
392
400
 
@@ -434,7 +442,7 @@ class AgentClient(BaseClient):
434
442
  tools: list[str | Any] | None = None,
435
443
  agents: list[str | Any] | None = None,
436
444
  timeout: int = DEFAULT_AGENT_RUN_TIMEOUT,
437
- **kwargs,
445
+ **kwargs: Any,
438
446
  ) -> "Agent":
439
447
  """Create a new agent."""
440
448
  # Client-side validation
@@ -470,7 +478,7 @@ class AgentClient(BaseClient):
470
478
  name: str | None = None,
471
479
  instruction: str | None = None,
472
480
  model: str | None = None,
473
- **kwargs,
481
+ **kwargs: Any,
474
482
  ) -> "Agent":
475
483
  """Update an existing agent."""
476
484
  # First, get the current agent data
@@ -493,39 +501,67 @@ class AgentClient(BaseClient):
493
501
  """Delete an agent."""
494
502
  self._request("DELETE", f"/agents/{agent_id}")
495
503
 
496
- def _prepare_payload_and_headers(
497
- self, message: str, files: list[str | BinaryIO] | None, tty: bool, **kwargs
498
- ):
499
- """Prepare payload and headers for agent run request."""
500
- multipart_data = None
501
- headers = None
504
+ def _prepare_sync_request_data(
505
+ self,
506
+ message: str,
507
+ files: list[str | BinaryIO] | None,
508
+ tty: bool,
509
+ **kwargs: Any,
510
+ ) -> tuple[dict | None, dict | None, list | None, dict, Any | None]:
511
+ """Prepare request data for synchronous agent runs with renderer support.
512
+
513
+ Args:
514
+ message: Message to send
515
+ files: Optional files to include
516
+ tty: Whether to enable TTY mode
517
+ **kwargs: Additional request parameters
518
+
519
+ Returns:
520
+ Tuple of (payload, data_payload, files_payload, headers, multipart_data)
521
+ """
522
+ headers = {"Accept": "text/event-stream"}
502
523
 
503
524
  if files:
525
+ # Handle multipart data for file uploads
504
526
  multipart_data = prepare_multipart_data(message, files)
505
- # Inject optional multipart extras expected by backend
506
527
  if "chat_history" in kwargs and kwargs["chat_history"] is not None:
507
528
  multipart_data.data["chat_history"] = kwargs["chat_history"]
508
529
  if "pii_mapping" in kwargs and kwargs["pii_mapping"] is not None:
509
530
  multipart_data.data["pii_mapping"] = kwargs["pii_mapping"]
510
531
 
511
- # When streaming, explicitly prefer SSE
512
- headers = {**(headers or {}), "Accept": "text/event-stream"}
513
-
514
- if files:
515
- payload = None
516
- data_payload = multipart_data.data
517
- files_payload = multipart_data.files
532
+ return (
533
+ None,
534
+ multipart_data.data,
535
+ multipart_data.files,
536
+ headers,
537
+ multipart_data,
538
+ )
518
539
  else:
519
- payload = {"input": message, **kwargs}
540
+ # Simple JSON payload for text-only requests
541
+ payload = {"input": message, "stream": True, **kwargs}
520
542
  if tty:
521
543
  payload["tty"] = True
522
- payload["stream"] = True
523
- data_payload = None
524
- files_payload = None
544
+ return payload, None, None, headers, None
545
+
546
+ def _get_timeout_values(
547
+ self, timeout: float | None, **kwargs: Any
548
+ ) -> tuple[float, float]:
549
+ """Get request timeout and execution timeout values.
525
550
 
526
- return payload, data_payload, files_payload, headers, multipart_data
551
+ Args:
552
+ timeout: Request timeout (overrides instance timeout)
553
+ **kwargs: Additional parameters including execution timeout
554
+
555
+ Returns:
556
+ Tuple of (request_timeout, execution_timeout)
557
+ """
558
+ request_timeout = timeout or self.timeout
559
+ execution_timeout = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
560
+ return request_timeout, execution_timeout
527
561
 
528
- def _create_renderer(self, renderer, **kwargs):
562
+ def _create_renderer(
563
+ self, renderer: RichStreamRenderer | None, **kwargs: Any
564
+ ) -> RichStreamRenderer:
529
565
  """Create appropriate renderer based on configuration."""
530
566
  if isinstance(renderer, RichStreamRenderer):
531
567
  return renderer
@@ -544,7 +580,7 @@ class AgentClient(BaseClient):
544
580
  else:
545
581
  return self._create_default_renderer(verbose)
546
582
 
547
- def _create_silent_renderer(self):
583
+ def _create_silent_renderer(self) -> RichStreamRenderer:
548
584
  """Create a silent renderer that suppresses all output."""
549
585
  silent_config = RendererConfig(
550
586
  live=False,
@@ -558,7 +594,7 @@ class AgentClient(BaseClient):
558
594
  verbose=False,
559
595
  )
560
596
 
561
- def _create_minimal_renderer(self):
597
+ def _create_minimal_renderer(self) -> RichStreamRenderer:
562
598
  """Create a minimal renderer with basic output."""
563
599
  minimal_config = RendererConfig(
564
600
  live=False,
@@ -572,7 +608,7 @@ class AgentClient(BaseClient):
572
608
  verbose=False,
573
609
  )
574
610
 
575
- def _create_verbose_renderer(self):
611
+ def _create_verbose_renderer(self) -> RichStreamRenderer:
576
612
  """Create a verbose renderer for detailed output."""
577
613
  verbose_config = RendererConfig(
578
614
  theme="dark",
@@ -587,7 +623,7 @@ class AgentClient(BaseClient):
587
623
  verbose=True,
588
624
  )
589
625
 
590
- def _create_default_renderer(self, verbose: bool):
626
+ def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
591
627
  """Create the default renderer."""
592
628
  if verbose:
593
629
  return self._create_verbose_renderer()
@@ -595,22 +631,22 @@ class AgentClient(BaseClient):
595
631
  default_config = RendererConfig(show_delegate_tool_panels=True)
596
632
  return RichStreamRenderer(console=_Console(), cfg=default_config)
597
633
 
598
- def _process_stream_events(
599
- self, stream_response, renderer, timeout_seconds, agent_name, kwargs
600
- ):
601
- """Process streaming events and accumulate response."""
602
- final_text = ""
603
- stats_usage = {}
604
- started_monotonic = None
605
- finished_monotonic = None
606
- meta = {
634
+ def _initialize_stream_metadata(self, kwargs: dict[str, Any]) -> dict[str, Any]:
635
+ """Initialize stream metadata."""
636
+ return {
607
637
  "agent_name": kwargs.get("agent_name", ""),
608
638
  "model": kwargs.get("model"),
609
639
  "run_id": None,
610
640
  "input_message": "", # Will be set from kwargs if available
611
641
  }
612
642
 
613
- # Capture request id if provided
643
+ def _capture_request_id(
644
+ self,
645
+ stream_response: httpx.Response,
646
+ meta: dict[str, Any],
647
+ renderer: RichStreamRenderer,
648
+ ) -> None:
649
+ """Capture request ID from response headers."""
614
650
  req_id = stream_response.headers.get(
615
651
  "x-request-id"
616
652
  ) or stream_response.headers.get("x-run-id")
@@ -618,50 +654,102 @@ class AgentClient(BaseClient):
618
654
  meta["run_id"] = req_id
619
655
  renderer.on_start(meta)
620
656
 
621
- for event in iter_sse_events(stream_response, timeout_seconds, agent_name):
622
- try:
623
- ev = json.loads(event["data"])
624
- except json.JSONDecodeError:
625
- logger.debug("Non-JSON SSE fragment skipped")
626
- continue
657
+ def _should_start_timer(self, ev: dict[str, Any]) -> bool:
658
+ """Check if timer should be started for this event."""
659
+ return "content" in ev or "status" in ev or ev.get("metadata")
660
+
661
+ def _handle_content_event(self, ev: dict[str, Any], final_text: str) -> str:
662
+ """Handle content events."""
663
+ content = ev.get("content", "")
664
+ if not content.startswith("Artifact received:"):
665
+ return content
666
+ return final_text
667
+
668
+ def _handle_final_response_event(self, ev: dict[str, Any]) -> str:
669
+ """Handle final response events."""
670
+ return ev.get("content", "")
671
+
672
+ def _handle_usage_event(
673
+ self, ev: dict[str, Any], stats_usage: dict[str, Any]
674
+ ) -> None:
675
+ """Handle usage events."""
676
+ stats_usage.update(ev.get("usage") or {})
677
+
678
+ def _handle_run_info_event(
679
+ self, ev: dict[str, Any], meta: dict[str, Any], renderer: RichStreamRenderer
680
+ ) -> None:
681
+ """Handle run info events."""
682
+ if ev.get("model"):
683
+ meta["model"] = ev["model"]
684
+ renderer.on_start(meta)
685
+ if ev.get("run_id"):
686
+ meta["run_id"] = ev["run_id"]
687
+ renderer.on_start(meta)
688
+
689
+ def _process_single_event(
690
+ self,
691
+ event: dict[str, Any],
692
+ renderer: RichStreamRenderer,
693
+ final_text: str,
694
+ stats_usage: dict[str, Any],
695
+ meta: dict[str, Any],
696
+ ) -> tuple[str, dict[str, Any]]:
697
+ """Process a single streaming event."""
698
+ try:
699
+ ev = json.loads(event["data"])
700
+ except json.JSONDecodeError:
701
+ logger.debug("Non-JSON SSE fragment skipped")
702
+ return final_text, stats_usage
703
+
704
+ kind = (ev.get("metadata") or {}).get("kind")
705
+ renderer.on_event(ev)
706
+
707
+ # Skip artifacts from content accumulation
708
+ if kind == "artifact":
709
+ return final_text, stats_usage
710
+
711
+ # Accumulate assistant content
712
+ if ev.get("content"):
713
+ final_text = self._handle_content_event(ev, final_text)
714
+ elif kind == "final_response" and ev.get("content"):
715
+ final_text = self._handle_final_response_event(ev)
716
+ elif kind == "usage":
717
+ self._handle_usage_event(ev, stats_usage)
718
+ elif kind == "run_info":
719
+ self._handle_run_info_event(ev, meta, renderer)
720
+
721
+ return final_text, stats_usage
722
+
723
+ def _process_stream_events(
724
+ self,
725
+ stream_response: httpx.Response,
726
+ renderer: RichStreamRenderer,
727
+ timeout_seconds: float,
728
+ agent_name: str | None,
729
+ kwargs: dict[str, Any],
730
+ ) -> tuple[str, dict[str, Any], float | None, float | None]:
731
+ """Process streaming events and accumulate response."""
732
+ final_text = ""
733
+ stats_usage = {}
734
+ started_monotonic = None
735
+ finished_monotonic = None
627
736
 
737
+ meta = self._initialize_stream_metadata(kwargs)
738
+ self._capture_request_id(stream_response, meta, renderer)
739
+
740
+ for event in iter_sse_events(stream_response, timeout_seconds, agent_name):
628
741
  # Start timer at first meaningful event
629
- if started_monotonic is None and (
630
- "content" in ev or "status" in ev or ev.get("metadata")
631
- ):
632
- started_monotonic = monotonic()
633
-
634
- kind = (ev.get("metadata") or {}).get("kind")
635
- renderer.on_event(ev)
636
-
637
- # Skip artifacts from content accumulation
638
- if kind == "artifact":
639
- continue
640
-
641
- # Accumulate assistant content
642
- if ev.get("content"):
643
- if not ev["content"].startswith("Artifact received:"):
644
- final_text = ev["content"]
645
- continue
646
-
647
- # Handle final response
648
- if kind == "final_response" and ev.get("content"):
649
- final_text = ev["content"]
650
- continue
651
-
652
- # Handle usage stats
653
- if kind == "usage":
654
- stats_usage.update(ev.get("usage") or {})
655
- continue
656
-
657
- # Handle run info updates
658
- if kind == "run_info":
659
- if ev.get("model"):
660
- meta["model"] = ev["model"]
661
- renderer.on_start(meta)
662
- if ev.get("run_id"):
663
- meta["run_id"] = ev["run_id"]
664
- renderer.on_start(meta)
742
+ if started_monotonic is None:
743
+ try:
744
+ ev = json.loads(event["data"])
745
+ if self._should_start_timer(ev):
746
+ started_monotonic = monotonic()
747
+ except json.JSONDecodeError:
748
+ pass
749
+
750
+ final_text, stats_usage = self._process_single_event(
751
+ event, renderer, final_text, stats_usage, meta
752
+ )
665
753
 
666
754
  finished_monotonic = monotonic()
667
755
  return final_text, stats_usage, started_monotonic, finished_monotonic
@@ -678,9 +766,13 @@ class AgentClient(BaseClient):
678
766
  ) -> str:
679
767
  """Run an agent with a message, streaming via a renderer."""
680
768
  # Prepare request payload and headers
681
- payload, data_payload, files_payload, headers, multipart_data = (
682
- self._prepare_payload_and_headers(message, files, tty, **kwargs)
683
- )
769
+ (
770
+ payload,
771
+ data_payload,
772
+ files_payload,
773
+ headers,
774
+ multipart_data,
775
+ ) = self._prepare_sync_request_data(message, files, tty, **kwargs)
684
776
 
685
777
  # Create renderer
686
778
  r = self._create_renderer(renderer, **kwargs)
@@ -712,10 +804,13 @@ class AgentClient(BaseClient):
712
804
  timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
713
805
  agent_name = kwargs.get("agent_name")
714
806
 
715
- final_text, stats_usage, started_monotonic, finished_monotonic = (
716
- self._process_stream_events(
717
- stream_response, r, timeout_seconds, agent_name, kwargs
718
- )
807
+ (
808
+ final_text,
809
+ stats_usage,
810
+ started_monotonic,
811
+ finished_monotonic,
812
+ ) = self._process_stream_events(
813
+ stream_response, r, timeout_seconds, agent_name, kwargs
719
814
  )
720
815
 
721
816
  except KeyboardInterrupt:
glaip_sdk/client/base.py CHANGED
@@ -7,13 +7,14 @@ Authors:
7
7
 
8
8
  import logging
9
9
  import os
10
- from typing import Any, Union
10
+ from typing import Any, NoReturn, Union
11
11
 
12
12
  import httpx
13
13
  from dotenv import load_dotenv
14
14
 
15
15
  import glaip_sdk
16
- from glaip_sdk.config.constants import SDK_NAME, SDK_VERSION
16
+ from glaip_sdk._version import __version__ as SDK_VERSION
17
+ from glaip_sdk.config.constants import SDK_NAME
17
18
  from glaip_sdk.exceptions import (
18
19
  AuthenticationError,
19
20
  ConflictError,
@@ -147,7 +148,7 @@ class BaseClient:
147
148
  return self._timeout
148
149
 
149
150
  @timeout.setter
150
- def timeout(self, value: float):
151
+ def timeout(self, value: float) -> None:
151
152
  """Set timeout and rebuild client."""
152
153
  self._timeout = value
153
154
  if (
@@ -165,10 +166,10 @@ class BaseClient:
165
166
  post_endpoint: str,
166
167
  get_endpoint_fmt: str,
167
168
  *,
168
- json=None,
169
- data=None,
170
- files=None,
171
- **kwargs,
169
+ json: Any | None = None,
170
+ data: Any | None = None,
171
+ files: Any | None = None,
172
+ **kwargs: Any,
172
173
  ) -> Any:
173
174
  """Helper for POST-then-GET pattern used in create methods.
174
175
 
@@ -210,7 +211,7 @@ class BaseClient:
210
211
  get_endpoint = get_endpoint_fmt.format(id=resource_id)
211
212
  return self._request("GET", get_endpoint)
212
213
 
213
- def _ensure_client_alive(self):
214
+ def _ensure_client_alive(self) -> None:
214
215
  """Ensure HTTP client is alive, recreate if needed."""
215
216
  if not hasattr(self, "http_client") or self.http_client is None:
216
217
  if not self._parent_client:
@@ -289,8 +290,13 @@ class BaseClient:
289
290
  self._raise_api_error(response.status_code, message, payload=parsed)
290
291
 
291
292
  def _raise_api_error(
292
- self, status: int, message: str, error_type: str | None = None, *, payload=None
293
- ):
293
+ self,
294
+ status: int,
295
+ message: str,
296
+ error_type: str | None = None,
297
+ *,
298
+ payload: Any | None = None,
299
+ ) -> NoReturn:
294
300
  """Raise appropriate exception with rich context."""
295
301
  request_id = None
296
302
  try:
@@ -324,7 +330,7 @@ class BaseClient:
324
330
  request_id=request_id,
325
331
  )
326
332
 
327
- def close(self):
333
+ def close(self) -> None:
328
334
  """Close the HTTP client."""
329
335
  if (
330
336
  hasattr(self, "http_client")
@@ -334,11 +340,16 @@ class BaseClient:
334
340
  ):
335
341
  self.http_client.close()
336
342
 
337
- def __enter__(self):
343
+ def __enter__(self) -> "BaseClient":
338
344
  """Context manager entry."""
339
345
  return self
340
346
 
341
- def __exit__(self, _exc_type, _exc_val, _exc_tb):
347
+ def __exit__(
348
+ self,
349
+ _exc_type: type[BaseException] | None,
350
+ _exc_val: BaseException | None,
351
+ _exc_tb: Any,
352
+ ) -> None:
342
353
  """Context manager exit."""
343
354
  # Only close if this is not session-scoped
344
355
  if not self._session_scoped: