glaip-sdk 0.0.20__py3-none-any.whl → 0.6.5b6__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 +10 -3
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1126 -0
- glaip_sdk/branding.py +15 -6
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +265 -45
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +270 -173
- glaip_sdk/cli/commands/common_config.py +101 -0
- glaip_sdk/cli/commands/configure.py +735 -143
- glaip_sdk/cli/commands/mcps.py +265 -134
- glaip_sdk/cli/commands/models.py +13 -9
- glaip_sdk/cli/commands/tools.py +67 -88
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/commands/update.py +3 -8
- glaip_sdk/cli/config.py +49 -7
- 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 +45 -32
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +14 -17
- glaip_sdk/cli/main.py +232 -143
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/mcp_validators.py +5 -15
- glaip_sdk/cli/pager.py +12 -19
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/parsers/json_input.py +11 -22
- glaip_sdk/cli/resolution.py +3 -9
- glaip_sdk/cli/rich_helpers.py +1 -3
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +500 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +61 -28
- glaip_sdk/cli/slash/prompt.py +13 -10
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +772 -222
- 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 +872 -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 +258 -60
- glaip_sdk/cli/transcript/capture.py +72 -21
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +1 -3
- glaip_sdk/cli/transcript/viewer.py +77 -329
- glaip_sdk/cli/update_notifier.py +177 -24
- glaip_sdk/cli/utils.py +242 -1309
- glaip_sdk/cli/validators.py +16 -18
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +53 -37
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +320 -92
- glaip_sdk/client/base.py +78 -35
- glaip_sdk/client/main.py +19 -10
- glaip_sdk/client/mcps.py +123 -15
- glaip_sdk/client/run_rendering.py +218 -78
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +161 -34
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/exceptions.py +1 -3
- glaip_sdk/icons.py +9 -3
- 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/payload_schemas/agent.py +1 -3
- 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 +231 -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 +597 -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 +158 -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 +177 -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/agent_config.py +4 -14
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +46 -28
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +25 -21
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +1 -36
- glaip_sdk/utils/import_export.py +15 -16
- 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 +38 -23
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +18 -8
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
- glaip_sdk/utils/rendering/renderer/base.py +476 -882
- glaip_sdk/utils/rendering/renderer/config.py +4 -10
- glaip_sdk/utils/rendering/renderer/debug.py +30 -34
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +13 -54
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/toggle.py +182 -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/step_tree_state.py +100 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/{steps.py → steps/manager.py} +122 -26
- 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 +29 -26
- glaip_sdk/utils/runtime_config.py +422 -0
- glaip_sdk/utils/serialization.py +32 -46
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +20 -28
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.6.5b6.dist-info}/METADATA +49 -4
- glaip_sdk-0.6.5b6.dist-info/RECORD +159 -0
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.6.5b6.dist-info}/WHEEL +1 -1
- glaip_sdk/models.py +0 -259
- glaip_sdk-0.0.20.dist-info/RECORD +0 -80
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.6.5b6.dist-info}/entry_points.txt +0 -0
|
@@ -20,9 +20,7 @@ from glaip_sdk.cli.transcript.capture import StoredTranscriptContext
|
|
|
20
20
|
from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def should_launch_post_run_viewer(
|
|
24
|
-
ctx: Any, console: Console, *, slash_mode: bool
|
|
25
|
-
) -> bool:
|
|
23
|
+
def should_launch_post_run_viewer(ctx: Any, console: Console, *, slash_mode: bool) -> bool:
|
|
26
24
|
"""Return True if the viewer should open automatically."""
|
|
27
25
|
if slash_mode:
|
|
28
26
|
return False
|
|
@@ -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,28 +21,20 @@ 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
|
-
|
|
34
|
-
|
|
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.viewer import (
|
|
27
|
+
ViewerContext as PresenterViewerContext,
|
|
28
|
+
prepare_viewer_snapshot as presenter_prepare_viewer_snapshot,
|
|
29
|
+
render_post_run_view as presenter_render_post_run_view,
|
|
30
|
+
render_transcript_events as presenter_render_transcript_events,
|
|
31
|
+
render_transcript_view as presenter_render_transcript_view,
|
|
35
32
|
)
|
|
36
33
|
|
|
37
34
|
EXPORT_CANCELLED_MESSAGE = "[dim]Export cancelled.[/dim]"
|
|
38
35
|
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
class ViewerContext:
|
|
42
|
-
"""Runtime context for the viewer session."""
|
|
43
|
-
|
|
44
|
-
manifest_entry: dict[str, Any]
|
|
45
|
-
events: list[dict[str, Any]]
|
|
46
|
-
default_output: str
|
|
47
|
-
final_output: str
|
|
48
|
-
stream_started_at: float | None
|
|
49
|
-
meta: dict[str, Any]
|
|
37
|
+
ViewerContext = PresenterViewerContext
|
|
50
38
|
|
|
51
39
|
|
|
52
40
|
class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
@@ -57,18 +45,18 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
57
45
|
console: Console,
|
|
58
46
|
ctx: ViewerContext,
|
|
59
47
|
export_callback: Callable[[Path], Path],
|
|
48
|
+
*,
|
|
49
|
+
initial_view: str = "default",
|
|
60
50
|
) -> None:
|
|
61
51
|
"""Initialize viewer state for a captured transcript."""
|
|
62
52
|
self.console = console
|
|
63
53
|
self.ctx = ctx
|
|
64
54
|
self._export_callback = export_callback
|
|
65
|
-
self._view_mode = "default"
|
|
55
|
+
self._view_mode = initial_view if initial_view in {"default", "transcript"} else "default"
|
|
66
56
|
|
|
67
57
|
def run(self) -> None:
|
|
68
58
|
"""Enter the interactive loop."""
|
|
69
|
-
if not self.ctx.events and not (
|
|
70
|
-
self.ctx.default_output or self.ctx.final_output
|
|
71
|
-
):
|
|
59
|
+
if not self.ctx.events and not (self.ctx.default_output or self.ctx.final_output):
|
|
72
60
|
return
|
|
73
61
|
if self._view_mode == "transcript":
|
|
74
62
|
self._render()
|
|
@@ -79,15 +67,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
79
67
|
# Rendering helpers
|
|
80
68
|
# ------------------------------------------------------------------
|
|
81
69
|
def _render(self) -> None:
|
|
70
|
+
"""Render the transcript viewer interface."""
|
|
82
71
|
try:
|
|
83
72
|
if self.console.is_terminal:
|
|
84
73
|
self.console.clear()
|
|
85
74
|
except Exception: # pragma: no cover - platform quirks
|
|
86
75
|
pass
|
|
87
76
|
|
|
88
|
-
header = (
|
|
89
|
-
f"Agent transcript viewer · run {self.ctx.manifest_entry.get('run_id')}"
|
|
90
|
-
)
|
|
77
|
+
header = f"Agent transcript viewer · run {self.ctx.manifest_entry.get('run_id')}"
|
|
91
78
|
agent_label = self.ctx.manifest_entry.get("agent_name") or "unknown agent"
|
|
92
79
|
model = self.ctx.manifest_entry.get("model") or self.ctx.meta.get("model")
|
|
93
80
|
agent_id = self.ctx.manifest_entry.get("agent_id")
|
|
@@ -103,66 +90,18 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
103
90
|
self.console.print(f"[dim]{' · '.join(subtitle_parts)}[/]")
|
|
104
91
|
self.console.print()
|
|
105
92
|
|
|
106
|
-
query = self._get_user_query()
|
|
107
|
-
|
|
108
93
|
if self._view_mode == "default":
|
|
109
|
-
self.
|
|
94
|
+
presenter_render_post_run_view(self.console, self.ctx)
|
|
110
95
|
else:
|
|
111
|
-
self.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if query:
|
|
115
|
-
self._render_user_query(query)
|
|
116
|
-
self._render_steps_summary()
|
|
117
|
-
self._render_final_panel()
|
|
118
|
-
|
|
119
|
-
def _render_transcript_view(self, query: str | None) -> None:
|
|
120
|
-
if not self.ctx.events:
|
|
121
|
-
self.console.print("[dim]No SSE events were captured for this run.[/dim]")
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
if query:
|
|
125
|
-
self._render_user_query(query)
|
|
126
|
-
|
|
127
|
-
self._render_steps_summary()
|
|
128
|
-
self._render_final_panel()
|
|
129
|
-
|
|
130
|
-
self.console.print("[bold]Transcript Events[/bold]")
|
|
131
|
-
self.console.print(
|
|
132
|
-
"[dim]────────────────────────────────────────────────────────[/dim]"
|
|
133
|
-
)
|
|
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 = (
|
|
150
|
-
self.ctx.final_output
|
|
151
|
-
or self.ctx.default_output
|
|
152
|
-
or "No response content captured."
|
|
153
|
-
)
|
|
154
|
-
title = "Final Result"
|
|
155
|
-
duration_text = self._extract_final_duration()
|
|
156
|
-
if duration_text:
|
|
157
|
-
title += f" · {duration_text}"
|
|
158
|
-
panel = create_final_panel(content, title=title, theme="dark")
|
|
159
|
-
self.console.print(panel)
|
|
160
|
-
self.console.print()
|
|
96
|
+
snapshot, state = presenter_prepare_viewer_snapshot(self.ctx, glyphs=None)
|
|
97
|
+
presenter_render_transcript_view(self.console, snapshot)
|
|
98
|
+
presenter_render_transcript_events(self.console, state.events)
|
|
161
99
|
|
|
162
100
|
# ------------------------------------------------------------------
|
|
163
101
|
# Interaction loops
|
|
164
102
|
# ------------------------------------------------------------------
|
|
165
103
|
def _fallback_loop(self) -> None:
|
|
104
|
+
"""Fallback interaction loop for non-interactive terminals."""
|
|
166
105
|
while True:
|
|
167
106
|
try:
|
|
168
107
|
ch = click.getchar()
|
|
@@ -183,6 +122,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
183
122
|
continue
|
|
184
123
|
|
|
185
124
|
def _handle_command(self, raw: str) -> bool:
|
|
125
|
+
"""Handle a command input.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
raw: Raw command string.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
True to continue, False to exit.
|
|
132
|
+
"""
|
|
186
133
|
lowered = raw.lower()
|
|
187
134
|
if lowered in {"exit", "quit", "q"}:
|
|
188
135
|
return True
|
|
@@ -212,9 +159,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
212
159
|
raw = str(path)
|
|
213
160
|
return raw if len(raw) <= 80 else f"…{raw[-77:]}"
|
|
214
161
|
|
|
215
|
-
selection = self._prompt_export_choice(
|
|
216
|
-
default_path, _display_path(default_path)
|
|
217
|
-
)
|
|
162
|
+
selection = self._prompt_export_choice(default_path, _display_path(default_path))
|
|
218
163
|
if selection is None:
|
|
219
164
|
self._legacy_export_prompt(default_path, _display_path)
|
|
220
165
|
return
|
|
@@ -240,42 +185,12 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
240
185
|
except Exception as exc: # pragma: no cover - unexpected IO failures
|
|
241
186
|
self.console.print(f"[red]Failed to export transcript: {exc}[/red]")
|
|
242
187
|
|
|
243
|
-
def _prompt_export_choice(
|
|
244
|
-
self, default_path: Path, default_display: str
|
|
245
|
-
) -> tuple[str, Any] | None:
|
|
188
|
+
def _prompt_export_choice(self, default_path: Path, default_display: str) -> tuple[str, Any] | None:
|
|
246
189
|
"""Render interactive export menu with numeric shortcuts."""
|
|
247
|
-
if not self.console.is_terminal
|
|
248
|
-
return None
|
|
249
|
-
|
|
250
|
-
try:
|
|
251
|
-
answer = questionary.select(
|
|
252
|
-
"Export transcript",
|
|
253
|
-
choices=[
|
|
254
|
-
Choice(
|
|
255
|
-
title=f"Save to default ({default_display})",
|
|
256
|
-
value=("default", default_path),
|
|
257
|
-
shortcut_key="1",
|
|
258
|
-
),
|
|
259
|
-
Choice(
|
|
260
|
-
title="Choose a different path",
|
|
261
|
-
value=("custom", None),
|
|
262
|
-
shortcut_key="2",
|
|
263
|
-
),
|
|
264
|
-
Choice(
|
|
265
|
-
title="Cancel",
|
|
266
|
-
value=("cancel", None),
|
|
267
|
-
shortcut_key="3",
|
|
268
|
-
),
|
|
269
|
-
],
|
|
270
|
-
use_shortcuts=True,
|
|
271
|
-
instruction="Press 1-3 (or arrows) then Enter.",
|
|
272
|
-
).ask()
|
|
273
|
-
except Exception:
|
|
190
|
+
if not self.console.is_terminal:
|
|
274
191
|
return None
|
|
275
192
|
|
|
276
|
-
|
|
277
|
-
return ("cancel", None)
|
|
278
|
-
return answer
|
|
193
|
+
return prompt_export_choice_questionary(default_path, default_display)
|
|
279
194
|
|
|
280
195
|
def _prompt_custom_destination(self) -> Path | None:
|
|
281
196
|
"""Prompt for custom export path with filesystem completion."""
|
|
@@ -283,11 +198,12 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
283
198
|
return None
|
|
284
199
|
|
|
285
200
|
try:
|
|
286
|
-
|
|
201
|
+
question = questionary.path(
|
|
287
202
|
"Destination path (Tab to autocomplete):",
|
|
288
203
|
default="",
|
|
289
204
|
only_directories=False,
|
|
290
|
-
)
|
|
205
|
+
)
|
|
206
|
+
response = questionary_safe_ask(question)
|
|
291
207
|
except Exception:
|
|
292
208
|
return None
|
|
293
209
|
|
|
@@ -299,9 +215,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
299
215
|
candidate = Path.cwd() / candidate
|
|
300
216
|
return candidate
|
|
301
217
|
|
|
302
|
-
def _legacy_export_prompt(
|
|
303
|
-
self, default_path: Path, formatter: Callable[[Path], str]
|
|
304
|
-
) -> None:
|
|
218
|
+
def _legacy_export_prompt(self, default_path: Path, formatter: Callable[[Path], str]) -> None:
|
|
305
219
|
"""Fallback export workflow when interactive UI is unavailable."""
|
|
306
220
|
self.console.print("[dim]Export options (fallback mode)[/dim]")
|
|
307
221
|
self.console.print(f" 1. Save to default ({formatter(default_path)})")
|
|
@@ -347,176 +261,22 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
347
261
|
self.console.print(f"[red]Failed to export transcript: {exc}[/red]")
|
|
348
262
|
|
|
349
263
|
def _print_command_hint(self) -> None:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
)
|
|
353
|
-
self.console.print()
|
|
354
|
-
|
|
355
|
-
def _get_user_query(self) -> str | None:
|
|
356
|
-
meta = self.ctx.meta or {}
|
|
357
|
-
manifest = self.ctx.manifest_entry or {}
|
|
358
|
-
return (
|
|
359
|
-
meta.get("input_message")
|
|
360
|
-
or meta.get("query")
|
|
361
|
-
or meta.get("message")
|
|
362
|
-
or manifest.get("input_message")
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
def _render_user_query(self, query: str) -> None:
|
|
366
|
-
panel = AIPPanel(
|
|
367
|
-
Markdown(f"Query: {query}"),
|
|
368
|
-
title="User Request",
|
|
369
|
-
border_style="#d97706",
|
|
370
|
-
)
|
|
371
|
-
self.console.print(panel)
|
|
264
|
+
"""Print command hint for user interaction."""
|
|
265
|
+
self.console.print("[dim]Ctrl+T to toggle transcript · type `e` to export · press Enter to exit[/dim]")
|
|
372
266
|
self.console.print()
|
|
373
267
|
|
|
374
|
-
def _render_steps_summary(self) -> None:
|
|
375
|
-
panel_content = self._format_steps_summary(self._build_step_summary())
|
|
376
|
-
panel = AIPPanel(
|
|
377
|
-
Text(panel_content, style="dim"),
|
|
378
|
-
title="Steps",
|
|
379
|
-
border_style="blue",
|
|
380
|
-
)
|
|
381
|
-
self.console.print(panel)
|
|
382
|
-
self.console.print()
|
|
383
|
-
|
|
384
|
-
@staticmethod
|
|
385
|
-
def _format_steps_summary(steps: list[dict[str, Any]]) -> str:
|
|
386
|
-
if not steps:
|
|
387
|
-
return " No steps yet"
|
|
388
|
-
|
|
389
|
-
lines = []
|
|
390
|
-
for step in steps:
|
|
391
|
-
icon = ICON_DELEGATE if step.get("is_delegate") else ICON_TOOL_STEP
|
|
392
|
-
duration = step.get("duration")
|
|
393
|
-
duration_str = f" [{duration}]" if duration else ""
|
|
394
|
-
status = " ✓" if step.get("finished") else ""
|
|
395
|
-
title = step.get("title") or step.get("name") or "Step"
|
|
396
|
-
lines.append(f" {icon} {title}{duration_str}{status}")
|
|
397
|
-
return "\n".join(lines)
|
|
398
|
-
|
|
399
|
-
@staticmethod
|
|
400
|
-
def _extract_event_time(event: dict[str, Any]) -> float | None:
|
|
401
|
-
metadata = event.get("metadata") or {}
|
|
402
|
-
time_value = metadata.get("time")
|
|
403
|
-
try:
|
|
404
|
-
if isinstance(time_value, (int, float)):
|
|
405
|
-
return float(time_value)
|
|
406
|
-
except Exception:
|
|
407
|
-
return None
|
|
408
|
-
return None
|
|
409
|
-
|
|
410
|
-
@staticmethod
|
|
411
|
-
def _parse_received_timestamp(event: dict[str, Any]) -> datetime | None:
|
|
412
|
-
value = event.get("received_at")
|
|
413
|
-
if not value:
|
|
414
|
-
return None
|
|
415
|
-
if isinstance(value, str):
|
|
416
|
-
try:
|
|
417
|
-
normalised = value.replace("Z", "+00:00")
|
|
418
|
-
parsed = datetime.fromisoformat(normalised)
|
|
419
|
-
except ValueError:
|
|
420
|
-
return None
|
|
421
|
-
return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
|
|
422
|
-
return None
|
|
423
|
-
|
|
424
|
-
def _extract_final_duration(self) -> str | None:
|
|
425
|
-
for event in self.ctx.events:
|
|
426
|
-
metadata = event.get("metadata") or {}
|
|
427
|
-
if metadata.get("kind") == "final_response":
|
|
428
|
-
time_value = metadata.get("time")
|
|
429
|
-
try:
|
|
430
|
-
if isinstance(time_value, (int, float)):
|
|
431
|
-
return f"{float(time_value):.2f}s"
|
|
432
|
-
except Exception:
|
|
433
|
-
return None
|
|
434
|
-
return None
|
|
435
|
-
|
|
436
|
-
def _build_step_summary(self) -> list[dict[str, Any]]:
|
|
437
|
-
stored = self.ctx.meta.get("transcript_steps")
|
|
438
|
-
if isinstance(stored, list) and stored:
|
|
439
|
-
return [
|
|
440
|
-
{
|
|
441
|
-
"title": entry.get("display_name") or entry.get("name") or "Step",
|
|
442
|
-
"is_delegate": entry.get("kind") == "delegate",
|
|
443
|
-
"finished": entry.get("status") == "finished",
|
|
444
|
-
"duration": self._format_duration_from_ms(entry.get("duration_ms")),
|
|
445
|
-
}
|
|
446
|
-
for entry in stored
|
|
447
|
-
]
|
|
448
|
-
|
|
449
|
-
steps: dict[str, dict[str, Any]] = {}
|
|
450
|
-
order: list[str] = []
|
|
451
|
-
|
|
452
|
-
for event in self.ctx.events:
|
|
453
|
-
metadata = event.get("metadata") or {}
|
|
454
|
-
if not self._is_step_event(metadata):
|
|
455
|
-
continue
|
|
456
|
-
|
|
457
|
-
for name, info in self._iter_step_candidates(event, metadata):
|
|
458
|
-
step = self._ensure_step_entry(steps, order, name)
|
|
459
|
-
self._apply_step_update(step, metadata, info, event)
|
|
460
|
-
|
|
461
|
-
return [steps[name] for name in order]
|
|
462
|
-
|
|
463
|
-
@staticmethod
|
|
464
|
-
def _format_duration_from_ms(value: Any) -> str | None:
|
|
465
|
-
try:
|
|
466
|
-
if value is None:
|
|
467
|
-
return None
|
|
468
|
-
duration_ms = float(value)
|
|
469
|
-
except Exception:
|
|
470
|
-
return None
|
|
471
|
-
|
|
472
|
-
if duration_ms <= 0:
|
|
473
|
-
return "<1ms"
|
|
474
|
-
if duration_ms < 1000:
|
|
475
|
-
return f"{int(duration_ms)}ms"
|
|
476
|
-
return f"{duration_ms / 1000:.2f}s"
|
|
477
|
-
|
|
478
|
-
@staticmethod
|
|
479
|
-
def _is_step_event(metadata: dict[str, Any]) -> bool:
|
|
480
|
-
kind = metadata.get("kind")
|
|
481
|
-
return kind in {"agent_step", "agent_thinking_step"}
|
|
482
|
-
|
|
483
|
-
def _iter_step_candidates(
|
|
484
|
-
self, event: dict[str, Any], metadata: dict[str, Any]
|
|
485
|
-
) -> Iterable[tuple[str, dict[str, Any]]]:
|
|
486
|
-
tool_info = metadata.get("tool_info") or {}
|
|
487
|
-
|
|
488
|
-
yielded = False
|
|
489
|
-
for candidate in self._iter_tool_call_candidates(tool_info):
|
|
490
|
-
yielded = True
|
|
491
|
-
yield candidate
|
|
492
|
-
|
|
493
|
-
if yielded:
|
|
494
|
-
return
|
|
495
|
-
|
|
496
|
-
direct_tool = self._extract_direct_tool(tool_info)
|
|
497
|
-
if direct_tool is not None:
|
|
498
|
-
yield direct_tool
|
|
499
|
-
return
|
|
500
|
-
|
|
501
|
-
completed = self._extract_completed_name(event)
|
|
502
|
-
if completed is not None:
|
|
503
|
-
yield completed, {}
|
|
504
|
-
|
|
505
|
-
@staticmethod
|
|
506
|
-
def _iter_tool_call_candidates(
|
|
507
|
-
tool_info: dict[str, Any],
|
|
508
|
-
) -> Iterable[tuple[str, dict[str, Any]]]:
|
|
509
|
-
tool_calls = tool_info.get("tool_calls")
|
|
510
|
-
if isinstance(tool_calls, list):
|
|
511
|
-
for call in tool_calls:
|
|
512
|
-
name = call.get("name")
|
|
513
|
-
if name:
|
|
514
|
-
yield name, call
|
|
515
|
-
|
|
516
268
|
@staticmethod
|
|
517
269
|
def _extract_direct_tool(
|
|
518
270
|
tool_info: dict[str, Any],
|
|
519
271
|
) -> tuple[str, dict[str, Any]] | None:
|
|
272
|
+
"""Extract direct tool from tool_info.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
tool_info: Tool info dictionary.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Tuple of (tool_name, tool_info) or None.
|
|
279
|
+
"""
|
|
520
280
|
if isinstance(tool_info, dict):
|
|
521
281
|
name = tool_info.get("name")
|
|
522
282
|
if name:
|
|
@@ -525,6 +285,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
525
285
|
|
|
526
286
|
@staticmethod
|
|
527
287
|
def _extract_completed_name(event: dict[str, Any]) -> str | None:
|
|
288
|
+
"""Extract completed tool name from event content.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
event: Event dictionary.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Tool name or None.
|
|
295
|
+
"""
|
|
528
296
|
content = event.get("content") or ""
|
|
529
297
|
if isinstance(content, str) and content.startswith("Completed "):
|
|
530
298
|
name = content.replace("Completed ", "").strip()
|
|
@@ -538,6 +306,16 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
538
306
|
order: list[str],
|
|
539
307
|
name: str,
|
|
540
308
|
) -> dict[str, Any]:
|
|
309
|
+
"""Ensure step entry exists, creating if needed.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
steps: Steps dictionary.
|
|
313
|
+
order: Order list.
|
|
314
|
+
name: Step name.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Step dictionary.
|
|
318
|
+
"""
|
|
541
319
|
if name not in steps:
|
|
542
320
|
steps[name] = {
|
|
543
321
|
"name": name,
|
|
@@ -557,14 +335,18 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
557
335
|
info: dict[str, Any],
|
|
558
336
|
event: dict[str, Any],
|
|
559
337
|
) -> None:
|
|
338
|
+
"""Apply update to step from event metadata.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
step: Step dictionary to update.
|
|
342
|
+
metadata: Event metadata.
|
|
343
|
+
info: Step info dictionary.
|
|
344
|
+
event: Event dictionary.
|
|
345
|
+
"""
|
|
560
346
|
status = metadata.get("status")
|
|
561
347
|
event_time = metadata.get("time")
|
|
562
348
|
|
|
563
|
-
if (
|
|
564
|
-
status == "running"
|
|
565
|
-
and step.get("started_at") is None
|
|
566
|
-
and isinstance(event_time, (int, float))
|
|
567
|
-
):
|
|
349
|
+
if status == "running" and step.get("started_at") is None and isinstance(event_time, (int, float)):
|
|
568
350
|
try:
|
|
569
351
|
step["started_at"] = float(event_time)
|
|
570
352
|
except Exception:
|
|
@@ -577,48 +359,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
577
359
|
if duration is not None:
|
|
578
360
|
step["duration"] = duration
|
|
579
361
|
|
|
580
|
-
@staticmethod
|
|
581
|
-
def _is_step_finished(metadata: dict[str, Any], event: dict[str, Any]) -> bool:
|
|
582
|
-
status = metadata.get("status")
|
|
583
|
-
return status == "finished" or bool(event.get("final"))
|
|
584
|
-
|
|
585
|
-
def _compute_step_duration(
|
|
586
|
-
self, step: dict[str, Any], info: dict[str, Any], metadata: dict[str, Any]
|
|
587
|
-
) -> str | None:
|
|
588
|
-
"""Calculate a formatted duration string for a step if possible."""
|
|
589
|
-
event_time = metadata.get("time")
|
|
590
|
-
started_at = step.get("started_at")
|
|
591
|
-
duration_value: float | None = None
|
|
592
|
-
|
|
593
|
-
if isinstance(event_time, (int, float)) and isinstance(
|
|
594
|
-
started_at, (int, float)
|
|
595
|
-
):
|
|
596
|
-
try:
|
|
597
|
-
delta = float(event_time) - float(started_at)
|
|
598
|
-
if delta >= 0:
|
|
599
|
-
duration_value = delta
|
|
600
|
-
except Exception:
|
|
601
|
-
duration_value = None
|
|
602
|
-
|
|
603
|
-
if duration_value is None:
|
|
604
|
-
exec_time = info.get("execution_time")
|
|
605
|
-
if isinstance(exec_time, (int, float)):
|
|
606
|
-
duration_value = float(exec_time)
|
|
607
|
-
|
|
608
|
-
if duration_value is None:
|
|
609
|
-
return None
|
|
610
|
-
|
|
611
|
-
try:
|
|
612
|
-
return format_elapsed_time(duration_value)
|
|
613
|
-
except Exception:
|
|
614
|
-
return None
|
|
615
|
-
|
|
616
362
|
|
|
617
363
|
def run_viewer_session(
|
|
618
364
|
console: Console,
|
|
619
365
|
ctx: ViewerContext,
|
|
620
366
|
export_callback: Callable[[Path], Path],
|
|
367
|
+
*,
|
|
368
|
+
initial_view: str = "default",
|
|
621
369
|
) -> None:
|
|
622
370
|
"""Entry point for creating and running the post-run viewer."""
|
|
623
|
-
viewer = PostRunViewer(console, ctx, export_callback)
|
|
371
|
+
viewer = PostRunViewer(console, ctx, export_callback, initial_view=initial_view)
|
|
624
372
|
viewer.run()
|