glaip-sdk 0.1.3__py3-none-any.whl → 0.6.10__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/__init__.py +5 -2
- glaip_sdk/_version.py +9 -0
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1191 -0
- glaip_sdk/branding.py +13 -0
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/auth.py +254 -15
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +213 -73
- glaip_sdk/cli/commands/common_config.py +101 -0
- glaip_sdk/cli/commands/configure.py +729 -113
- glaip_sdk/cli/commands/mcps.py +241 -72
- glaip_sdk/cli/commands/models.py +11 -5
- glaip_sdk/cli/commands/tools.py +49 -57
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/config.py +48 -4
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +846 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +35 -19
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +6 -3
- glaip_sdk/cli/main.py +228 -119
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/pager.py +9 -10
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +578 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +62 -21
- glaip_sdk/cli/slash/prompt.py +21 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +771 -140
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +255 -44
- glaip_sdk/cli/transcript/capture.py +27 -1
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/viewer.py +72 -499
- glaip_sdk/cli/update_notifier.py +14 -5
- glaip_sdk/cli/utils.py +243 -1252
- glaip_sdk/cli/validators.py +5 -6
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +45 -9
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +287 -29
- glaip_sdk/client/base.py +1 -0
- glaip_sdk/client/main.py +19 -10
- glaip_sdk/client/mcps.py +122 -12
- glaip_sdk/client/run_rendering.py +133 -88
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +155 -10
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +90 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +116 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +232 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +115 -0
- glaip_sdk/runner/langgraph.py +706 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +58 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +39 -7
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +23 -15
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +0 -33
- glaip_sdk/utils/import_export.py +12 -7
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +5 -30
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +1 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
- glaip_sdk/utils/rendering/renderer/base.py +217 -1476
- glaip_sdk/utils/rendering/renderer/debug.py +26 -20
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +4 -12
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/resource_refs.py +25 -13
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +18 -0
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +16 -24
- {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/METADATA +42 -4
- glaip_sdk-0.6.10.dist-info/RECORD +159 -0
- {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/WHEEL +1 -1
- glaip_sdk/models.py +0 -240
- glaip_sdk-0.1.3.dist-info/RECORD +0 -83
- {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/entry_points.txt +0 -0
|
@@ -6,16 +6,12 @@ Authors:
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from collections.abc import Callable
|
|
10
|
-
from dataclasses import dataclass
|
|
11
|
-
from datetime import datetime, timezone
|
|
9
|
+
from collections.abc import Callable
|
|
12
10
|
from pathlib import Path
|
|
13
11
|
from typing import Any
|
|
14
12
|
|
|
15
13
|
import click
|
|
16
14
|
from rich.console import Console
|
|
17
|
-
from rich.markdown import Markdown
|
|
18
|
-
from rich.text import Text
|
|
19
15
|
|
|
20
16
|
try: # pragma: no cover - optional dependency
|
|
21
17
|
import questionary
|
|
@@ -25,34 +21,21 @@ except Exception: # pragma: no cover - optional dependency
|
|
|
25
21
|
Choice = None # type: ignore[assignment]
|
|
26
22
|
|
|
27
23
|
from glaip_sdk.cli.transcript.cache import suggest_filename
|
|
28
|
-
from glaip_sdk.
|
|
29
|
-
from glaip_sdk.
|
|
30
|
-
from glaip_sdk.utils.rendering.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
from glaip_sdk.cli.utils import prompt_export_choice_questionary, questionary_safe_ask
|
|
25
|
+
from glaip_sdk.utils.rendering.layout.progress import is_delegation_tool
|
|
26
|
+
from glaip_sdk.utils.rendering.layout.transcript import DEFAULT_TRANSCRIPT_THEME
|
|
27
|
+
from glaip_sdk.utils.rendering.viewer import (
|
|
28
|
+
ViewerContext as PresenterViewerContext,
|
|
29
|
+
prepare_viewer_snapshot as presenter_prepare_viewer_snapshot,
|
|
30
|
+
render_post_run_view as presenter_render_post_run_view,
|
|
31
|
+
render_transcript_events as presenter_render_transcript_events,
|
|
32
|
+
render_transcript_view as presenter_render_transcript_view,
|
|
34
33
|
)
|
|
35
|
-
from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
|
|
36
|
-
from glaip_sdk.utils.rendering.renderer.panels import create_final_panel
|
|
37
|
-
from glaip_sdk.utils.rendering.renderer.progress import (
|
|
38
|
-
format_elapsed_time,
|
|
39
|
-
is_delegation_tool,
|
|
40
|
-
)
|
|
41
|
-
from glaip_sdk.utils.rendering.steps import StepManager
|
|
42
34
|
|
|
43
35
|
EXPORT_CANCELLED_MESSAGE = "[dim]Export cancelled.[/dim]"
|
|
44
36
|
|
|
45
37
|
|
|
46
|
-
|
|
47
|
-
class ViewerContext:
|
|
48
|
-
"""Runtime context for the viewer session."""
|
|
49
|
-
|
|
50
|
-
manifest_entry: dict[str, Any]
|
|
51
|
-
events: list[dict[str, Any]]
|
|
52
|
-
default_output: str
|
|
53
|
-
final_output: str
|
|
54
|
-
stream_started_at: float | None
|
|
55
|
-
meta: dict[str, Any]
|
|
38
|
+
ViewerContext = PresenterViewerContext
|
|
56
39
|
|
|
57
40
|
|
|
58
41
|
class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
@@ -63,12 +46,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
63
46
|
console: Console,
|
|
64
47
|
ctx: ViewerContext,
|
|
65
48
|
export_callback: Callable[[Path], Path],
|
|
49
|
+
*,
|
|
50
|
+
initial_view: str = "default",
|
|
66
51
|
) -> None:
|
|
67
52
|
"""Initialize viewer state for a captured transcript."""
|
|
68
53
|
self.console = console
|
|
69
54
|
self.ctx = ctx
|
|
70
55
|
self._export_callback = export_callback
|
|
71
|
-
self._view_mode = "default"
|
|
56
|
+
self._view_mode = initial_view if initial_view in {"default", "transcript"} else "default"
|
|
72
57
|
|
|
73
58
|
def run(self) -> None:
|
|
74
59
|
"""Enter the interactive loop."""
|
|
@@ -83,6 +68,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
83
68
|
# Rendering helpers
|
|
84
69
|
# ------------------------------------------------------------------
|
|
85
70
|
def _render(self) -> None:
|
|
71
|
+
"""Render the transcript viewer interface."""
|
|
86
72
|
try:
|
|
87
73
|
if self.console.is_terminal:
|
|
88
74
|
self.console.clear()
|
|
@@ -105,60 +91,19 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
105
91
|
self.console.print(f"[dim]{' · '.join(subtitle_parts)}[/]")
|
|
106
92
|
self.console.print()
|
|
107
93
|
|
|
108
|
-
query = self._get_user_query()
|
|
109
|
-
|
|
110
94
|
if self._view_mode == "default":
|
|
111
|
-
self.
|
|
95
|
+
presenter_render_post_run_view(self.console, self.ctx)
|
|
112
96
|
else:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
self._render_user_query(query)
|
|
118
|
-
self._render_steps_summary()
|
|
119
|
-
self._render_final_panel()
|
|
120
|
-
|
|
121
|
-
def _render_transcript_view(self, query: str | None) -> None:
|
|
122
|
-
if not self.ctx.events:
|
|
123
|
-
self.console.print("[dim]No SSE events were captured for this run.[/dim]")
|
|
124
|
-
return
|
|
125
|
-
|
|
126
|
-
if query:
|
|
127
|
-
self._render_user_query(query)
|
|
128
|
-
|
|
129
|
-
self._render_steps_summary()
|
|
130
|
-
self._render_final_panel()
|
|
131
|
-
|
|
132
|
-
self.console.print("[bold]Transcript Events[/bold]")
|
|
133
|
-
self.console.print("[dim]────────────────────────────────────────────────────────[/dim]")
|
|
134
|
-
|
|
135
|
-
base_received_ts: datetime | None = None
|
|
136
|
-
for event in self.ctx.events:
|
|
137
|
-
received_ts = self._parse_received_timestamp(event)
|
|
138
|
-
if base_received_ts is None and received_ts is not None:
|
|
139
|
-
base_received_ts = received_ts
|
|
140
|
-
render_debug_event(
|
|
141
|
-
event,
|
|
142
|
-
self.console,
|
|
143
|
-
received_ts=received_ts,
|
|
144
|
-
baseline_ts=base_received_ts,
|
|
145
|
-
)
|
|
146
|
-
self.console.print()
|
|
147
|
-
|
|
148
|
-
def _render_final_panel(self) -> None:
|
|
149
|
-
content = self.ctx.final_output or self.ctx.default_output or "No response content captured."
|
|
150
|
-
title = "Final Result"
|
|
151
|
-
duration_text = self._extract_final_duration()
|
|
152
|
-
if duration_text:
|
|
153
|
-
title += f" · {duration_text}"
|
|
154
|
-
panel = create_final_panel(content, title=title, theme="dark")
|
|
155
|
-
self.console.print(panel)
|
|
156
|
-
self.console.print()
|
|
97
|
+
theme = DEFAULT_TRANSCRIPT_THEME
|
|
98
|
+
snapshot, state = presenter_prepare_viewer_snapshot(self.ctx, glyphs=None, theme=theme)
|
|
99
|
+
presenter_render_transcript_view(self.console, snapshot, theme=theme)
|
|
100
|
+
presenter_render_transcript_events(self.console, state.events)
|
|
157
101
|
|
|
158
102
|
# ------------------------------------------------------------------
|
|
159
103
|
# Interaction loops
|
|
160
104
|
# ------------------------------------------------------------------
|
|
161
105
|
def _fallback_loop(self) -> None:
|
|
106
|
+
"""Fallback interaction loop for non-interactive terminals."""
|
|
162
107
|
while True:
|
|
163
108
|
try:
|
|
164
109
|
ch = click.getchar()
|
|
@@ -179,6 +124,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
179
124
|
continue
|
|
180
125
|
|
|
181
126
|
def _handle_command(self, raw: str) -> bool:
|
|
127
|
+
"""Handle a command input.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
raw: Raw command string.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
True to continue, False to exit.
|
|
134
|
+
"""
|
|
182
135
|
lowered = raw.lower()
|
|
183
136
|
if lowered in {"exit", "quit", "q"}:
|
|
184
137
|
return True
|
|
@@ -236,38 +189,10 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
236
189
|
|
|
237
190
|
def _prompt_export_choice(self, default_path: Path, default_display: str) -> tuple[str, Any] | None:
|
|
238
191
|
"""Render interactive export menu with numeric shortcuts."""
|
|
239
|
-
if not self.console.is_terminal
|
|
240
|
-
return None
|
|
241
|
-
|
|
242
|
-
try:
|
|
243
|
-
answer = questionary.select(
|
|
244
|
-
"Export transcript",
|
|
245
|
-
choices=[
|
|
246
|
-
Choice(
|
|
247
|
-
title=f"Save to default ({default_display})",
|
|
248
|
-
value=("default", default_path),
|
|
249
|
-
shortcut_key="1",
|
|
250
|
-
),
|
|
251
|
-
Choice(
|
|
252
|
-
title="Choose a different path",
|
|
253
|
-
value=("custom", None),
|
|
254
|
-
shortcut_key="2",
|
|
255
|
-
),
|
|
256
|
-
Choice(
|
|
257
|
-
title="Cancel",
|
|
258
|
-
value=("cancel", None),
|
|
259
|
-
shortcut_key="3",
|
|
260
|
-
),
|
|
261
|
-
],
|
|
262
|
-
use_shortcuts=True,
|
|
263
|
-
instruction="Press 1-3 (or arrows) then Enter.",
|
|
264
|
-
).ask()
|
|
265
|
-
except Exception:
|
|
192
|
+
if not self.console.is_terminal:
|
|
266
193
|
return None
|
|
267
194
|
|
|
268
|
-
|
|
269
|
-
return ("cancel", None)
|
|
270
|
-
return answer
|
|
195
|
+
return prompt_export_choice_questionary(default_path, default_display)
|
|
271
196
|
|
|
272
197
|
def _prompt_custom_destination(self) -> Path | None:
|
|
273
198
|
"""Prompt for custom export path with filesystem completion."""
|
|
@@ -275,11 +200,12 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
275
200
|
return None
|
|
276
201
|
|
|
277
202
|
try:
|
|
278
|
-
|
|
203
|
+
question = questionary.path(
|
|
279
204
|
"Destination path (Tab to autocomplete):",
|
|
280
205
|
default="",
|
|
281
206
|
only_directories=False,
|
|
282
|
-
)
|
|
207
|
+
)
|
|
208
|
+
response = questionary_safe_ask(question)
|
|
283
209
|
except Exception:
|
|
284
210
|
return None
|
|
285
211
|
|
|
@@ -337,369 +263,22 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
337
263
|
self.console.print(f"[red]Failed to export transcript: {exc}[/red]")
|
|
338
264
|
|
|
339
265
|
def _print_command_hint(self) -> None:
|
|
266
|
+
"""Print command hint for user interaction."""
|
|
340
267
|
self.console.print("[dim]Ctrl+T to toggle transcript · type `e` to export · press Enter to exit[/dim]")
|
|
341
268
|
self.console.print()
|
|
342
269
|
|
|
343
|
-
def _get_user_query(self) -> str | None:
|
|
344
|
-
meta = self.ctx.meta or {}
|
|
345
|
-
manifest = self.ctx.manifest_entry or {}
|
|
346
|
-
return meta.get("input_message") or meta.get("query") or meta.get("message") or manifest.get("input_message")
|
|
347
|
-
|
|
348
|
-
def _render_user_query(self, query: str) -> None:
|
|
349
|
-
panel = AIPPanel(
|
|
350
|
-
Markdown(f"Query: {query}"),
|
|
351
|
-
title="User Request",
|
|
352
|
-
border_style="#d97706",
|
|
353
|
-
)
|
|
354
|
-
self.console.print(panel)
|
|
355
|
-
self.console.print()
|
|
356
|
-
|
|
357
|
-
def _render_steps_summary(self) -> None:
|
|
358
|
-
stored_lines = self.ctx.meta.get("transcript_step_lines")
|
|
359
|
-
if stored_lines:
|
|
360
|
-
body = Text("\n".join(stored_lines), style="dim")
|
|
361
|
-
else:
|
|
362
|
-
tree_text = self._build_tree_summary_text()
|
|
363
|
-
if tree_text is not None:
|
|
364
|
-
body = tree_text
|
|
365
|
-
else:
|
|
366
|
-
panel_content = self._format_steps_summary(self._build_step_summary())
|
|
367
|
-
body = Text(panel_content, style="dim")
|
|
368
|
-
panel = AIPPanel(body, title="Steps", border_style="blue")
|
|
369
|
-
self.console.print(panel)
|
|
370
|
-
self.console.print()
|
|
371
|
-
|
|
372
|
-
@staticmethod
|
|
373
|
-
def _format_steps_summary(steps: list[dict[str, Any]]) -> str:
|
|
374
|
-
if not steps:
|
|
375
|
-
return " No steps yet"
|
|
376
|
-
|
|
377
|
-
lines = []
|
|
378
|
-
for step in steps:
|
|
379
|
-
icon = ICON_DELEGATE if step.get("is_delegate") else ICON_TOOL_STEP
|
|
380
|
-
duration = step.get("duration")
|
|
381
|
-
duration_str = f" [{duration}]" if duration else ""
|
|
382
|
-
status = " ✓" if step.get("finished") else ""
|
|
383
|
-
title = step.get("title") or step.get("name") or "Step"
|
|
384
|
-
lines.append(f" {icon} {title}{duration_str}{status}")
|
|
385
|
-
return "\n".join(lines)
|
|
386
|
-
|
|
387
|
-
@staticmethod
|
|
388
|
-
def _extract_event_time(event: dict[str, Any]) -> float | None:
|
|
389
|
-
metadata = event.get("metadata") or {}
|
|
390
|
-
time_value = metadata.get("time")
|
|
391
|
-
try:
|
|
392
|
-
if isinstance(time_value, (int, float)):
|
|
393
|
-
return float(time_value)
|
|
394
|
-
except Exception:
|
|
395
|
-
return None
|
|
396
|
-
return None
|
|
397
|
-
|
|
398
|
-
@staticmethod
|
|
399
|
-
def _parse_received_timestamp(event: dict[str, Any]) -> datetime | None:
|
|
400
|
-
value = event.get("received_at")
|
|
401
|
-
if not value:
|
|
402
|
-
return None
|
|
403
|
-
if isinstance(value, str):
|
|
404
|
-
try:
|
|
405
|
-
normalised = value.replace("Z", "+00:00")
|
|
406
|
-
parsed = datetime.fromisoformat(normalised)
|
|
407
|
-
except ValueError:
|
|
408
|
-
return None
|
|
409
|
-
return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
|
|
410
|
-
return None
|
|
411
|
-
|
|
412
|
-
def _extract_final_duration(self) -> str | None:
|
|
413
|
-
for event in self.ctx.events:
|
|
414
|
-
metadata = event.get("metadata") or {}
|
|
415
|
-
if metadata.get("kind") == "final_response":
|
|
416
|
-
time_value = metadata.get("time")
|
|
417
|
-
try:
|
|
418
|
-
if isinstance(time_value, (int, float)):
|
|
419
|
-
return f"{float(time_value):.2f}s"
|
|
420
|
-
except Exception:
|
|
421
|
-
return None
|
|
422
|
-
return None
|
|
423
|
-
|
|
424
|
-
def _build_step_summary(self) -> list[dict[str, Any]]:
|
|
425
|
-
stored = self.ctx.meta.get("transcript_steps")
|
|
426
|
-
if isinstance(stored, list) and stored:
|
|
427
|
-
return [
|
|
428
|
-
{
|
|
429
|
-
"title": entry.get("display_name") or entry.get("name") or "Step",
|
|
430
|
-
"is_delegate": entry.get("kind") == "delegate",
|
|
431
|
-
"finished": entry.get("status") == "finished",
|
|
432
|
-
"duration": self._format_duration_from_ms(entry.get("duration_ms")),
|
|
433
|
-
}
|
|
434
|
-
for entry in stored
|
|
435
|
-
]
|
|
436
|
-
|
|
437
|
-
steps: dict[str, dict[str, Any]] = {}
|
|
438
|
-
order: list[str] = []
|
|
439
|
-
|
|
440
|
-
for event in self.ctx.events:
|
|
441
|
-
metadata = event.get("metadata") or {}
|
|
442
|
-
if not self._is_step_event(metadata):
|
|
443
|
-
continue
|
|
444
|
-
|
|
445
|
-
for name, info in self._iter_step_candidates(event, metadata):
|
|
446
|
-
step = self._ensure_step_entry(steps, order, name)
|
|
447
|
-
self._apply_step_update(step, metadata, info, event)
|
|
448
|
-
|
|
449
|
-
return [steps[name] for name in order]
|
|
450
|
-
|
|
451
|
-
def _build_tree_summary_text(self) -> Text | None:
|
|
452
|
-
"""Render hierarchical tree from captured SSE events when available."""
|
|
453
|
-
manager = StepManager()
|
|
454
|
-
processed = False
|
|
455
|
-
|
|
456
|
-
for event in self.ctx.events:
|
|
457
|
-
payload = self._coerce_step_event(event)
|
|
458
|
-
if not payload:
|
|
459
|
-
continue
|
|
460
|
-
try:
|
|
461
|
-
manager.apply_event(payload)
|
|
462
|
-
processed = True
|
|
463
|
-
except ValueError:
|
|
464
|
-
continue
|
|
465
|
-
|
|
466
|
-
if not processed or not manager.order:
|
|
467
|
-
return None
|
|
468
|
-
|
|
469
|
-
lines: list[str] = []
|
|
470
|
-
roots = manager.order
|
|
471
|
-
total_roots = len(roots)
|
|
472
|
-
for index, root_id in enumerate(roots):
|
|
473
|
-
self._render_tree_branch(
|
|
474
|
-
manager=manager,
|
|
475
|
-
step_id=root_id,
|
|
476
|
-
ancestor_state=(),
|
|
477
|
-
is_last=index == total_roots - 1,
|
|
478
|
-
lines=lines,
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
if not lines:
|
|
482
|
-
return None
|
|
483
|
-
|
|
484
|
-
self._decorate_root_presentation(manager, roots[0], lines)
|
|
485
|
-
|
|
486
|
-
return Text("\n".join(lines), style="dim")
|
|
487
|
-
|
|
488
|
-
def _render_tree_branch(
|
|
489
|
-
self,
|
|
490
|
-
*,
|
|
491
|
-
manager: StepManager,
|
|
492
|
-
step_id: str,
|
|
493
|
-
ancestor_state: tuple[bool, ...],
|
|
494
|
-
is_last: bool,
|
|
495
|
-
lines: list[str],
|
|
496
|
-
) -> None:
|
|
497
|
-
step = manager.by_id.get(step_id)
|
|
498
|
-
if not step:
|
|
499
|
-
return
|
|
500
|
-
suppress = self._should_hide_step(step)
|
|
501
|
-
children = manager.children.get(step_id, [])
|
|
502
|
-
|
|
503
|
-
if not suppress:
|
|
504
|
-
branch_state = ancestor_state
|
|
505
|
-
if branch_state:
|
|
506
|
-
branch_state = branch_state + (is_last,)
|
|
507
|
-
lines.append(self._format_tree_line(step, branch_state))
|
|
508
|
-
next_ancestor_state = ancestor_state + (is_last,)
|
|
509
|
-
else:
|
|
510
|
-
next_ancestor_state = ancestor_state
|
|
511
|
-
|
|
512
|
-
if not children:
|
|
513
|
-
return
|
|
514
|
-
|
|
515
|
-
total_children = len(children)
|
|
516
|
-
for idx, child_id in enumerate(children):
|
|
517
|
-
self._render_tree_branch(
|
|
518
|
-
manager=manager,
|
|
519
|
-
step_id=child_id,
|
|
520
|
-
ancestor_state=next_ancestor_state if not suppress else ancestor_state,
|
|
521
|
-
is_last=idx == total_children - 1,
|
|
522
|
-
lines=lines,
|
|
523
|
-
)
|
|
524
|
-
|
|
525
|
-
def _should_hide_step(self, step: Any) -> bool:
|
|
526
|
-
if getattr(step, "parent_id", None) is None:
|
|
527
|
-
return False
|
|
528
|
-
name = getattr(step, "name", "") or ""
|
|
529
|
-
return self._looks_like_uuid(name)
|
|
530
|
-
|
|
531
|
-
def _decorate_root_presentation(
|
|
532
|
-
self,
|
|
533
|
-
manager: StepManager,
|
|
534
|
-
root_id: str,
|
|
535
|
-
lines: list[str],
|
|
536
|
-
) -> None:
|
|
537
|
-
if not lines:
|
|
538
|
-
return
|
|
539
|
-
|
|
540
|
-
root_step = manager.by_id.get(root_id)
|
|
541
|
-
if not root_step:
|
|
542
|
-
return
|
|
543
|
-
|
|
544
|
-
original_label = getattr(root_step, "display_label", None)
|
|
545
|
-
root_step.display_label = self._friendly_root_label(root_step, original_label)
|
|
546
|
-
lines[0] = self._format_tree_line(root_step, ())
|
|
547
|
-
if original_label is not None:
|
|
548
|
-
root_step.display_label = original_label
|
|
549
|
-
|
|
550
|
-
query = self._get_user_query()
|
|
551
|
-
if query:
|
|
552
|
-
lines.insert(1, f" {query}")
|
|
553
|
-
|
|
554
|
-
def _coerce_step_event(self, event: dict[str, Any]) -> dict[str, Any] | None:
|
|
555
|
-
metadata = event.get("metadata")
|
|
556
|
-
if not isinstance(metadata, dict):
|
|
557
|
-
return None
|
|
558
|
-
if not isinstance(metadata.get("step_id"), str):
|
|
559
|
-
return None
|
|
560
|
-
return {
|
|
561
|
-
"metadata": metadata,
|
|
562
|
-
"status": event.get("status"),
|
|
563
|
-
"task_state": event.get("task_state"),
|
|
564
|
-
"content": event.get("content"),
|
|
565
|
-
"task_id": event.get("task_id"),
|
|
566
|
-
"context_id": event.get("context_id"),
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
def _format_tree_line(self, step: Any, branch_state: tuple[bool, ...]) -> str:
|
|
570
|
-
prefix = build_connector_prefix(branch_state)
|
|
571
|
-
raw_label = normalise_display_label(getattr(step, "display_label", None))
|
|
572
|
-
title, summary = self._split_label(raw_label)
|
|
573
|
-
line = f"{prefix}{title}"
|
|
574
|
-
|
|
575
|
-
if summary:
|
|
576
|
-
line += f" — {self._truncate_summary(summary)}"
|
|
577
|
-
|
|
578
|
-
badge = self._format_duration_badge(step)
|
|
579
|
-
if badge:
|
|
580
|
-
line += f" {badge}"
|
|
581
|
-
|
|
582
|
-
glyph = glyph_for_status(getattr(step, "status_icon", None))
|
|
583
|
-
failure_reason = getattr(step, "failure_reason", None)
|
|
584
|
-
if glyph and glyph != "spinner":
|
|
585
|
-
if failure_reason and glyph == "✗":
|
|
586
|
-
line += f" {glyph} {failure_reason}"
|
|
587
|
-
else:
|
|
588
|
-
line += f" {glyph}"
|
|
589
|
-
elif failure_reason:
|
|
590
|
-
line += f" ✗ {failure_reason}"
|
|
591
|
-
|
|
592
|
-
return line
|
|
593
|
-
|
|
594
|
-
def _friendly_root_label(self, step: Any, fallback: str | None) -> str:
|
|
595
|
-
agent_name = self.ctx.manifest_entry.get("agent_name") or (self.ctx.meta or {}).get("agent_name")
|
|
596
|
-
agent_id = self.ctx.manifest_entry.get("agent_id") or getattr(step, "name", "")
|
|
597
|
-
|
|
598
|
-
if not agent_name:
|
|
599
|
-
return fallback or agent_id or ICON_AGENT
|
|
600
|
-
|
|
601
|
-
parts = [ICON_AGENT, agent_name]
|
|
602
|
-
if agent_id and agent_id != agent_name:
|
|
603
|
-
parts.append(f"({agent_id})")
|
|
604
|
-
return " ".join(parts)
|
|
605
|
-
|
|
606
|
-
@staticmethod
|
|
607
|
-
def _format_duration_badge(step: Any) -> str | None:
|
|
608
|
-
duration_ms = getattr(step, "duration_ms", None)
|
|
609
|
-
if duration_ms is None:
|
|
610
|
-
return None
|
|
611
|
-
try:
|
|
612
|
-
duration_ms = int(duration_ms)
|
|
613
|
-
except Exception:
|
|
614
|
-
return None
|
|
615
|
-
|
|
616
|
-
if duration_ms <= 0:
|
|
617
|
-
payload = "<1ms"
|
|
618
|
-
elif duration_ms >= 1000:
|
|
619
|
-
payload = f"{duration_ms / 1000:.2f}s"
|
|
620
|
-
else:
|
|
621
|
-
payload = f"{duration_ms}ms"
|
|
622
|
-
|
|
623
|
-
return f"[{payload}]"
|
|
624
|
-
|
|
625
|
-
@staticmethod
|
|
626
|
-
def _split_label(label: str) -> tuple[str, str | None]:
|
|
627
|
-
if " — " in label:
|
|
628
|
-
title, summary = label.split(" — ", 1)
|
|
629
|
-
return title.strip(), summary.strip()
|
|
630
|
-
return label.strip(), None
|
|
631
|
-
|
|
632
|
-
@staticmethod
|
|
633
|
-
def _truncate_summary(summary: str, limit: int = 48) -> str:
|
|
634
|
-
summary = summary.strip()
|
|
635
|
-
if len(summary) <= limit:
|
|
636
|
-
return summary
|
|
637
|
-
return summary[: limit - 1].rstrip() + "…"
|
|
638
|
-
|
|
639
|
-
@staticmethod
|
|
640
|
-
def _looks_like_uuid(value: str) -> bool:
|
|
641
|
-
stripped = value.replace("-", "").replace(" ", "")
|
|
642
|
-
if len(stripped) not in {32, 36}:
|
|
643
|
-
return False
|
|
644
|
-
return all(ch in "0123456789abcdefABCDEF" for ch in stripped)
|
|
645
|
-
|
|
646
|
-
@staticmethod
|
|
647
|
-
def _format_duration_from_ms(value: Any) -> str | None:
|
|
648
|
-
try:
|
|
649
|
-
if value is None:
|
|
650
|
-
return None
|
|
651
|
-
duration_ms = float(value)
|
|
652
|
-
except Exception:
|
|
653
|
-
return None
|
|
654
|
-
|
|
655
|
-
if duration_ms <= 0:
|
|
656
|
-
return "<1ms"
|
|
657
|
-
if duration_ms < 1000:
|
|
658
|
-
return f"{int(duration_ms)}ms"
|
|
659
|
-
return f"{duration_ms / 1000:.2f}s"
|
|
660
|
-
|
|
661
|
-
@staticmethod
|
|
662
|
-
def _is_step_event(metadata: dict[str, Any]) -> bool:
|
|
663
|
-
kind = metadata.get("kind")
|
|
664
|
-
return kind in {"agent_step", "agent_thinking_step"}
|
|
665
|
-
|
|
666
|
-
def _iter_step_candidates(
|
|
667
|
-
self, event: dict[str, Any], metadata: dict[str, Any]
|
|
668
|
-
) -> Iterable[tuple[str, dict[str, Any]]]:
|
|
669
|
-
tool_info = metadata.get("tool_info") or {}
|
|
670
|
-
|
|
671
|
-
yielded = False
|
|
672
|
-
for candidate in self._iter_tool_call_candidates(tool_info):
|
|
673
|
-
yielded = True
|
|
674
|
-
yield candidate
|
|
675
|
-
|
|
676
|
-
if yielded:
|
|
677
|
-
return
|
|
678
|
-
|
|
679
|
-
direct_tool = self._extract_direct_tool(tool_info)
|
|
680
|
-
if direct_tool is not None:
|
|
681
|
-
yield direct_tool
|
|
682
|
-
return
|
|
683
|
-
|
|
684
|
-
completed = self._extract_completed_name(event)
|
|
685
|
-
if completed is not None:
|
|
686
|
-
yield completed, {}
|
|
687
|
-
|
|
688
|
-
@staticmethod
|
|
689
|
-
def _iter_tool_call_candidates(
|
|
690
|
-
tool_info: dict[str, Any],
|
|
691
|
-
) -> Iterable[tuple[str, dict[str, Any]]]:
|
|
692
|
-
tool_calls = tool_info.get("tool_calls")
|
|
693
|
-
if isinstance(tool_calls, list):
|
|
694
|
-
for call in tool_calls:
|
|
695
|
-
name = call.get("name")
|
|
696
|
-
if name:
|
|
697
|
-
yield name, call
|
|
698
|
-
|
|
699
270
|
@staticmethod
|
|
700
271
|
def _extract_direct_tool(
|
|
701
272
|
tool_info: dict[str, Any],
|
|
702
273
|
) -> tuple[str, dict[str, Any]] | None:
|
|
274
|
+
"""Extract direct tool from tool_info.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
tool_info: Tool info dictionary.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Tuple of (tool_name, tool_info) or None.
|
|
281
|
+
"""
|
|
703
282
|
if isinstance(tool_info, dict):
|
|
704
283
|
name = tool_info.get("name")
|
|
705
284
|
if name:
|
|
@@ -708,6 +287,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
708
287
|
|
|
709
288
|
@staticmethod
|
|
710
289
|
def _extract_completed_name(event: dict[str, Any]) -> str | None:
|
|
290
|
+
"""Extract completed tool name from event content.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
event: Event dictionary.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Tool name or None.
|
|
297
|
+
"""
|
|
711
298
|
content = event.get("content") or ""
|
|
712
299
|
if isinstance(content, str) and content.startswith("Completed "):
|
|
713
300
|
name = content.replace("Completed ", "").strip()
|
|
@@ -721,6 +308,16 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
721
308
|
order: list[str],
|
|
722
309
|
name: str,
|
|
723
310
|
) -> dict[str, Any]:
|
|
311
|
+
"""Ensure step entry exists, creating if needed.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
steps: Steps dictionary.
|
|
315
|
+
order: Order list.
|
|
316
|
+
name: Step name.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Step dictionary.
|
|
320
|
+
"""
|
|
724
321
|
if name not in steps:
|
|
725
322
|
steps[name] = {
|
|
726
323
|
"name": name,
|
|
@@ -740,6 +337,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
740
337
|
info: dict[str, Any],
|
|
741
338
|
event: dict[str, Any],
|
|
742
339
|
) -> None:
|
|
340
|
+
"""Apply update to step from event metadata.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
step: Step dictionary to update.
|
|
344
|
+
metadata: Event metadata.
|
|
345
|
+
info: Step info dictionary.
|
|
346
|
+
event: Event dictionary.
|
|
347
|
+
"""
|
|
743
348
|
status = metadata.get("status")
|
|
744
349
|
event_time = metadata.get("time")
|
|
745
350
|
|
|
@@ -756,46 +361,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
756
361
|
if duration is not None:
|
|
757
362
|
step["duration"] = duration
|
|
758
363
|
|
|
759
|
-
@staticmethod
|
|
760
|
-
def _is_step_finished(metadata: dict[str, Any], event: dict[str, Any]) -> bool:
|
|
761
|
-
status = metadata.get("status")
|
|
762
|
-
return status == "finished" or bool(event.get("final"))
|
|
763
|
-
|
|
764
|
-
def _compute_step_duration(
|
|
765
|
-
self, step: dict[str, Any], info: dict[str, Any], metadata: dict[str, Any]
|
|
766
|
-
) -> str | None:
|
|
767
|
-
"""Calculate a formatted duration string for a step if possible."""
|
|
768
|
-
event_time = metadata.get("time")
|
|
769
|
-
started_at = step.get("started_at")
|
|
770
|
-
duration_value: float | None = None
|
|
771
|
-
|
|
772
|
-
if isinstance(event_time, (int, float)) and isinstance(started_at, (int, float)):
|
|
773
|
-
try:
|
|
774
|
-
delta = float(event_time) - float(started_at)
|
|
775
|
-
if delta >= 0:
|
|
776
|
-
duration_value = delta
|
|
777
|
-
except Exception:
|
|
778
|
-
duration_value = None
|
|
779
|
-
|
|
780
|
-
if duration_value is None:
|
|
781
|
-
exec_time = info.get("execution_time")
|
|
782
|
-
if isinstance(exec_time, (int, float)):
|
|
783
|
-
duration_value = float(exec_time)
|
|
784
|
-
|
|
785
|
-
if duration_value is None:
|
|
786
|
-
return None
|
|
787
|
-
|
|
788
|
-
try:
|
|
789
|
-
return format_elapsed_time(duration_value)
|
|
790
|
-
except Exception:
|
|
791
|
-
return None
|
|
792
|
-
|
|
793
364
|
|
|
794
365
|
def run_viewer_session(
|
|
795
366
|
console: Console,
|
|
796
367
|
ctx: ViewerContext,
|
|
797
368
|
export_callback: Callable[[Path], Path],
|
|
369
|
+
*,
|
|
370
|
+
initial_view: str = "default",
|
|
798
371
|
) -> None:
|
|
799
372
|
"""Entry point for creating and running the post-run viewer."""
|
|
800
|
-
viewer = PostRunViewer(console, ctx, export_callback)
|
|
373
|
+
viewer = PostRunViewer(console, ctx, export_callback, initial_view=initial_view)
|
|
801
374
|
viewer.run()
|