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
@@ -25,6 +25,7 @@ except Exception: # pragma: no cover - optional dependency
25
25
  Choice = None # type: ignore[assignment]
26
26
 
27
27
  from glaip_sdk.cli.transcript.cache import suggest_filename
28
+ from glaip_sdk.cli.utils import prompt_export_choice_questionary, questionary_safe_ask
28
29
  from glaip_sdk.icons import ICON_AGENT, ICON_DELEGATE, ICON_TOOL_STEP
29
30
  from glaip_sdk.rich_components import AIPPanel
30
31
  from glaip_sdk.utils.rendering.formatting import (
@@ -85,6 +86,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
85
86
  # Rendering helpers
86
87
  # ------------------------------------------------------------------
87
88
  def _render(self) -> None:
89
+ """Render the transcript viewer interface."""
88
90
  try:
89
91
  if self.console.is_terminal:
90
92
  self.console.clear()
@@ -115,12 +117,22 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
115
117
  self._render_transcript_view(query)
116
118
 
117
119
  def _render_default_view(self, query: str | None) -> None:
120
+ """Render the default summary view.
121
+
122
+ Args:
123
+ query: Optional user query to display.
124
+ """
118
125
  if query:
119
126
  self._render_user_query(query)
120
127
  self._render_steps_summary()
121
128
  self._render_final_panel()
122
129
 
123
130
  def _render_transcript_view(self, query: str | None) -> None:
131
+ """Render the full transcript view with events.
132
+
133
+ Args:
134
+ query: Optional user query to display.
135
+ """
124
136
  if not self.ctx.events:
125
137
  self.console.print("[dim]No SSE events were captured for this run.[/dim]")
126
138
  return
@@ -148,6 +160,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
148
160
  self.console.print()
149
161
 
150
162
  def _render_final_panel(self) -> None:
163
+ """Render the final result panel."""
151
164
  content = self.ctx.final_output or self.ctx.default_output or "No response content captured."
152
165
  title = "Final Result"
153
166
  duration_text = self._extract_final_duration()
@@ -161,6 +174,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
161
174
  # Interaction loops
162
175
  # ------------------------------------------------------------------
163
176
  def _fallback_loop(self) -> None:
177
+ """Fallback interaction loop for non-interactive terminals."""
164
178
  while True:
165
179
  try:
166
180
  ch = click.getchar()
@@ -181,6 +195,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
181
195
  continue
182
196
 
183
197
  def _handle_command(self, raw: str) -> bool:
198
+ """Handle a command input.
199
+
200
+ Args:
201
+ raw: Raw command string.
202
+
203
+ Returns:
204
+ True to continue, False to exit.
205
+ """
184
206
  lowered = raw.lower()
185
207
  if lowered in {"exit", "quit", "q"}:
186
208
  return True
@@ -238,38 +260,10 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
238
260
 
239
261
  def _prompt_export_choice(self, default_path: Path, default_display: str) -> tuple[str, Any] | None:
240
262
  """Render interactive export menu with numeric shortcuts."""
241
- if not self.console.is_terminal or questionary is None or Choice is None:
242
- return None
243
-
244
- try:
245
- answer = questionary.select(
246
- "Export transcript",
247
- choices=[
248
- Choice(
249
- title=f"Save to default ({default_display})",
250
- value=("default", default_path),
251
- shortcut_key="1",
252
- ),
253
- Choice(
254
- title="Choose a different path",
255
- value=("custom", None),
256
- shortcut_key="2",
257
- ),
258
- Choice(
259
- title="Cancel",
260
- value=("cancel", None),
261
- shortcut_key="3",
262
- ),
263
- ],
264
- use_shortcuts=True,
265
- instruction="Press 1-3 (or arrows) then Enter.",
266
- ).ask()
267
- except Exception:
263
+ if not self.console.is_terminal:
268
264
  return None
269
265
 
270
- if answer is None:
271
- return ("cancel", None)
272
- return answer
266
+ return prompt_export_choice_questionary(default_path, default_display)
273
267
 
274
268
  def _prompt_custom_destination(self) -> Path | None:
275
269
  """Prompt for custom export path with filesystem completion."""
@@ -277,11 +271,12 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
277
271
  return None
278
272
 
279
273
  try:
280
- response = questionary.path(
274
+ question = questionary.path(
281
275
  "Destination path (Tab to autocomplete):",
282
276
  default="",
283
277
  only_directories=False,
284
- ).ask()
278
+ )
279
+ response = questionary_safe_ask(question)
285
280
  except Exception:
286
281
  return None
287
282
 
@@ -339,15 +334,26 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
339
334
  self.console.print(f"[red]Failed to export transcript: {exc}[/red]")
340
335
 
341
336
  def _print_command_hint(self) -> None:
337
+ """Print command hint for user interaction."""
342
338
  self.console.print("[dim]Ctrl+T to toggle transcript · type `e` to export · press Enter to exit[/dim]")
343
339
  self.console.print()
344
340
 
345
341
  def _get_user_query(self) -> str | None:
342
+ """Extract user query from metadata or manifest.
343
+
344
+ Returns:
345
+ User query string or None.
346
+ """
346
347
  meta = self.ctx.meta or {}
347
348
  manifest = self.ctx.manifest_entry or {}
348
349
  return meta.get("input_message") or meta.get("query") or meta.get("message") or manifest.get("input_message")
349
350
 
350
351
  def _render_user_query(self, query: str) -> None:
352
+ """Render user query in a panel.
353
+
354
+ Args:
355
+ query: User query string to render.
356
+ """
351
357
  panel = AIPPanel(
352
358
  Markdown(f"Query: {query}"),
353
359
  title="User Request",
@@ -357,6 +363,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
357
363
  self.console.print()
358
364
 
359
365
  def _render_steps_summary(self) -> None:
366
+ """Render steps summary panel."""
360
367
  stored_lines = self.ctx.meta.get("transcript_step_lines")
361
368
  if stored_lines:
362
369
  body = Text("\n".join(stored_lines), style="dim")
@@ -373,6 +380,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
373
380
 
374
381
  @staticmethod
375
382
  def _format_steps_summary(steps: list[dict[str, Any]]) -> str:
383
+ """Format steps summary as text.
384
+
385
+ Args:
386
+ steps: List of step dictionaries.
387
+
388
+ Returns:
389
+ Formatted text string.
390
+ """
376
391
  if not steps:
377
392
  return " No steps yet"
378
393
 
@@ -388,6 +403,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
388
403
 
389
404
  @staticmethod
390
405
  def _extract_event_time(event: dict[str, Any]) -> float | None:
406
+ """Extract timestamp from event metadata.
407
+
408
+ Args:
409
+ event: Event dictionary.
410
+
411
+ Returns:
412
+ Time value as float or None.
413
+ """
391
414
  metadata = event.get("metadata") or {}
392
415
  time_value = metadata.get("time")
393
416
  try:
@@ -399,6 +422,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
399
422
 
400
423
  @staticmethod
401
424
  def _parse_received_timestamp(event: dict[str, Any]) -> datetime | None:
425
+ """Parse received timestamp from event.
426
+
427
+ Args:
428
+ event: Event dictionary.
429
+
430
+ Returns:
431
+ Parsed datetime or None.
432
+ """
402
433
  value = event.get("received_at")
403
434
  if not value:
404
435
  return None
@@ -412,6 +443,11 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
412
443
  return None
413
444
 
414
445
  def _extract_final_duration(self) -> str | None:
446
+ """Extract final duration from events.
447
+
448
+ Returns:
449
+ Duration string or None.
450
+ """
415
451
  for event in self.ctx.events:
416
452
  metadata = event.get("metadata") or {}
417
453
  if metadata.get("kind") == "final_response":
@@ -424,6 +460,11 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
424
460
  return None
425
461
 
426
462
  def _build_step_summary(self) -> list[dict[str, Any]]:
463
+ """Build step summary from stored steps or events.
464
+
465
+ Returns:
466
+ List of step dictionaries.
467
+ """
427
468
  stored = self.ctx.meta.get("transcript_steps")
428
469
  if isinstance(stored, list) and stored:
429
470
  return [
@@ -496,6 +537,15 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
496
537
  is_last: bool,
497
538
  lines: list[str],
498
539
  ) -> None:
540
+ """Recursively render a tree branch of steps.
541
+
542
+ Args:
543
+ manager: StepManager instance.
544
+ step_id: ID of step to render.
545
+ ancestor_state: Tuple of ancestor branch states.
546
+ is_last: Whether this is the last child.
547
+ lines: List to append rendered lines to.
548
+ """
499
549
  step = manager.by_id.get(step_id)
500
550
  if not step:
501
551
  return
@@ -525,6 +575,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
525
575
  )
526
576
 
527
577
  def _should_hide_step(self, step: Any) -> bool:
578
+ """Check if a step should be hidden.
579
+
580
+ Args:
581
+ step: Step object.
582
+
583
+ Returns:
584
+ True if step should be hidden.
585
+ """
528
586
  if getattr(step, "parent_id", None) is None:
529
587
  return False
530
588
  name = getattr(step, "name", "") or ""
@@ -536,6 +594,13 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
536
594
  root_id: str,
537
595
  lines: list[str],
538
596
  ) -> None:
597
+ """Decorate root step presentation with friendly label.
598
+
599
+ Args:
600
+ manager: StepManager instance.
601
+ root_id: Root step ID.
602
+ lines: Lines list to modify.
603
+ """
539
604
  if not lines:
540
605
  return
541
606
 
@@ -554,6 +619,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
554
619
  lines.insert(1, f" {query}")
555
620
 
556
621
  def _coerce_step_event(self, event: dict[str, Any]) -> dict[str, Any] | None:
622
+ """Coerce event to step event format.
623
+
624
+ Args:
625
+ event: Event dictionary.
626
+
627
+ Returns:
628
+ Step event dictionary or None.
629
+ """
557
630
  metadata = event.get("metadata")
558
631
  if not isinstance(metadata, dict):
559
632
  return None
@@ -569,6 +642,15 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
569
642
  }
570
643
 
571
644
  def _format_tree_line(self, step: Any, branch_state: tuple[bool, ...]) -> str:
645
+ """Format a tree line for a step.
646
+
647
+ Args:
648
+ step: Step object.
649
+ branch_state: Branch state tuple.
650
+
651
+ Returns:
652
+ Formatted line string.
653
+ """
572
654
  prefix = build_connector_prefix(branch_state)
573
655
  raw_label = normalise_display_label(getattr(step, "display_label", None))
574
656
  title, summary = self._split_label(raw_label)
@@ -594,6 +676,15 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
594
676
  return line
595
677
 
596
678
  def _friendly_root_label(self, step: Any, fallback: str | None) -> str:
679
+ """Generate friendly label for root step.
680
+
681
+ Args:
682
+ step: Step object.
683
+ fallback: Fallback label string.
684
+
685
+ Returns:
686
+ Friendly label string.
687
+ """
597
688
  agent_name = self.ctx.manifest_entry.get("agent_name") or (self.ctx.meta or {}).get("agent_name")
598
689
  agent_id = self.ctx.manifest_entry.get("agent_id") or getattr(step, "name", "")
599
690
 
@@ -607,6 +698,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
607
698
 
608
699
  @staticmethod
609
700
  def _format_duration_badge(step: Any) -> str | None:
701
+ """Format duration badge for a step.
702
+
703
+ Args:
704
+ step: Step object.
705
+
706
+ Returns:
707
+ Duration badge string or None.
708
+ """
610
709
  duration_ms = getattr(step, "duration_ms", None)
611
710
  if duration_ms is None:
612
711
  return None
@@ -626,6 +725,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
626
725
 
627
726
  @staticmethod
628
727
  def _split_label(label: str) -> tuple[str, str | None]:
728
+ """Split label into title and summary.
729
+
730
+ Args:
731
+ label: Label string.
732
+
733
+ Returns:
734
+ Tuple of (title, summary).
735
+ """
629
736
  if " — " in label:
630
737
  title, summary = label.split(" — ", 1)
631
738
  return title.strip(), summary.strip()
@@ -633,6 +740,15 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
633
740
 
634
741
  @staticmethod
635
742
  def _truncate_summary(summary: str, limit: int = 48) -> str:
743
+ """Truncate summary to specified length.
744
+
745
+ Args:
746
+ summary: Summary string.
747
+ limit: Maximum length.
748
+
749
+ Returns:
750
+ Truncated summary string.
751
+ """
636
752
  summary = summary.strip()
637
753
  if len(summary) <= limit:
638
754
  return summary
@@ -640,6 +756,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
640
756
 
641
757
  @staticmethod
642
758
  def _looks_like_uuid(value: str) -> bool:
759
+ """Check if string looks like a UUID.
760
+
761
+ Args:
762
+ value: String to check.
763
+
764
+ Returns:
765
+ True if value looks like UUID.
766
+ """
643
767
  stripped = value.replace("-", "").replace(" ", "")
644
768
  if len(stripped) not in {32, 36}:
645
769
  return False
@@ -647,6 +771,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
647
771
 
648
772
  @staticmethod
649
773
  def _format_duration_from_ms(value: Any) -> str | None:
774
+ """Format duration from milliseconds.
775
+
776
+ Args:
777
+ value: Duration value in milliseconds.
778
+
779
+ Returns:
780
+ Formatted duration string or None.
781
+ """
650
782
  try:
651
783
  if value is None:
652
784
  return None
@@ -662,12 +794,29 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
662
794
 
663
795
  @staticmethod
664
796
  def _is_step_event(metadata: dict[str, Any]) -> bool:
797
+ """Check if metadata represents a step event.
798
+
799
+ Args:
800
+ metadata: Event metadata dictionary.
801
+
802
+ Returns:
803
+ True if metadata represents a step event.
804
+ """
665
805
  kind = metadata.get("kind")
666
806
  return kind in {"agent_step", "agent_thinking_step"}
667
807
 
668
808
  def _iter_step_candidates(
669
809
  self, event: dict[str, Any], metadata: dict[str, Any]
670
810
  ) -> Iterable[tuple[str, dict[str, Any]]]:
811
+ """Iterate step candidates from event.
812
+
813
+ Args:
814
+ event: Event dictionary.
815
+ metadata: Event metadata dictionary.
816
+
817
+ Yields:
818
+ Tuples of (step_name, step_info).
819
+ """
671
820
  tool_info = metadata.get("tool_info") or {}
672
821
 
673
822
  yielded = False
@@ -691,6 +840,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
691
840
  def _iter_tool_call_candidates(
692
841
  tool_info: dict[str, Any],
693
842
  ) -> Iterable[tuple[str, dict[str, Any]]]:
843
+ """Iterate tool call candidates from tool_info.
844
+
845
+ Args:
846
+ tool_info: Tool info dictionary.
847
+
848
+ Yields:
849
+ Tuples of (tool_name, tool_call_info).
850
+ """
694
851
  tool_calls = tool_info.get("tool_calls")
695
852
  if isinstance(tool_calls, list):
696
853
  for call in tool_calls:
@@ -702,6 +859,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
702
859
  def _extract_direct_tool(
703
860
  tool_info: dict[str, Any],
704
861
  ) -> tuple[str, dict[str, Any]] | None:
862
+ """Extract direct tool from tool_info.
863
+
864
+ Args:
865
+ tool_info: Tool info dictionary.
866
+
867
+ Returns:
868
+ Tuple of (tool_name, tool_info) or None.
869
+ """
705
870
  if isinstance(tool_info, dict):
706
871
  name = tool_info.get("name")
707
872
  if name:
@@ -710,6 +875,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
710
875
 
711
876
  @staticmethod
712
877
  def _extract_completed_name(event: dict[str, Any]) -> str | None:
878
+ """Extract completed tool name from event content.
879
+
880
+ Args:
881
+ event: Event dictionary.
882
+
883
+ Returns:
884
+ Tool name or None.
885
+ """
713
886
  content = event.get("content") or ""
714
887
  if isinstance(content, str) and content.startswith("Completed "):
715
888
  name = content.replace("Completed ", "").strip()
@@ -723,6 +896,16 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
723
896
  order: list[str],
724
897
  name: str,
725
898
  ) -> dict[str, Any]:
899
+ """Ensure step entry exists, creating if needed.
900
+
901
+ Args:
902
+ steps: Steps dictionary.
903
+ order: Order list.
904
+ name: Step name.
905
+
906
+ Returns:
907
+ Step dictionary.
908
+ """
726
909
  if name not in steps:
727
910
  steps[name] = {
728
911
  "name": name,
@@ -742,6 +925,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
742
925
  info: dict[str, Any],
743
926
  event: dict[str, Any],
744
927
  ) -> None:
928
+ """Apply update to step from event metadata.
929
+
930
+ Args:
931
+ step: Step dictionary to update.
932
+ metadata: Event metadata.
933
+ info: Step info dictionary.
934
+ event: Event dictionary.
935
+ """
745
936
  status = metadata.get("status")
746
937
  event_time = metadata.get("time")
747
938
 
@@ -760,6 +951,15 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
760
951
 
761
952
  @staticmethod
762
953
  def _is_step_finished(metadata: dict[str, Any], event: dict[str, Any]) -> bool:
954
+ """Check if step is finished.
955
+
956
+ Args:
957
+ metadata: Event metadata.
958
+ event: Event dictionary.
959
+
960
+ Returns:
961
+ True if step is finished.
962
+ """
763
963
  status = metadata.get("status")
764
964
  return status == "finished" or bool(event.get("final"))
765
965
 
@@ -8,7 +8,6 @@ from __future__ import annotations
8
8
 
9
9
  import importlib
10
10
  import logging
11
- import os
12
11
  from collections.abc import Callable, Iterable, Iterator
13
12
  from contextlib import contextmanager
14
13
  from typing import Any, Literal
@@ -26,6 +25,7 @@ from glaip_sdk.branding import (
26
25
  WARNING_STYLE,
27
26
  )
28
27
  from glaip_sdk.cli.commands.update import update_command
28
+ from glaip_sdk.cli.constants import UPDATE_CHECK_ENABLED
29
29
  from glaip_sdk.cli.utils import command_hint, format_command_hint
30
30
  from glaip_sdk.rich_components import AIPPanel
31
31
 
@@ -72,7 +72,7 @@ def _fetch_latest_version(package_name: str) -> str | None:
72
72
 
73
73
  def _should_check_for_updates() -> bool:
74
74
  """Return False when update checks are explicitly disabled."""
75
- return os.getenv("AIP_NO_UPDATE_CHECK") is None
75
+ return UPDATE_CHECK_ENABLED
76
76
 
77
77
 
78
78
  def _build_update_panel(