glaip-sdk 0.2.0__py3-none-any.whl → 0.2.2__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.
- glaip_sdk/_version.py +8 -0
- glaip_sdk/branding.py +13 -0
- glaip_sdk/cli/commands/agents.py +131 -5
- glaip_sdk/cli/commands/mcps.py +16 -0
- glaip_sdk/cli/commands/models.py +8 -0
- glaip_sdk/cli/commands/tools.py +8 -0
- glaip_sdk/cli/commands/transcripts.py +8 -0
- glaip_sdk/cli/constants.py +35 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/display.py +34 -19
- glaip_sdk/cli/main.py +13 -4
- glaip_sdk/cli/masking.py +8 -33
- glaip_sdk/cli/pager.py +9 -10
- glaip_sdk/cli/slash/agent_session.py +54 -7
- glaip_sdk/cli/slash/prompt.py +5 -0
- glaip_sdk/cli/slash/session.py +214 -4
- glaip_sdk/cli/transcript/viewer.py +226 -0
- glaip_sdk/cli/update_notifier.py +7 -4
- glaip_sdk/cli/utils.py +84 -27
- glaip_sdk/client/_agent_payloads.py +30 -0
- glaip_sdk/client/agents.py +144 -0
- glaip_sdk/client/main.py +5 -0
- glaip_sdk/client/run_rendering.py +66 -0
- glaip_sdk/utils/serialization.py +16 -0
- {glaip_sdk-0.2.0.dist-info → glaip_sdk-0.2.2.dist-info}/METADATA +1 -1
- {glaip_sdk-0.2.0.dist-info → glaip_sdk-0.2.2.dist-info}/RECORD +28 -27
- {glaip_sdk-0.2.0.dist-info → glaip_sdk-0.2.2.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.2.0.dist-info → glaip_sdk-0.2.2.dist-info}/entry_points.txt +0 -0
|
@@ -85,6 +85,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
85
85
|
# Rendering helpers
|
|
86
86
|
# ------------------------------------------------------------------
|
|
87
87
|
def _render(self) -> None:
|
|
88
|
+
"""Render the transcript viewer interface."""
|
|
88
89
|
try:
|
|
89
90
|
if self.console.is_terminal:
|
|
90
91
|
self.console.clear()
|
|
@@ -115,12 +116,22 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
115
116
|
self._render_transcript_view(query)
|
|
116
117
|
|
|
117
118
|
def _render_default_view(self, query: str | None) -> None:
|
|
119
|
+
"""Render the default summary view.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
query: Optional user query to display.
|
|
123
|
+
"""
|
|
118
124
|
if query:
|
|
119
125
|
self._render_user_query(query)
|
|
120
126
|
self._render_steps_summary()
|
|
121
127
|
self._render_final_panel()
|
|
122
128
|
|
|
123
129
|
def _render_transcript_view(self, query: str | None) -> None:
|
|
130
|
+
"""Render the full transcript view with events.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
query: Optional user query to display.
|
|
134
|
+
"""
|
|
124
135
|
if not self.ctx.events:
|
|
125
136
|
self.console.print("[dim]No SSE events were captured for this run.[/dim]")
|
|
126
137
|
return
|
|
@@ -148,6 +159,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
148
159
|
self.console.print()
|
|
149
160
|
|
|
150
161
|
def _render_final_panel(self) -> None:
|
|
162
|
+
"""Render the final result panel."""
|
|
151
163
|
content = self.ctx.final_output or self.ctx.default_output or "No response content captured."
|
|
152
164
|
title = "Final Result"
|
|
153
165
|
duration_text = self._extract_final_duration()
|
|
@@ -161,6 +173,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
161
173
|
# Interaction loops
|
|
162
174
|
# ------------------------------------------------------------------
|
|
163
175
|
def _fallback_loop(self) -> None:
|
|
176
|
+
"""Fallback interaction loop for non-interactive terminals."""
|
|
164
177
|
while True:
|
|
165
178
|
try:
|
|
166
179
|
ch = click.getchar()
|
|
@@ -181,6 +194,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
181
194
|
continue
|
|
182
195
|
|
|
183
196
|
def _handle_command(self, raw: str) -> bool:
|
|
197
|
+
"""Handle a command input.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
raw: Raw command string.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
True to continue, False to exit.
|
|
204
|
+
"""
|
|
184
205
|
lowered = raw.lower()
|
|
185
206
|
if lowered in {"exit", "quit", "q"}:
|
|
186
207
|
return True
|
|
@@ -339,15 +360,26 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
339
360
|
self.console.print(f"[red]Failed to export transcript: {exc}[/red]")
|
|
340
361
|
|
|
341
362
|
def _print_command_hint(self) -> None:
|
|
363
|
+
"""Print command hint for user interaction."""
|
|
342
364
|
self.console.print("[dim]Ctrl+T to toggle transcript · type `e` to export · press Enter to exit[/dim]")
|
|
343
365
|
self.console.print()
|
|
344
366
|
|
|
345
367
|
def _get_user_query(self) -> str | None:
|
|
368
|
+
"""Extract user query from metadata or manifest.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
User query string or None.
|
|
372
|
+
"""
|
|
346
373
|
meta = self.ctx.meta or {}
|
|
347
374
|
manifest = self.ctx.manifest_entry or {}
|
|
348
375
|
return meta.get("input_message") or meta.get("query") or meta.get("message") or manifest.get("input_message")
|
|
349
376
|
|
|
350
377
|
def _render_user_query(self, query: str) -> None:
|
|
378
|
+
"""Render user query in a panel.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
query: User query string to render.
|
|
382
|
+
"""
|
|
351
383
|
panel = AIPPanel(
|
|
352
384
|
Markdown(f"Query: {query}"),
|
|
353
385
|
title="User Request",
|
|
@@ -357,6 +389,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
357
389
|
self.console.print()
|
|
358
390
|
|
|
359
391
|
def _render_steps_summary(self) -> None:
|
|
392
|
+
"""Render steps summary panel."""
|
|
360
393
|
stored_lines = self.ctx.meta.get("transcript_step_lines")
|
|
361
394
|
if stored_lines:
|
|
362
395
|
body = Text("\n".join(stored_lines), style="dim")
|
|
@@ -373,6 +406,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
373
406
|
|
|
374
407
|
@staticmethod
|
|
375
408
|
def _format_steps_summary(steps: list[dict[str, Any]]) -> str:
|
|
409
|
+
"""Format steps summary as text.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
steps: List of step dictionaries.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Formatted text string.
|
|
416
|
+
"""
|
|
376
417
|
if not steps:
|
|
377
418
|
return " No steps yet"
|
|
378
419
|
|
|
@@ -388,6 +429,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
388
429
|
|
|
389
430
|
@staticmethod
|
|
390
431
|
def _extract_event_time(event: dict[str, Any]) -> float | None:
|
|
432
|
+
"""Extract timestamp from event metadata.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
event: Event dictionary.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Time value as float or None.
|
|
439
|
+
"""
|
|
391
440
|
metadata = event.get("metadata") or {}
|
|
392
441
|
time_value = metadata.get("time")
|
|
393
442
|
try:
|
|
@@ -399,6 +448,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
399
448
|
|
|
400
449
|
@staticmethod
|
|
401
450
|
def _parse_received_timestamp(event: dict[str, Any]) -> datetime | None:
|
|
451
|
+
"""Parse received timestamp from event.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
event: Event dictionary.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Parsed datetime or None.
|
|
458
|
+
"""
|
|
402
459
|
value = event.get("received_at")
|
|
403
460
|
if not value:
|
|
404
461
|
return None
|
|
@@ -412,6 +469,11 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
412
469
|
return None
|
|
413
470
|
|
|
414
471
|
def _extract_final_duration(self) -> str | None:
|
|
472
|
+
"""Extract final duration from events.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
Duration string or None.
|
|
476
|
+
"""
|
|
415
477
|
for event in self.ctx.events:
|
|
416
478
|
metadata = event.get("metadata") or {}
|
|
417
479
|
if metadata.get("kind") == "final_response":
|
|
@@ -424,6 +486,11 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
424
486
|
return None
|
|
425
487
|
|
|
426
488
|
def _build_step_summary(self) -> list[dict[str, Any]]:
|
|
489
|
+
"""Build step summary from stored steps or events.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
List of step dictionaries.
|
|
493
|
+
"""
|
|
427
494
|
stored = self.ctx.meta.get("transcript_steps")
|
|
428
495
|
if isinstance(stored, list) and stored:
|
|
429
496
|
return [
|
|
@@ -496,6 +563,15 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
496
563
|
is_last: bool,
|
|
497
564
|
lines: list[str],
|
|
498
565
|
) -> None:
|
|
566
|
+
"""Recursively render a tree branch of steps.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
manager: StepManager instance.
|
|
570
|
+
step_id: ID of step to render.
|
|
571
|
+
ancestor_state: Tuple of ancestor branch states.
|
|
572
|
+
is_last: Whether this is the last child.
|
|
573
|
+
lines: List to append rendered lines to.
|
|
574
|
+
"""
|
|
499
575
|
step = manager.by_id.get(step_id)
|
|
500
576
|
if not step:
|
|
501
577
|
return
|
|
@@ -525,6 +601,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
525
601
|
)
|
|
526
602
|
|
|
527
603
|
def _should_hide_step(self, step: Any) -> bool:
|
|
604
|
+
"""Check if a step should be hidden.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
step: Step object.
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
True if step should be hidden.
|
|
611
|
+
"""
|
|
528
612
|
if getattr(step, "parent_id", None) is None:
|
|
529
613
|
return False
|
|
530
614
|
name = getattr(step, "name", "") or ""
|
|
@@ -536,6 +620,13 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
536
620
|
root_id: str,
|
|
537
621
|
lines: list[str],
|
|
538
622
|
) -> None:
|
|
623
|
+
"""Decorate root step presentation with friendly label.
|
|
624
|
+
|
|
625
|
+
Args:
|
|
626
|
+
manager: StepManager instance.
|
|
627
|
+
root_id: Root step ID.
|
|
628
|
+
lines: Lines list to modify.
|
|
629
|
+
"""
|
|
539
630
|
if not lines:
|
|
540
631
|
return
|
|
541
632
|
|
|
@@ -554,6 +645,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
554
645
|
lines.insert(1, f" {query}")
|
|
555
646
|
|
|
556
647
|
def _coerce_step_event(self, event: dict[str, Any]) -> dict[str, Any] | None:
|
|
648
|
+
"""Coerce event to step event format.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
event: Event dictionary.
|
|
652
|
+
|
|
653
|
+
Returns:
|
|
654
|
+
Step event dictionary or None.
|
|
655
|
+
"""
|
|
557
656
|
metadata = event.get("metadata")
|
|
558
657
|
if not isinstance(metadata, dict):
|
|
559
658
|
return None
|
|
@@ -569,6 +668,15 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
569
668
|
}
|
|
570
669
|
|
|
571
670
|
def _format_tree_line(self, step: Any, branch_state: tuple[bool, ...]) -> str:
|
|
671
|
+
"""Format a tree line for a step.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
step: Step object.
|
|
675
|
+
branch_state: Branch state tuple.
|
|
676
|
+
|
|
677
|
+
Returns:
|
|
678
|
+
Formatted line string.
|
|
679
|
+
"""
|
|
572
680
|
prefix = build_connector_prefix(branch_state)
|
|
573
681
|
raw_label = normalise_display_label(getattr(step, "display_label", None))
|
|
574
682
|
title, summary = self._split_label(raw_label)
|
|
@@ -594,6 +702,15 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
594
702
|
return line
|
|
595
703
|
|
|
596
704
|
def _friendly_root_label(self, step: Any, fallback: str | None) -> str:
|
|
705
|
+
"""Generate friendly label for root step.
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
step: Step object.
|
|
709
|
+
fallback: Fallback label string.
|
|
710
|
+
|
|
711
|
+
Returns:
|
|
712
|
+
Friendly label string.
|
|
713
|
+
"""
|
|
597
714
|
agent_name = self.ctx.manifest_entry.get("agent_name") or (self.ctx.meta or {}).get("agent_name")
|
|
598
715
|
agent_id = self.ctx.manifest_entry.get("agent_id") or getattr(step, "name", "")
|
|
599
716
|
|
|
@@ -607,6 +724,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
607
724
|
|
|
608
725
|
@staticmethod
|
|
609
726
|
def _format_duration_badge(step: Any) -> str | None:
|
|
727
|
+
"""Format duration badge for a step.
|
|
728
|
+
|
|
729
|
+
Args:
|
|
730
|
+
step: Step object.
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
Duration badge string or None.
|
|
734
|
+
"""
|
|
610
735
|
duration_ms = getattr(step, "duration_ms", None)
|
|
611
736
|
if duration_ms is None:
|
|
612
737
|
return None
|
|
@@ -626,6 +751,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
626
751
|
|
|
627
752
|
@staticmethod
|
|
628
753
|
def _split_label(label: str) -> tuple[str, str | None]:
|
|
754
|
+
"""Split label into title and summary.
|
|
755
|
+
|
|
756
|
+
Args:
|
|
757
|
+
label: Label string.
|
|
758
|
+
|
|
759
|
+
Returns:
|
|
760
|
+
Tuple of (title, summary).
|
|
761
|
+
"""
|
|
629
762
|
if " — " in label:
|
|
630
763
|
title, summary = label.split(" — ", 1)
|
|
631
764
|
return title.strip(), summary.strip()
|
|
@@ -633,6 +766,15 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
633
766
|
|
|
634
767
|
@staticmethod
|
|
635
768
|
def _truncate_summary(summary: str, limit: int = 48) -> str:
|
|
769
|
+
"""Truncate summary to specified length.
|
|
770
|
+
|
|
771
|
+
Args:
|
|
772
|
+
summary: Summary string.
|
|
773
|
+
limit: Maximum length.
|
|
774
|
+
|
|
775
|
+
Returns:
|
|
776
|
+
Truncated summary string.
|
|
777
|
+
"""
|
|
636
778
|
summary = summary.strip()
|
|
637
779
|
if len(summary) <= limit:
|
|
638
780
|
return summary
|
|
@@ -640,6 +782,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
640
782
|
|
|
641
783
|
@staticmethod
|
|
642
784
|
def _looks_like_uuid(value: str) -> bool:
|
|
785
|
+
"""Check if string looks like a UUID.
|
|
786
|
+
|
|
787
|
+
Args:
|
|
788
|
+
value: String to check.
|
|
789
|
+
|
|
790
|
+
Returns:
|
|
791
|
+
True if value looks like UUID.
|
|
792
|
+
"""
|
|
643
793
|
stripped = value.replace("-", "").replace(" ", "")
|
|
644
794
|
if len(stripped) not in {32, 36}:
|
|
645
795
|
return False
|
|
@@ -647,6 +797,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
647
797
|
|
|
648
798
|
@staticmethod
|
|
649
799
|
def _format_duration_from_ms(value: Any) -> str | None:
|
|
800
|
+
"""Format duration from milliseconds.
|
|
801
|
+
|
|
802
|
+
Args:
|
|
803
|
+
value: Duration value in milliseconds.
|
|
804
|
+
|
|
805
|
+
Returns:
|
|
806
|
+
Formatted duration string or None.
|
|
807
|
+
"""
|
|
650
808
|
try:
|
|
651
809
|
if value is None:
|
|
652
810
|
return None
|
|
@@ -662,12 +820,29 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
662
820
|
|
|
663
821
|
@staticmethod
|
|
664
822
|
def _is_step_event(metadata: dict[str, Any]) -> bool:
|
|
823
|
+
"""Check if metadata represents a step event.
|
|
824
|
+
|
|
825
|
+
Args:
|
|
826
|
+
metadata: Event metadata dictionary.
|
|
827
|
+
|
|
828
|
+
Returns:
|
|
829
|
+
True if metadata represents a step event.
|
|
830
|
+
"""
|
|
665
831
|
kind = metadata.get("kind")
|
|
666
832
|
return kind in {"agent_step", "agent_thinking_step"}
|
|
667
833
|
|
|
668
834
|
def _iter_step_candidates(
|
|
669
835
|
self, event: dict[str, Any], metadata: dict[str, Any]
|
|
670
836
|
) -> Iterable[tuple[str, dict[str, Any]]]:
|
|
837
|
+
"""Iterate step candidates from event.
|
|
838
|
+
|
|
839
|
+
Args:
|
|
840
|
+
event: Event dictionary.
|
|
841
|
+
metadata: Event metadata dictionary.
|
|
842
|
+
|
|
843
|
+
Yields:
|
|
844
|
+
Tuples of (step_name, step_info).
|
|
845
|
+
"""
|
|
671
846
|
tool_info = metadata.get("tool_info") or {}
|
|
672
847
|
|
|
673
848
|
yielded = False
|
|
@@ -691,6 +866,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
691
866
|
def _iter_tool_call_candidates(
|
|
692
867
|
tool_info: dict[str, Any],
|
|
693
868
|
) -> Iterable[tuple[str, dict[str, Any]]]:
|
|
869
|
+
"""Iterate tool call candidates from tool_info.
|
|
870
|
+
|
|
871
|
+
Args:
|
|
872
|
+
tool_info: Tool info dictionary.
|
|
873
|
+
|
|
874
|
+
Yields:
|
|
875
|
+
Tuples of (tool_name, tool_call_info).
|
|
876
|
+
"""
|
|
694
877
|
tool_calls = tool_info.get("tool_calls")
|
|
695
878
|
if isinstance(tool_calls, list):
|
|
696
879
|
for call in tool_calls:
|
|
@@ -702,6 +885,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
702
885
|
def _extract_direct_tool(
|
|
703
886
|
tool_info: dict[str, Any],
|
|
704
887
|
) -> tuple[str, dict[str, Any]] | None:
|
|
888
|
+
"""Extract direct tool from tool_info.
|
|
889
|
+
|
|
890
|
+
Args:
|
|
891
|
+
tool_info: Tool info dictionary.
|
|
892
|
+
|
|
893
|
+
Returns:
|
|
894
|
+
Tuple of (tool_name, tool_info) or None.
|
|
895
|
+
"""
|
|
705
896
|
if isinstance(tool_info, dict):
|
|
706
897
|
name = tool_info.get("name")
|
|
707
898
|
if name:
|
|
@@ -710,6 +901,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
710
901
|
|
|
711
902
|
@staticmethod
|
|
712
903
|
def _extract_completed_name(event: dict[str, Any]) -> str | None:
|
|
904
|
+
"""Extract completed tool name from event content.
|
|
905
|
+
|
|
906
|
+
Args:
|
|
907
|
+
event: Event dictionary.
|
|
908
|
+
|
|
909
|
+
Returns:
|
|
910
|
+
Tool name or None.
|
|
911
|
+
"""
|
|
713
912
|
content = event.get("content") or ""
|
|
714
913
|
if isinstance(content, str) and content.startswith("Completed "):
|
|
715
914
|
name = content.replace("Completed ", "").strip()
|
|
@@ -723,6 +922,16 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
723
922
|
order: list[str],
|
|
724
923
|
name: str,
|
|
725
924
|
) -> dict[str, Any]:
|
|
925
|
+
"""Ensure step entry exists, creating if needed.
|
|
926
|
+
|
|
927
|
+
Args:
|
|
928
|
+
steps: Steps dictionary.
|
|
929
|
+
order: Order list.
|
|
930
|
+
name: Step name.
|
|
931
|
+
|
|
932
|
+
Returns:
|
|
933
|
+
Step dictionary.
|
|
934
|
+
"""
|
|
726
935
|
if name not in steps:
|
|
727
936
|
steps[name] = {
|
|
728
937
|
"name": name,
|
|
@@ -742,6 +951,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
742
951
|
info: dict[str, Any],
|
|
743
952
|
event: dict[str, Any],
|
|
744
953
|
) -> None:
|
|
954
|
+
"""Apply update to step from event metadata.
|
|
955
|
+
|
|
956
|
+
Args:
|
|
957
|
+
step: Step dictionary to update.
|
|
958
|
+
metadata: Event metadata.
|
|
959
|
+
info: Step info dictionary.
|
|
960
|
+
event: Event dictionary.
|
|
961
|
+
"""
|
|
745
962
|
status = metadata.get("status")
|
|
746
963
|
event_time = metadata.get("time")
|
|
747
964
|
|
|
@@ -760,6 +977,15 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
760
977
|
|
|
761
978
|
@staticmethod
|
|
762
979
|
def _is_step_finished(metadata: dict[str, Any], event: dict[str, Any]) -> bool:
|
|
980
|
+
"""Check if step is finished.
|
|
981
|
+
|
|
982
|
+
Args:
|
|
983
|
+
metadata: Event metadata.
|
|
984
|
+
event: Event dictionary.
|
|
985
|
+
|
|
986
|
+
Returns:
|
|
987
|
+
True if step is finished.
|
|
988
|
+
"""
|
|
763
989
|
status = metadata.get("status")
|
|
764
990
|
return status == "finished" or bool(event.get("final"))
|
|
765
991
|
|
glaip_sdk/cli/update_notifier.py
CHANGED
|
@@ -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
|
|
75
|
+
return UPDATE_CHECK_ENABLED
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
def _build_update_panel(
|
|
@@ -244,6 +244,7 @@ def _suppress_library_logging(
|
|
|
244
244
|
def _refresh_installed_version(console: Console, ctx: Any) -> None:
|
|
245
245
|
"""Reload runtime metadata after an in-process upgrade."""
|
|
246
246
|
new_version: str | None = None
|
|
247
|
+
branding_module: Any | None = None
|
|
247
248
|
|
|
248
249
|
try:
|
|
249
250
|
version_module = importlib.reload(importlib.import_module("glaip_sdk._version"))
|
|
@@ -252,16 +253,18 @@ def _refresh_installed_version(console: Console, ctx: Any) -> None:
|
|
|
252
253
|
_LOGGER.debug("Failed to reload glaip_sdk._version: %s", exc, exc_info=True)
|
|
253
254
|
|
|
254
255
|
try:
|
|
255
|
-
branding_module = importlib.import_module("glaip_sdk.branding")
|
|
256
|
+
branding_module = importlib.reload(importlib.import_module("glaip_sdk.branding"))
|
|
256
257
|
if new_version:
|
|
257
258
|
branding_module.SDK_VERSION = new_version
|
|
258
259
|
except Exception as exc: # pragma: no cover - defensive guard
|
|
259
260
|
_LOGGER.debug("Failed to update branding metadata: %s", exc, exc_info=True)
|
|
261
|
+
branding_module = None
|
|
260
262
|
|
|
261
263
|
session = _get_slash_session(ctx)
|
|
262
264
|
if session and hasattr(session, "refresh_branding"):
|
|
263
265
|
try:
|
|
264
|
-
|
|
266
|
+
branding_cls = getattr(branding_module, "AIPBranding", None) if branding_module else None
|
|
267
|
+
session.refresh_branding(new_version, branding_cls=branding_cls)
|
|
265
268
|
return
|
|
266
269
|
except Exception as exc: # pragma: no cover - defensive guard
|
|
267
270
|
_LOGGER.debug("Failed to refresh active slash session: %s", exc, exc_info=True)
|
glaip_sdk/cli/utils.py
CHANGED
|
@@ -18,9 +18,10 @@ from pathlib import Path
|
|
|
18
18
|
from typing import TYPE_CHECKING, Any, cast
|
|
19
19
|
|
|
20
20
|
import click
|
|
21
|
+
import yaml
|
|
21
22
|
from rich.console import Console, Group
|
|
22
23
|
from rich.markdown import Markdown
|
|
23
|
-
from rich.
|
|
24
|
+
from rich.syntax import Syntax
|
|
24
25
|
|
|
25
26
|
from glaip_sdk import _version as _version_module
|
|
26
27
|
from glaip_sdk.branding import (
|
|
@@ -30,9 +31,40 @@ from glaip_sdk.branding import (
|
|
|
30
31
|
SUCCESS_STYLE,
|
|
31
32
|
WARNING_STYLE,
|
|
32
33
|
)
|
|
34
|
+
from glaip_sdk.cli import masking, pager
|
|
35
|
+
from glaip_sdk.cli.constants import LITERAL_STRING_THRESHOLD, TABLE_SORT_ENABLED
|
|
36
|
+
from glaip_sdk.cli.config import load_config
|
|
37
|
+
from glaip_sdk.cli.context import (
|
|
38
|
+
_get_view,
|
|
39
|
+
detect_export_format as _detect_export_format,
|
|
40
|
+
get_ctx_value,
|
|
41
|
+
)
|
|
33
42
|
from glaip_sdk.cli.rich_helpers import markup_text
|
|
34
43
|
from glaip_sdk.icons import ICON_AGENT
|
|
35
|
-
from glaip_sdk.rich_components import AIPPanel
|
|
44
|
+
from glaip_sdk.rich_components import AIPPanel, AIPTable
|
|
45
|
+
from glaip_sdk.utils import is_uuid
|
|
46
|
+
from glaip_sdk.utils.rendering.renderer import (
|
|
47
|
+
CapturingConsole,
|
|
48
|
+
RendererConfig,
|
|
49
|
+
RichStreamRenderer,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _LiteralYamlDumper(yaml.SafeDumper):
|
|
54
|
+
"""YAML dumper that emits literal scalars for multiline strings."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _literal_str_representer(dumper: yaml.Dumper, data: str) -> yaml.nodes.ScalarNode:
|
|
58
|
+
"""Represent strings in YAML, using literal blocks for verbose values."""
|
|
59
|
+
needs_literal = "\n" in data or "\r" in data
|
|
60
|
+
if not needs_literal and LITERAL_STRING_THRESHOLD and len(data) >= LITERAL_STRING_THRESHOLD: # pragma: no cover
|
|
61
|
+
needs_literal = True
|
|
62
|
+
|
|
63
|
+
style = "|" if needs_literal else None
|
|
64
|
+
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=style)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_LiteralYamlDumper.add_representer(str, _literal_str_representer)
|
|
36
68
|
|
|
37
69
|
# Optional interactive deps (fuzzy palette)
|
|
38
70
|
try:
|
|
@@ -56,22 +88,6 @@ except Exception: # pragma: no cover - optional dependency
|
|
|
56
88
|
|
|
57
89
|
if TYPE_CHECKING: # pragma: no cover - import-only during type checking
|
|
58
90
|
from glaip_sdk import Client
|
|
59
|
-
from glaip_sdk.cli import masking, pager
|
|
60
|
-
from glaip_sdk.cli.config import load_config
|
|
61
|
-
from glaip_sdk.cli.context import (
|
|
62
|
-
_get_view,
|
|
63
|
-
get_ctx_value,
|
|
64
|
-
)
|
|
65
|
-
from glaip_sdk.cli.context import (
|
|
66
|
-
detect_export_format as _detect_export_format,
|
|
67
|
-
)
|
|
68
|
-
from glaip_sdk.rich_components import AIPTable
|
|
69
|
-
from glaip_sdk.utils import is_uuid
|
|
70
|
-
from glaip_sdk.utils.rendering.renderer import (
|
|
71
|
-
CapturingConsole,
|
|
72
|
-
RendererConfig,
|
|
73
|
-
RichStreamRenderer,
|
|
74
|
-
)
|
|
75
91
|
|
|
76
92
|
console = Console()
|
|
77
93
|
pager.console = console
|
|
@@ -602,6 +618,7 @@ def _prompt_with_auto_select(
|
|
|
602
618
|
valid_choices = set(choices)
|
|
603
619
|
|
|
604
620
|
def _auto_select(_: Buffer) -> None:
|
|
621
|
+
"""Auto-select text when a valid choice is entered."""
|
|
605
622
|
text = buffer.text
|
|
606
623
|
if not text or text not in valid_choices:
|
|
607
624
|
return
|
|
@@ -635,9 +652,23 @@ class _FuzzyCompleter:
|
|
|
635
652
|
"""Fuzzy completer for prompt_toolkit."""
|
|
636
653
|
|
|
637
654
|
def __init__(self, words: list[str]) -> None:
|
|
655
|
+
"""Initialize fuzzy completer with word list.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
words: List of words to complete from.
|
|
659
|
+
"""
|
|
638
660
|
self.words = words
|
|
639
661
|
|
|
640
662
|
def get_completions(self, document: Any, _complete_event: Any) -> Any: # pragma: no cover
|
|
663
|
+
"""Get fuzzy completions for the current word.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
document: Document object from prompt_toolkit.
|
|
667
|
+
_complete_event: Completion event (unused).
|
|
668
|
+
|
|
669
|
+
Yields:
|
|
670
|
+
Completion objects matching the current word.
|
|
671
|
+
"""
|
|
641
672
|
word = document.get_word_before_cursor()
|
|
642
673
|
if not word:
|
|
643
674
|
return
|
|
@@ -769,7 +800,7 @@ def _fuzzy_score(search: str, target: str) -> int:
|
|
|
769
800
|
return score
|
|
770
801
|
|
|
771
802
|
|
|
772
|
-
#
|
|
803
|
+
# ----------------------- Structured renderer helpers -------------------- #
|
|
773
804
|
|
|
774
805
|
|
|
775
806
|
def _coerce_result_payload(result: Any) -> Any:
|
|
@@ -811,6 +842,37 @@ def _render_markdown_output(data: Any) -> None:
|
|
|
811
842
|
click.echo(str(data))
|
|
812
843
|
|
|
813
844
|
|
|
845
|
+
def _format_yaml_text(data: Any) -> str:
|
|
846
|
+
"""Convert structured payloads to YAML for readability."""
|
|
847
|
+
try:
|
|
848
|
+
yaml_text = yaml.dump(
|
|
849
|
+
data,
|
|
850
|
+
sort_keys=False,
|
|
851
|
+
default_flow_style=False,
|
|
852
|
+
allow_unicode=True,
|
|
853
|
+
Dumper=_LiteralYamlDumper,
|
|
854
|
+
)
|
|
855
|
+
except Exception: # pragma: no cover - defensive YAML fallback
|
|
856
|
+
try:
|
|
857
|
+
return str(data)
|
|
858
|
+
except Exception: # pragma: no cover - defensive str fallback
|
|
859
|
+
return repr(data)
|
|
860
|
+
|
|
861
|
+
yaml_text = yaml_text.rstrip()
|
|
862
|
+
if yaml_text.endswith("..."): # pragma: no cover - defensive YAML cleanup
|
|
863
|
+
yaml_text = yaml_text[:-3].rstrip()
|
|
864
|
+
return yaml_text
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def _build_yaml_renderable(data: Any) -> Any:
|
|
868
|
+
"""Return a syntax-highlighted YAML renderable when possible."""
|
|
869
|
+
yaml_text = _format_yaml_text(data) or "# No data"
|
|
870
|
+
try:
|
|
871
|
+
return Syntax(yaml_text, "yaml", word_wrap=False)
|
|
872
|
+
except Exception: # pragma: no cover - defensive syntax highlighting fallback
|
|
873
|
+
return yaml_text
|
|
874
|
+
|
|
875
|
+
|
|
814
876
|
def output_result(
|
|
815
877
|
ctx: Any,
|
|
816
878
|
result: Any,
|
|
@@ -843,7 +905,7 @@ def output_result(
|
|
|
843
905
|
_render_markdown_output(data)
|
|
844
906
|
return
|
|
845
907
|
|
|
846
|
-
renderable =
|
|
908
|
+
renderable = _build_yaml_renderable(data)
|
|
847
909
|
if panel_title:
|
|
848
910
|
console.print(AIPPanel(renderable, title=panel_title))
|
|
849
911
|
else:
|
|
@@ -854,7 +916,7 @@ def output_result(
|
|
|
854
916
|
# ----------------------------- List rendering ---------------------------- #
|
|
855
917
|
|
|
856
918
|
# Threshold no longer used - fuzzy palette is always default for TTY
|
|
857
|
-
# _PICK_THRESHOLD =
|
|
919
|
+
# _PICK_THRESHOLD = 5
|
|
858
920
|
|
|
859
921
|
|
|
860
922
|
def _normalise_rows(items: list[Any], transform_func: Callable[[Any], dict[str, Any]] | None) -> list[dict[str, Any]]:
|
|
@@ -902,12 +964,7 @@ def _render_markdown_list(rows: list[dict[str, Any]], title: str, columns: list[
|
|
|
902
964
|
|
|
903
965
|
def _should_sort_rows(rows: list[dict[str, Any]]) -> bool:
|
|
904
966
|
"""Return True when rows should be name-sorted prior to rendering."""
|
|
905
|
-
return (
|
|
906
|
-
os.getenv("AIP_TABLE_NO_SORT", "0") not in ("1", "true", "on")
|
|
907
|
-
and rows
|
|
908
|
-
and isinstance(rows[0], dict)
|
|
909
|
-
and "name" in rows[0]
|
|
910
|
-
)
|
|
967
|
+
return TABLE_SORT_ENABLED and rows and isinstance(rows[0], dict) and "name" in rows[0]
|
|
911
968
|
|
|
912
969
|
|
|
913
970
|
def _create_table(columns: list[tuple[str, str, str, int | None]], title: str) -> Any:
|