glaip-sdk 0.0.5b1__py3-none-any.whl → 0.0.7__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 (43) hide show
  1. glaip_sdk/__init__.py +1 -1
  2. glaip_sdk/_version.py +42 -19
  3. glaip_sdk/branding.py +3 -2
  4. glaip_sdk/cli/commands/__init__.py +1 -1
  5. glaip_sdk/cli/commands/agents.py +452 -285
  6. glaip_sdk/cli/commands/configure.py +14 -13
  7. glaip_sdk/cli/commands/mcps.py +30 -20
  8. glaip_sdk/cli/commands/models.py +5 -3
  9. glaip_sdk/cli/commands/tools.py +111 -106
  10. glaip_sdk/cli/display.py +48 -27
  11. glaip_sdk/cli/io.py +1 -1
  12. glaip_sdk/cli/main.py +26 -5
  13. glaip_sdk/cli/resolution.py +5 -4
  14. glaip_sdk/cli/utils.py +437 -188
  15. glaip_sdk/cli/validators.py +7 -2
  16. glaip_sdk/client/agents.py +276 -153
  17. glaip_sdk/client/base.py +69 -27
  18. glaip_sdk/client/tools.py +44 -26
  19. glaip_sdk/client/validators.py +154 -94
  20. glaip_sdk/config/constants.py +0 -2
  21. glaip_sdk/models.py +5 -4
  22. glaip_sdk/utils/__init__.py +7 -7
  23. glaip_sdk/utils/client_utils.py +191 -101
  24. glaip_sdk/utils/display.py +4 -2
  25. glaip_sdk/utils/general.py +8 -6
  26. glaip_sdk/utils/import_export.py +58 -25
  27. glaip_sdk/utils/rendering/formatting.py +12 -6
  28. glaip_sdk/utils/rendering/models.py +1 -1
  29. glaip_sdk/utils/rendering/renderer/base.py +523 -332
  30. glaip_sdk/utils/rendering/renderer/console.py +6 -5
  31. glaip_sdk/utils/rendering/renderer/debug.py +94 -52
  32. glaip_sdk/utils/rendering/renderer/stream.py +93 -48
  33. glaip_sdk/utils/rendering/steps.py +103 -39
  34. glaip_sdk/utils/rich_utils.py +1 -1
  35. glaip_sdk/utils/run_renderer.py +1 -1
  36. glaip_sdk/utils/serialization.py +9 -3
  37. glaip_sdk/utils/validation.py +2 -2
  38. glaip_sdk-0.0.7.dist-info/METADATA +183 -0
  39. glaip_sdk-0.0.7.dist-info/RECORD +55 -0
  40. glaip_sdk-0.0.5b1.dist-info/METADATA +0 -645
  41. glaip_sdk-0.0.5b1.dist-info/RECORD +0 -55
  42. {glaip_sdk-0.0.5b1.dist-info → glaip_sdk-0.0.7.dist-info}/WHEEL +0 -0
  43. {glaip_sdk-0.0.5b1.dist-info → glaip_sdk-0.0.7.dist-info}/entry_points.txt +0 -0
@@ -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
 
@@ -42,6 +42,9 @@ from glaip_sdk.utils.validation import validate_agent_instruction
42
42
  # API endpoints
43
43
  AGENTS_ENDPOINT = "/agents/"
44
44
 
45
+ # SSE content type
46
+ SSE_CONTENT_TYPE = "text/event-stream"
47
+
45
48
  # Set up module-level logger
46
49
  logger = logging.getLogger("glaip_sdk.agents")
47
50
 
@@ -49,7 +52,12 @@ logger = logging.getLogger("glaip_sdk.agents")
49
52
  class AgentClient(BaseClient):
50
53
  """Client for agent operations."""
51
54
 
52
- def __init__(self, *, parent_client: BaseClient | None = None, **kwargs):
55
+ def __init__(
56
+ self,
57
+ *,
58
+ parent_client: BaseClient | None = None,
59
+ **kwargs: Any,
60
+ ) -> None:
53
61
  """Initialize the agent client.
54
62
 
55
63
  Args:
@@ -161,7 +169,7 @@ class AgentClient(BaseClient):
161
169
  tools: list[str | Any] | None = None,
162
170
  agents: list[str | Any] | None = None,
163
171
  timeout: int = DEFAULT_AGENT_RUN_TIMEOUT,
164
- **kwargs,
172
+ **kwargs: Any,
165
173
  ) -> dict[str, Any]:
166
174
  """Build payload for agent creation with proper LM selection and metadata handling.
167
175
 
@@ -365,7 +373,10 @@ class AgentClient(BaseClient):
365
373
  agent_config.pop(key, None)
366
374
 
367
375
  def _finalize_update_payload(
368
- self, update_data: dict[str, Any], current_agent: "Agent", **kwargs
376
+ self,
377
+ update_data: dict[str, Any],
378
+ current_agent: "Agent",
379
+ **kwargs: Any,
369
380
  ) -> dict[str, Any]:
370
381
  """Finalize the update payload with metadata and additional kwargs."""
371
382
  # Handle metadata preservation
@@ -386,7 +397,7 @@ class AgentClient(BaseClient):
386
397
  name: str | None = None,
387
398
  instruction: str | None = None,
388
399
  model: str | None = None,
389
- **kwargs,
400
+ **kwargs: Any,
390
401
  ) -> dict[str, Any]:
391
402
  """Build payload for agent update with proper LM selection and current state preservation.
392
403
 
@@ -434,7 +445,7 @@ class AgentClient(BaseClient):
434
445
  tools: list[str | Any] | None = None,
435
446
  agents: list[str | Any] | None = None,
436
447
  timeout: int = DEFAULT_AGENT_RUN_TIMEOUT,
437
- **kwargs,
448
+ **kwargs: Any,
438
449
  ) -> "Agent":
439
450
  """Create a new agent."""
440
451
  # Client-side validation
@@ -470,7 +481,7 @@ class AgentClient(BaseClient):
470
481
  name: str | None = None,
471
482
  instruction: str | None = None,
472
483
  model: str | None = None,
473
- **kwargs,
484
+ **kwargs: Any,
474
485
  ) -> "Agent":
475
486
  """Update an existing agent."""
476
487
  # First, get the current agent data
@@ -493,39 +504,67 @@ class AgentClient(BaseClient):
493
504
  """Delete an agent."""
494
505
  self._request("DELETE", f"/agents/{agent_id}")
495
506
 
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
507
+ def _prepare_sync_request_data(
508
+ self,
509
+ message: str,
510
+ files: list[str | BinaryIO] | None,
511
+ tty: bool,
512
+ **kwargs: Any,
513
+ ) -> tuple[dict | None, dict | None, list | None, dict, Any | None]:
514
+ """Prepare request data for synchronous agent runs with renderer support.
515
+
516
+ Args:
517
+ message: Message to send
518
+ files: Optional files to include
519
+ tty: Whether to enable TTY mode
520
+ **kwargs: Additional request parameters
521
+
522
+ Returns:
523
+ Tuple of (payload, data_payload, files_payload, headers, multipart_data)
524
+ """
525
+ headers = {"Accept": SSE_CONTENT_TYPE}
502
526
 
503
527
  if files:
528
+ # Handle multipart data for file uploads
504
529
  multipart_data = prepare_multipart_data(message, files)
505
- # Inject optional multipart extras expected by backend
506
530
  if "chat_history" in kwargs and kwargs["chat_history"] is not None:
507
531
  multipart_data.data["chat_history"] = kwargs["chat_history"]
508
532
  if "pii_mapping" in kwargs and kwargs["pii_mapping"] is not None:
509
533
  multipart_data.data["pii_mapping"] = kwargs["pii_mapping"]
510
534
 
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
535
+ return (
536
+ None,
537
+ multipart_data.data,
538
+ multipart_data.files,
539
+ headers,
540
+ multipart_data,
541
+ )
518
542
  else:
519
- payload = {"input": message, **kwargs}
543
+ # Simple JSON payload for text-only requests
544
+ payload = {"input": message, "stream": True, **kwargs}
520
545
  if tty:
521
546
  payload["tty"] = True
522
- payload["stream"] = True
523
- data_payload = None
524
- files_payload = None
547
+ return payload, None, None, headers, None
548
+
549
+ def _get_timeout_values(
550
+ self, timeout: float | None, **kwargs: Any
551
+ ) -> tuple[float, float]:
552
+ """Get request timeout and execution timeout values.
553
+
554
+ Args:
555
+ timeout: Request timeout (overrides instance timeout)
556
+ **kwargs: Additional parameters including execution timeout
525
557
 
526
- return payload, data_payload, files_payload, headers, multipart_data
558
+ Returns:
559
+ Tuple of (request_timeout, execution_timeout)
560
+ """
561
+ request_timeout = timeout or self.timeout
562
+ execution_timeout = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
563
+ return request_timeout, execution_timeout
527
564
 
528
- def _create_renderer(self, renderer, **kwargs):
565
+ def _create_renderer(
566
+ self, renderer: RichStreamRenderer | None, **kwargs: Any
567
+ ) -> RichStreamRenderer:
529
568
  """Create appropriate renderer based on configuration."""
530
569
  if isinstance(renderer, RichStreamRenderer):
531
570
  return renderer
@@ -544,7 +583,7 @@ class AgentClient(BaseClient):
544
583
  else:
545
584
  return self._create_default_renderer(verbose)
546
585
 
547
- def _create_silent_renderer(self):
586
+ def _create_silent_renderer(self) -> RichStreamRenderer:
548
587
  """Create a silent renderer that suppresses all output."""
549
588
  silent_config = RendererConfig(
550
589
  live=False,
@@ -558,7 +597,7 @@ class AgentClient(BaseClient):
558
597
  verbose=False,
559
598
  )
560
599
 
561
- def _create_minimal_renderer(self):
600
+ def _create_minimal_renderer(self) -> RichStreamRenderer:
562
601
  """Create a minimal renderer with basic output."""
563
602
  minimal_config = RendererConfig(
564
603
  live=False,
@@ -572,7 +611,7 @@ class AgentClient(BaseClient):
572
611
  verbose=False,
573
612
  )
574
613
 
575
- def _create_verbose_renderer(self):
614
+ def _create_verbose_renderer(self) -> RichStreamRenderer:
576
615
  """Create a verbose renderer for detailed output."""
577
616
  verbose_config = RendererConfig(
578
617
  theme="dark",
@@ -587,7 +626,7 @@ class AgentClient(BaseClient):
587
626
  verbose=True,
588
627
  )
589
628
 
590
- def _create_default_renderer(self, verbose: bool):
629
+ def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
591
630
  """Create the default renderer."""
592
631
  if verbose:
593
632
  return self._create_verbose_renderer()
@@ -595,22 +634,22 @@ class AgentClient(BaseClient):
595
634
  default_config = RendererConfig(show_delegate_tool_panels=True)
596
635
  return RichStreamRenderer(console=_Console(), cfg=default_config)
597
636
 
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 = {
637
+ def _initialize_stream_metadata(self, kwargs: dict[str, Any]) -> dict[str, Any]:
638
+ """Initialize stream metadata."""
639
+ return {
607
640
  "agent_name": kwargs.get("agent_name", ""),
608
641
  "model": kwargs.get("model"),
609
642
  "run_id": None,
610
643
  "input_message": "", # Will be set from kwargs if available
611
644
  }
612
645
 
613
- # Capture request id if provided
646
+ def _capture_request_id(
647
+ self,
648
+ stream_response: httpx.Response,
649
+ meta: dict[str, Any],
650
+ renderer: RichStreamRenderer,
651
+ ) -> None:
652
+ """Capture request ID from response headers."""
614
653
  req_id = stream_response.headers.get(
615
654
  "x-request-id"
616
655
  ) or stream_response.headers.get("x-run-id")
@@ -618,50 +657,98 @@ class AgentClient(BaseClient):
618
657
  meta["run_id"] = req_id
619
658
  renderer.on_start(meta)
620
659
 
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
660
+ def _should_start_timer(self, ev: dict[str, Any]) -> bool:
661
+ """Check if timer should be started for this event."""
662
+ return "content" in ev or "status" in ev or ev.get("metadata")
663
+
664
+ def _handle_content_event(self, ev: dict[str, Any], final_text: str) -> str:
665
+ """Handle content events."""
666
+ content = ev.get("content", "")
667
+ if not content.startswith("Artifact received:"):
668
+ return content
669
+ return final_text
670
+
671
+ def _handle_usage_event(
672
+ self, ev: dict[str, Any], stats_usage: dict[str, Any]
673
+ ) -> None:
674
+ """Handle usage events."""
675
+ stats_usage.update(ev.get("usage") or {})
676
+
677
+ def _handle_run_info_event(
678
+ self, ev: dict[str, Any], meta: dict[str, Any], renderer: RichStreamRenderer
679
+ ) -> None:
680
+ """Handle run info events."""
681
+ if ev.get("model"):
682
+ meta["model"] = ev["model"]
683
+ renderer.on_start(meta)
684
+ if ev.get("run_id"):
685
+ meta["run_id"] = ev["run_id"]
686
+ renderer.on_start(meta)
687
+
688
+ def _process_single_event(
689
+ self,
690
+ event: dict[str, Any],
691
+ renderer: RichStreamRenderer,
692
+ final_text: str,
693
+ stats_usage: dict[str, Any],
694
+ meta: dict[str, Any],
695
+ ) -> tuple[str, dict[str, Any]]:
696
+ """Process a single streaming event."""
697
+ try:
698
+ ev = json.loads(event["data"])
699
+ except json.JSONDecodeError:
700
+ logger.debug("Non-JSON SSE fragment skipped")
701
+ return final_text, stats_usage
702
+
703
+ kind = (ev.get("metadata") or {}).get("kind")
704
+ renderer.on_event(ev)
705
+
706
+ # Skip artifacts from content accumulation
707
+ if kind == "artifact":
708
+ return final_text, stats_usage
709
+
710
+ # Handle different event types
711
+ if kind == "final_response" and ev.get("content"):
712
+ final_text = ev.get("content", "")
713
+ elif ev.get("content"):
714
+ final_text = self._handle_content_event(ev, final_text)
715
+ elif kind == "usage":
716
+ self._handle_usage_event(ev, stats_usage)
717
+ elif kind == "run_info":
718
+ self._handle_run_info_event(ev, meta, renderer)
719
+
720
+ return final_text, stats_usage
627
721
 
722
+ def _process_stream_events(
723
+ self,
724
+ stream_response: httpx.Response,
725
+ renderer: RichStreamRenderer,
726
+ timeout_seconds: float,
727
+ agent_name: str | None,
728
+ kwargs: dict[str, Any],
729
+ ) -> tuple[str, dict[str, Any], float | None, float | None]:
730
+ """Process streaming events and accumulate response."""
731
+ final_text = ""
732
+ stats_usage = {}
733
+ started_monotonic = None
734
+ finished_monotonic = None
735
+
736
+ meta = self._initialize_stream_metadata(kwargs)
737
+ self._capture_request_id(stream_response, meta, renderer)
738
+
739
+ for event in iter_sse_events(stream_response, timeout_seconds, agent_name):
628
740
  # 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)
741
+ if started_monotonic is None:
742
+ try:
743
+ ev = json.loads(event["data"])
744
+ if self._should_start_timer(ev):
745
+ started_monotonic = monotonic()
746
+ except json.JSONDecodeError:
747
+ pass
748
+
749
+ final_text, stats_usage = self._process_single_event(
750
+ event, renderer, final_text, stats_usage, meta
751
+ )
665
752
 
666
753
  finished_monotonic = monotonic()
667
754
  return final_text, stats_usage, started_monotonic, finished_monotonic
@@ -678,9 +765,13 @@ class AgentClient(BaseClient):
678
765
  ) -> str:
679
766
  """Run an agent with a message, streaming via a renderer."""
680
767
  # 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
- )
768
+ (
769
+ payload,
770
+ data_payload,
771
+ files_payload,
772
+ headers,
773
+ multipart_data,
774
+ ) = self._prepare_sync_request_data(message, files, tty, **kwargs)
684
775
 
685
776
  # Create renderer
686
777
  r = self._create_renderer(renderer, **kwargs)
@@ -712,10 +803,13 @@ class AgentClient(BaseClient):
712
803
  timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
713
804
  agent_name = kwargs.get("agent_name")
714
805
 
715
- final_text, stats_usage, started_monotonic, finished_monotonic = (
716
- self._process_stream_events(
717
- stream_response, r, timeout_seconds, agent_name, kwargs
718
- )
806
+ (
807
+ final_text,
808
+ stats_usage,
809
+ started_monotonic,
810
+ finished_monotonic,
811
+ ) = self._process_stream_events(
812
+ stream_response, r, timeout_seconds, agent_name, kwargs
719
813
  )
720
814
 
721
815
  except KeyboardInterrupt:
@@ -749,6 +843,75 @@ class AgentClient(BaseClient):
749
843
  r.on_complete(st)
750
844
  return final_payload
751
845
 
846
+ def _prepare_request_data(
847
+ self,
848
+ message: str,
849
+ files: list[str | BinaryIO] | None,
850
+ **kwargs,
851
+ ) -> tuple[dict | None, dict | None, dict | None, dict | None]:
852
+ """Prepare request data for async agent runs.
853
+
854
+ Returns:
855
+ Tuple of (payload, data_payload, files_payload, headers)
856
+ """
857
+ if files:
858
+ # Handle multipart data for file uploads
859
+ multipart_data = prepare_multipart_data(message, files)
860
+ # Inject optional multipart extras expected by backend
861
+ if "chat_history" in kwargs and kwargs["chat_history"] is not None:
862
+ multipart_data.data["chat_history"] = kwargs["chat_history"]
863
+ if "pii_mapping" in kwargs and kwargs["pii_mapping"] is not None:
864
+ multipart_data.data["pii_mapping"] = kwargs["pii_mapping"]
865
+
866
+ headers = {"Accept": SSE_CONTENT_TYPE}
867
+ return None, multipart_data.data, multipart_data.files, headers
868
+ else:
869
+ # Simple JSON payload for text-only requests
870
+ payload = {"input": message, "stream": True, **kwargs}
871
+ headers = {"Accept": SSE_CONTENT_TYPE}
872
+ return payload, None, None, headers
873
+
874
+ def _create_async_client_config(
875
+ self, timeout: float | None, headers: dict | None
876
+ ) -> dict:
877
+ """Create async client configuration with proper headers and timeout."""
878
+ config = self._build_async_client(timeout or self.timeout)
879
+ if headers:
880
+ config["headers"] = {**config["headers"], **headers}
881
+ return config
882
+
883
+ async def _stream_agent_response(
884
+ self,
885
+ async_client: httpx.AsyncClient,
886
+ agent_id: str,
887
+ payload: dict | None,
888
+ data_payload: dict | None,
889
+ files_payload: dict | None,
890
+ headers: dict | None,
891
+ timeout_seconds: float,
892
+ agent_name: str | None,
893
+ ) -> AsyncGenerator[dict, None]:
894
+ """Stream the agent response and yield parsed JSON chunks."""
895
+ async with async_client.stream(
896
+ "POST",
897
+ f"/agents/{agent_id}/run",
898
+ json=payload,
899
+ data=data_payload,
900
+ files=files_payload,
901
+ headers=headers,
902
+ ) as stream_response:
903
+ stream_response.raise_for_status()
904
+
905
+ async for event in aiter_sse_events(
906
+ stream_response, timeout_seconds, agent_name
907
+ ):
908
+ try:
909
+ chunk = json.loads(event["data"])
910
+ yield chunk
911
+ except json.JSONDecodeError:
912
+ logger.debug("Non-JSON SSE fragment skipped")
913
+ continue
914
+
752
915
  async def arun_agent(
753
916
  self,
754
917
  agent_id: str,
@@ -775,74 +938,34 @@ class AgentClient(BaseClient):
775
938
  httpx.TimeoutException: When general timeout occurs
776
939
  Exception: For other unexpected errors
777
940
  """
778
- # Prepare multipart data if files are provided
779
- multipart_data = None
780
- headers = None # None means "don't override client defaults"
781
-
782
- if files:
783
- multipart_data = prepare_multipart_data(message, files)
784
- # Inject optional multipart extras expected by backend
785
- if "chat_history" in kwargs and kwargs["chat_history"] is not None:
786
- multipart_data.data["chat_history"] = kwargs["chat_history"]
787
- if "pii_mapping" in kwargs and kwargs["pii_mapping"] is not None:
788
- multipart_data.data["pii_mapping"] = kwargs["pii_mapping"]
789
- headers = None # Let httpx set proper multipart boundaries
790
-
791
- # When streaming, explicitly prefer SSE
792
- headers = {**(headers or {}), "Accept": "text/event-stream"}
941
+ # Prepare request data
942
+ payload, data_payload, files_payload, headers = self._prepare_request_data(
943
+ message, files, **kwargs
944
+ )
793
945
 
794
- if files:
795
- payload = None
796
- # Use multipart data
797
- data_payload = multipart_data.data
798
- files_payload = multipart_data.files
799
- else:
800
- payload = {"input": message, **kwargs}
801
- # Explicitly send stream intent both ways
802
- payload["stream"] = True
803
- data_payload = None
804
- files_payload = None
946
+ # Create async client configuration
947
+ async_client_config = self._create_async_client_config(timeout, headers)
805
948
 
806
- # Use timeout from parameter or instance default
807
- request_timeout = timeout or self.timeout
949
+ # Get execution timeout for streaming control
950
+ timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
951
+ agent_name = kwargs.get("agent_name")
808
952
 
809
953
  try:
810
- async_client_config = self._build_async_client(request_timeout)
811
- if headers:
812
- async_client_config["headers"] = {
813
- **async_client_config["headers"],
814
- **headers,
815
- }
816
-
817
- # Create async client for this request
954
+ # Create async client and stream response
818
955
  async with httpx.AsyncClient(**async_client_config) as async_client:
819
- async with async_client.stream(
820
- "POST",
821
- f"/agents/{agent_id}/run",
822
- json=payload,
823
- data=data_payload,
824
- files=files_payload,
825
- headers=headers,
826
- ) as stream_response:
827
- stream_response.raise_for_status()
828
-
829
- # Get agent run timeout for execution control
830
- # Prefer parameter timeout, otherwise use default
831
- timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
832
-
833
- agent_name = kwargs.get("agent_name")
834
-
835
- async for event in aiter_sse_events(
836
- stream_response, timeout_seconds, agent_name
837
- ):
838
- try:
839
- chunk = json.loads(event["data"])
840
- yield chunk
841
- except json.JSONDecodeError:
842
- logger.debug("Non-JSON SSE fragment skipped")
843
- continue
956
+ async for chunk in self._stream_agent_response(
957
+ async_client,
958
+ agent_id,
959
+ payload,
960
+ data_payload,
961
+ files_payload,
962
+ headers,
963
+ timeout_seconds,
964
+ agent_name,
965
+ ):
966
+ yield chunk
844
967
 
845
968
  finally:
846
- # Ensure we close any opened file handles from multipart
847
- if multipart_data:
848
- multipart_data.close()
969
+ # Ensure cleanup - this is handled by the calling context
970
+ # but we keep this for safety in case of future changes
971
+ pass