glaip-sdk 0.0.19__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glaip_sdk/_version.py +2 -2
- glaip_sdk/branding.py +27 -2
- glaip_sdk/cli/auth.py +93 -28
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/agents.py +127 -21
- glaip_sdk/cli/commands/configure.py +141 -90
- glaip_sdk/cli/commands/mcps.py +82 -31
- glaip_sdk/cli/commands/models.py +4 -3
- glaip_sdk/cli/commands/tools.py +27 -14
- glaip_sdk/cli/commands/update.py +66 -0
- glaip_sdk/cli/config.py +13 -2
- glaip_sdk/cli/display.py +35 -26
- glaip_sdk/cli/io.py +14 -5
- glaip_sdk/cli/main.py +185 -73
- glaip_sdk/cli/pager.py +2 -1
- glaip_sdk/cli/resolution.py +4 -1
- glaip_sdk/cli/slash/__init__.py +3 -4
- glaip_sdk/cli/slash/agent_session.py +88 -36
- glaip_sdk/cli/slash/prompt.py +20 -48
- glaip_sdk/cli/slash/session.py +437 -189
- glaip_sdk/cli/transcript/__init__.py +71 -0
- glaip_sdk/cli/transcript/cache.py +338 -0
- glaip_sdk/cli/transcript/capture.py +278 -0
- glaip_sdk/cli/transcript/export.py +38 -0
- glaip_sdk/cli/transcript/launcher.py +79 -0
- glaip_sdk/cli/transcript/viewer.py +794 -0
- glaip_sdk/cli/update_notifier.py +29 -5
- glaip_sdk/cli/utils.py +255 -74
- glaip_sdk/client/agents.py +3 -1
- glaip_sdk/client/run_rendering.py +126 -21
- glaip_sdk/icons.py +25 -0
- glaip_sdk/models.py +6 -0
- glaip_sdk/rich_components.py +29 -1
- glaip_sdk/utils/__init__.py +1 -1
- glaip_sdk/utils/client_utils.py +6 -4
- glaip_sdk/utils/display.py +61 -32
- glaip_sdk/utils/rendering/formatting.py +55 -11
- glaip_sdk/utils/rendering/models.py +15 -2
- glaip_sdk/utils/rendering/renderer/__init__.py +0 -2
- glaip_sdk/utils/rendering/renderer/base.py +1287 -227
- glaip_sdk/utils/rendering/renderer/config.py +3 -5
- glaip_sdk/utils/rendering/renderer/debug.py +73 -16
- glaip_sdk/utils/rendering/renderer/panels.py +27 -15
- glaip_sdk/utils/rendering/renderer/progress.py +61 -38
- glaip_sdk/utils/rendering/renderer/stream.py +3 -3
- glaip_sdk/utils/rendering/renderer/toggle.py +184 -0
- glaip_sdk/utils/rendering/step_tree_state.py +102 -0
- glaip_sdk/utils/rendering/steps.py +944 -16
- glaip_sdk/utils/serialization.py +5 -2
- glaip_sdk/utils/validation.py +1 -2
- {glaip_sdk-0.0.19.dist-info → glaip_sdk-0.1.0.dist-info}/METADATA +12 -1
- glaip_sdk-0.1.0.dist-info/RECORD +82 -0
- glaip_sdk/utils/rich_utils.py +0 -29
- glaip_sdk-0.0.19.dist-info/RECORD +0 -73
- {glaip_sdk-0.0.19.dist-info → glaip_sdk-0.1.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.19.dist-info → glaip_sdk-0.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -23,6 +23,58 @@ from glaip_sdk.utils.rendering.renderer import RichStreamRenderer
|
|
|
23
23
|
from glaip_sdk.utils.rendering.renderer.config import RendererConfig
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def _coerce_to_string(value: Any) -> str:
|
|
27
|
+
"""Return a best-effort string representation for transcripts."""
|
|
28
|
+
try:
|
|
29
|
+
return str(value)
|
|
30
|
+
except Exception:
|
|
31
|
+
return f"{value}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _has_visible_text(value: Any) -> bool:
|
|
35
|
+
"""Return True when the value is a non-empty string."""
|
|
36
|
+
return isinstance(value, str) and bool(value.strip())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _update_state_transcript(state: Any, text_value: str) -> bool:
|
|
40
|
+
"""Inject transcript text into renderer state if possible."""
|
|
41
|
+
if state is None:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
updated = False
|
|
45
|
+
|
|
46
|
+
if hasattr(state, "final_text") and not _has_visible_text(
|
|
47
|
+
getattr(state, "final_text", "")
|
|
48
|
+
):
|
|
49
|
+
try:
|
|
50
|
+
state.final_text = text_value
|
|
51
|
+
updated = True
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
buffer = getattr(state, "buffer", None)
|
|
56
|
+
if isinstance(buffer, list) and not any(_has_visible_text(item) for item in buffer):
|
|
57
|
+
buffer.append(text_value)
|
|
58
|
+
updated = True
|
|
59
|
+
|
|
60
|
+
return updated
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _update_renderer_transcript(renderer: Any, text_value: str) -> None:
|
|
64
|
+
"""Populate the renderer (or its state) with the supplied text."""
|
|
65
|
+
state = getattr(renderer, "state", None)
|
|
66
|
+
if _update_state_transcript(state, text_value):
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
if hasattr(renderer, "final_text") and not _has_visible_text(
|
|
70
|
+
getattr(renderer, "final_text", "")
|
|
71
|
+
):
|
|
72
|
+
try:
|
|
73
|
+
setattr(renderer, "final_text", text_value)
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
26
78
|
class AgentRunRenderingManager:
|
|
27
79
|
"""Coordinate renderer creation and streaming event handling."""
|
|
28
80
|
|
|
@@ -79,7 +131,6 @@ class AgentRunRenderingManager:
|
|
|
79
131
|
silent_config = RendererConfig(
|
|
80
132
|
live=False,
|
|
81
133
|
persist_live=False,
|
|
82
|
-
show_delegate_tool_panels=False,
|
|
83
134
|
render_thinking=False,
|
|
84
135
|
)
|
|
85
136
|
return RichStreamRenderer(
|
|
@@ -92,7 +143,6 @@ class AgentRunRenderingManager:
|
|
|
92
143
|
minimal_config = RendererConfig(
|
|
93
144
|
live=False,
|
|
94
145
|
persist_live=False,
|
|
95
|
-
show_delegate_tool_panels=False,
|
|
96
146
|
render_thinking=False,
|
|
97
147
|
)
|
|
98
148
|
return RichStreamRenderer(
|
|
@@ -106,7 +156,6 @@ class AgentRunRenderingManager:
|
|
|
106
156
|
theme="dark",
|
|
107
157
|
style="debug",
|
|
108
158
|
live=False,
|
|
109
|
-
show_delegate_tool_panels=True,
|
|
110
159
|
append_finished_snapshots=False,
|
|
111
160
|
)
|
|
112
161
|
return RichStreamRenderer(
|
|
@@ -118,7 +167,7 @@ class AgentRunRenderingManager:
|
|
|
118
167
|
def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
|
|
119
168
|
if verbose:
|
|
120
169
|
return self._create_verbose_renderer()
|
|
121
|
-
default_config = RendererConfig(
|
|
170
|
+
default_config = RendererConfig()
|
|
122
171
|
return RichStreamRenderer(console=_Console(), cfg=default_config)
|
|
123
172
|
|
|
124
173
|
# --------------------------------------------------------------------- #
|
|
@@ -139,17 +188,28 @@ class AgentRunRenderingManager:
|
|
|
139
188
|
|
|
140
189
|
self._capture_request_id(stream_response, meta, renderer)
|
|
141
190
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
191
|
+
controller = getattr(renderer, "transcript_controller", None)
|
|
192
|
+
if controller and getattr(controller, "enabled", False):
|
|
193
|
+
controller.on_stream_start(renderer)
|
|
145
194
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
195
|
+
try:
|
|
196
|
+
for event in iter_sse_events(stream_response, timeout_seconds, agent_name):
|
|
197
|
+
if started_monotonic is None:
|
|
198
|
+
started_monotonic = self._maybe_start_timer(event)
|
|
199
|
+
|
|
200
|
+
final_text, stats_usage = self._process_single_event(
|
|
201
|
+
event,
|
|
202
|
+
renderer,
|
|
203
|
+
final_text,
|
|
204
|
+
stats_usage,
|
|
205
|
+
meta,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if controller and getattr(controller, "enabled", False):
|
|
209
|
+
controller.poll(renderer)
|
|
210
|
+
finally:
|
|
211
|
+
if controller and getattr(controller, "enabled", False):
|
|
212
|
+
controller.on_stream_complete()
|
|
153
213
|
|
|
154
214
|
finished_monotonic = monotonic()
|
|
155
215
|
return final_text, stats_usage, started_monotonic, finished_monotonic
|
|
@@ -194,19 +254,50 @@ class AgentRunRenderingManager:
|
|
|
194
254
|
kind = (ev.get("metadata") or {}).get("kind")
|
|
195
255
|
renderer.on_event(ev)
|
|
196
256
|
|
|
257
|
+
handled = self._handle_metadata_kind(
|
|
258
|
+
kind,
|
|
259
|
+
ev,
|
|
260
|
+
final_text,
|
|
261
|
+
stats_usage,
|
|
262
|
+
meta,
|
|
263
|
+
renderer,
|
|
264
|
+
)
|
|
265
|
+
if handled is not None:
|
|
266
|
+
return handled
|
|
267
|
+
|
|
268
|
+
if ev.get("content"):
|
|
269
|
+
final_text = self._handle_content_event(ev, final_text)
|
|
270
|
+
|
|
271
|
+
return final_text, stats_usage
|
|
272
|
+
|
|
273
|
+
def _handle_metadata_kind(
|
|
274
|
+
self,
|
|
275
|
+
kind: str | None,
|
|
276
|
+
ev: dict[str, Any],
|
|
277
|
+
final_text: str,
|
|
278
|
+
stats_usage: dict[str, Any],
|
|
279
|
+
meta: dict[str, Any],
|
|
280
|
+
renderer: RichStreamRenderer,
|
|
281
|
+
) -> tuple[str, dict[str, Any]] | None:
|
|
282
|
+
"""Process well-known metadata kinds and return updated state."""
|
|
197
283
|
if kind == "artifact":
|
|
198
284
|
return final_text, stats_usage
|
|
199
285
|
|
|
200
|
-
if kind == "final_response"
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
286
|
+
if kind == "final_response":
|
|
287
|
+
content = ev.get("content")
|
|
288
|
+
if content:
|
|
289
|
+
return content, stats_usage
|
|
290
|
+
return final_text, stats_usage
|
|
291
|
+
|
|
292
|
+
if kind == "usage":
|
|
205
293
|
stats_usage.update(ev.get("usage") or {})
|
|
206
|
-
|
|
294
|
+
return final_text, stats_usage
|
|
295
|
+
|
|
296
|
+
if kind == "run_info":
|
|
207
297
|
self._handle_run_info_event(ev, meta, renderer)
|
|
298
|
+
return final_text, stats_usage
|
|
208
299
|
|
|
209
|
-
return
|
|
300
|
+
return None
|
|
210
301
|
|
|
211
302
|
def _handle_content_event(self, ev: dict[str, Any], final_text: str) -> str:
|
|
212
303
|
content = ev.get("content", "")
|
|
@@ -227,6 +318,16 @@ class AgentRunRenderingManager:
|
|
|
227
318
|
meta["run_id"] = ev["run_id"]
|
|
228
319
|
renderer.on_start(meta)
|
|
229
320
|
|
|
321
|
+
def _ensure_renderer_final_content(
|
|
322
|
+
self, renderer: RichStreamRenderer, text: str
|
|
323
|
+
) -> None:
|
|
324
|
+
"""Populate renderer state with final output when the stream omits it."""
|
|
325
|
+
if not text:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
text_value = _coerce_to_string(text)
|
|
329
|
+
_update_renderer_transcript(renderer, text_value)
|
|
330
|
+
|
|
230
331
|
# --------------------------------------------------------------------- #
|
|
231
332
|
# Finalisation helpers
|
|
232
333
|
# --------------------------------------------------------------------- #
|
|
@@ -258,6 +359,10 @@ class AgentRunRenderingManager:
|
|
|
258
359
|
except TypeError:
|
|
259
360
|
rendered_text = ""
|
|
260
361
|
|
|
362
|
+
fallback_text = final_text or rendered_text
|
|
363
|
+
if fallback_text:
|
|
364
|
+
self._ensure_renderer_final_content(renderer, fallback_text)
|
|
365
|
+
|
|
261
366
|
renderer.on_complete(st)
|
|
262
367
|
return final_text or rendered_text or "No response content received."
|
|
263
368
|
|
glaip_sdk/icons.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Lightweight icon definitions used across the CLI.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
ICON_AGENT = "🤖"
|
|
8
|
+
ICON_AGENT_STEP = "🤖"
|
|
9
|
+
ICON_TOOL = "🔧"
|
|
10
|
+
ICON_TOOL_STEP = "🔧"
|
|
11
|
+
ICON_DELEGATE = ICON_AGENT_STEP
|
|
12
|
+
ICON_STATUS_SUCCESS = "✓"
|
|
13
|
+
ICON_STATUS_FAILED = "✗"
|
|
14
|
+
ICON_STATUS_WARNING = "⚠"
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ICON_AGENT",
|
|
18
|
+
"ICON_AGENT_STEP",
|
|
19
|
+
"ICON_TOOL",
|
|
20
|
+
"ICON_TOOL_STEP",
|
|
21
|
+
"ICON_DELEGATE",
|
|
22
|
+
"ICON_STATUS_SUCCESS",
|
|
23
|
+
"ICON_STATUS_FAILED",
|
|
24
|
+
"ICON_STATUS_WARNING",
|
|
25
|
+
]
|
glaip_sdk/models.py
CHANGED
|
@@ -58,6 +58,9 @@ class Agent(BaseModel):
|
|
|
58
58
|
)
|
|
59
59
|
# Automatically pass the agent name for better renderer display
|
|
60
60
|
kwargs.setdefault("agent_name", self.name)
|
|
61
|
+
# Pass the agent's configured timeout if not explicitly overridden
|
|
62
|
+
if "timeout" not in kwargs:
|
|
63
|
+
kwargs["timeout"] = self.timeout
|
|
61
64
|
# Pass verbose flag through to enable event JSON output
|
|
62
65
|
return self._client.run_agent(self.id, message, verbose=verbose, **kwargs)
|
|
63
66
|
|
|
@@ -82,6 +85,9 @@ class Agent(BaseModel):
|
|
|
82
85
|
)
|
|
83
86
|
# Automatically pass the agent name for better context
|
|
84
87
|
kwargs.setdefault("agent_name", self.name)
|
|
88
|
+
# Pass the agent's configured timeout if not explicitly overridden
|
|
89
|
+
if "timeout" not in kwargs:
|
|
90
|
+
kwargs["timeout"] = self.timeout
|
|
85
91
|
|
|
86
92
|
async for chunk in self._client.arun_agent(self.id, message, **kwargs):
|
|
87
93
|
yield chunk
|
glaip_sdk/rich_components.py
CHANGED
|
@@ -38,4 +38,32 @@ class AIPTable(Table):
|
|
|
38
38
|
super().__init__(*args, **kwargs)
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
class AIPGrid(Table):
|
|
42
|
+
"""Table-based grid with GL AIP defaults for layout blocks."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
expand: bool = True,
|
|
48
|
+
padding: tuple[int, int] = (0, 1),
|
|
49
|
+
collapse_padding: bool = True,
|
|
50
|
+
):
|
|
51
|
+
"""Initialize AIPGrid with zero-edge borders and optional expansion.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
expand: Whether the grid should expand to fill available width.
|
|
55
|
+
padding: Cell padding for the grid (row, column).
|
|
56
|
+
collapse_padding: Collapse padding between renderables.
|
|
57
|
+
"""
|
|
58
|
+
super().__init__(
|
|
59
|
+
show_header=False,
|
|
60
|
+
show_edge=False,
|
|
61
|
+
pad_edge=False,
|
|
62
|
+
box=None,
|
|
63
|
+
expand=expand,
|
|
64
|
+
padding=padding,
|
|
65
|
+
collapse_padding=collapse_padding,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
__all__ = ["AIPPanel", "AIPTable", "AIPGrid"]
|
glaip_sdk/utils/__init__.py
CHANGED
|
@@ -5,6 +5,7 @@ Authors:
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from glaip_sdk.utils.display import (
|
|
8
|
+
RICH_AVAILABLE,
|
|
8
9
|
print_agent_created,
|
|
9
10
|
print_agent_deleted,
|
|
10
11
|
print_agent_output,
|
|
@@ -19,7 +20,6 @@ from glaip_sdk.utils.general import (
|
|
|
19
20
|
)
|
|
20
21
|
from glaip_sdk.utils.rendering.models import RunStats, Step
|
|
21
22
|
from glaip_sdk.utils.rendering.steps import StepManager
|
|
22
|
-
from glaip_sdk.utils.rich_utils import RICH_AVAILABLE
|
|
23
23
|
from glaip_sdk.utils.run_renderer import RichStreamRenderer
|
|
24
24
|
|
|
25
25
|
__all__ = [
|
glaip_sdk/utils/client_utils.py
CHANGED
|
@@ -17,6 +17,12 @@ from typing import Any, BinaryIO, NoReturn
|
|
|
17
17
|
import httpx
|
|
18
18
|
|
|
19
19
|
from glaip_sdk.exceptions import AgentTimeoutError
|
|
20
|
+
from glaip_sdk.utils.resource_refs import (
|
|
21
|
+
extract_ids as extract_ids_new,
|
|
22
|
+
)
|
|
23
|
+
from glaip_sdk.utils.resource_refs import (
|
|
24
|
+
find_by_name as find_by_name_new,
|
|
25
|
+
)
|
|
20
26
|
|
|
21
27
|
# Set up module-level logger
|
|
22
28
|
logger = logging.getLogger("glaip_sdk.client_utils")
|
|
@@ -77,8 +83,6 @@ def extract_ids(items: list[str | Any] | None) -> list[str] | None:
|
|
|
77
83
|
This function maintains backward compatibility by returning None for empty input.
|
|
78
84
|
New code should use glaip_sdk.utils.resource_refs.extract_ids which returns [].
|
|
79
85
|
"""
|
|
80
|
-
from .resource_refs import extract_ids as extract_ids_new
|
|
81
|
-
|
|
82
86
|
if not items:
|
|
83
87
|
return None
|
|
84
88
|
|
|
@@ -127,8 +131,6 @@ def find_by_name(
|
|
|
127
131
|
Note:
|
|
128
132
|
This function now delegates to glaip_sdk.utils.resource_refs.find_by_name.
|
|
129
133
|
"""
|
|
130
|
-
from .resource_refs import find_by_name as find_by_name_new
|
|
131
|
-
|
|
132
134
|
return find_by_name_new(items, name, case_sensitive)
|
|
133
135
|
|
|
134
136
|
|
glaip_sdk/utils/display.py
CHANGED
|
@@ -4,9 +4,51 @@ Authors:
|
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
|
-
from glaip_sdk.
|
|
9
|
+
from glaip_sdk.branding import SUCCESS, SUCCESS_STYLE
|
|
10
|
+
from glaip_sdk.icons import ICON_AGENT
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING: # pragma: no cover - import-time typing helpers
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
from glaip_sdk.rich_components import AIPPanel
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _check_rich_available() -> bool:
|
|
20
|
+
"""Check if Rich and our custom components can be imported."""
|
|
21
|
+
try:
|
|
22
|
+
__import__("rich.console")
|
|
23
|
+
__import__("rich.text")
|
|
24
|
+
__import__("glaip_sdk.rich_components")
|
|
25
|
+
return True
|
|
26
|
+
except Exception:
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
RICH_AVAILABLE = _check_rich_available()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _create_console() -> "Console":
|
|
34
|
+
"""Return a Console instance with lazy import to ease mocking."""
|
|
35
|
+
from rich.console import Console # Local import for test friendliness
|
|
36
|
+
|
|
37
|
+
return Console()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _create_text(*args: Any, **kwargs: Any) -> "Text":
|
|
41
|
+
"""Return a Text instance with lazy import to ease mocking."""
|
|
42
|
+
from rich.text import Text # Local import for test friendliness
|
|
43
|
+
|
|
44
|
+
return Text(*args, **kwargs)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _create_panel(*args: Any, **kwargs: Any) -> "AIPPanel":
|
|
48
|
+
"""Return an AIPPanel instance with lazy import to ease mocking."""
|
|
49
|
+
from glaip_sdk.rich_components import AIPPanel # Local import for test friendliness
|
|
50
|
+
|
|
51
|
+
return AIPPanel(*args, **kwargs)
|
|
10
52
|
|
|
11
53
|
|
|
12
54
|
def print_agent_output(output: str, title: str = "Agent Output") -> None:
|
|
@@ -17,17 +59,11 @@ def print_agent_output(output: str, title: str = "Agent Output") -> None:
|
|
|
17
59
|
title: Title for the output panel
|
|
18
60
|
"""
|
|
19
61
|
if RICH_AVAILABLE:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
from glaip_sdk.rich_components import AIPPanel
|
|
25
|
-
|
|
26
|
-
console = Console()
|
|
27
|
-
panel = AIPPanel(
|
|
28
|
-
Text(output, style="green"),
|
|
62
|
+
console = _create_console()
|
|
63
|
+
panel = _create_panel(
|
|
64
|
+
_create_text(output, style=SUCCESS),
|
|
29
65
|
title=title,
|
|
30
|
-
border_style=
|
|
66
|
+
border_style=SUCCESS,
|
|
31
67
|
)
|
|
32
68
|
console.print(panel)
|
|
33
69
|
else:
|
|
@@ -36,7 +72,7 @@ def print_agent_output(output: str, title: str = "Agent Output") -> None:
|
|
|
36
72
|
print("=" * (len(title) + 8))
|
|
37
73
|
|
|
38
74
|
|
|
39
|
-
def print_agent_created(agent: Any, title: str = "
|
|
75
|
+
def print_agent_created(agent: Any, title: str = f"{ICON_AGENT} Agent Created") -> None:
|
|
40
76
|
"""Print agent creation success with rich formatting.
|
|
41
77
|
|
|
42
78
|
Args:
|
|
@@ -44,21 +80,16 @@ def print_agent_created(agent: Any, title: str = "🤖 Agent Created") -> None:
|
|
|
44
80
|
title: Title for the output panel
|
|
45
81
|
"""
|
|
46
82
|
if RICH_AVAILABLE:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
from glaip_sdk.rich_components import AIPPanel
|
|
51
|
-
|
|
52
|
-
console = Console()
|
|
53
|
-
panel = AIPPanel(
|
|
54
|
-
f"[green]✅ Agent '{agent.name}' created successfully![/green]\n\n"
|
|
83
|
+
console = _create_console()
|
|
84
|
+
panel = _create_panel(
|
|
85
|
+
f"[{SUCCESS_STYLE}]✅ Agent '{agent.name}' created successfully![/]\n\n"
|
|
55
86
|
f"ID: {agent.id}\n"
|
|
56
87
|
f"Model: {getattr(agent, 'model', 'N/A')}\n"
|
|
57
88
|
f"Type: {getattr(agent, 'type', 'config')}\n"
|
|
58
89
|
f"Framework: {getattr(agent, 'framework', 'langchain')}\n"
|
|
59
90
|
f"Version: {getattr(agent, 'version', '1.0')}",
|
|
60
91
|
title=title,
|
|
61
|
-
border_style=
|
|
92
|
+
border_style=SUCCESS,
|
|
62
93
|
)
|
|
63
94
|
console.print(panel)
|
|
64
95
|
else:
|
|
@@ -77,11 +108,10 @@ def print_agent_updated(agent: Any) -> None:
|
|
|
77
108
|
agent: The updated agent object
|
|
78
109
|
"""
|
|
79
110
|
if RICH_AVAILABLE:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
console.print(f"[green]✅ Agent '{agent.name}' updated successfully[/green]")
|
|
111
|
+
console = _create_console()
|
|
112
|
+
console.print(
|
|
113
|
+
f"[{SUCCESS_STYLE}]✅ Agent '{agent.name}' updated successfully[/]"
|
|
114
|
+
)
|
|
85
115
|
else:
|
|
86
116
|
print(f"✅ Agent '{agent.name}' updated successfully")
|
|
87
117
|
|
|
@@ -93,10 +123,9 @@ def print_agent_deleted(agent_id: str) -> None:
|
|
|
93
123
|
agent_id: The deleted agent's ID
|
|
94
124
|
"""
|
|
95
125
|
if RICH_AVAILABLE:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
console.print(f"[green]✅ Agent deleted successfully (ID: {agent_id})[/green]")
|
|
126
|
+
console = _create_console()
|
|
127
|
+
console.print(
|
|
128
|
+
f"[{SUCCESS_STYLE}]✅ Agent deleted successfully (ID: {agent_id})[/]"
|
|
129
|
+
)
|
|
101
130
|
else:
|
|
102
131
|
print(f"✅ Agent deleted successfully (ID: {agent_id})")
|
|
@@ -6,11 +6,21 @@ Authors:
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import json
|
|
9
10
|
import re
|
|
10
11
|
import time
|
|
11
12
|
from collections.abc import Callable
|
|
12
13
|
from typing import Any
|
|
13
14
|
|
|
15
|
+
from glaip_sdk.icons import (
|
|
16
|
+
ICON_AGENT_STEP,
|
|
17
|
+
ICON_DELEGATE,
|
|
18
|
+
ICON_STATUS_FAILED,
|
|
19
|
+
ICON_STATUS_SUCCESS,
|
|
20
|
+
ICON_STATUS_WARNING,
|
|
21
|
+
ICON_TOOL_STEP,
|
|
22
|
+
)
|
|
23
|
+
|
|
14
24
|
# Constants for argument formatting
|
|
15
25
|
DEFAULT_ARGS_MAX_LEN = 100
|
|
16
26
|
IMPORTANT_PARAMETER_KEYS = [
|
|
@@ -34,9 +44,20 @@ SECRET_VALUE_PATTERNS = [
|
|
|
34
44
|
re.compile(r"eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+"), # JWT tokens
|
|
35
45
|
]
|
|
36
46
|
SENSITIVE_PATTERNS = re.compile(
|
|
37
|
-
r"password
|
|
47
|
+
r"(?:password|secret|token|key|api_key)(?:\s*[:=]\s*[^\s,}]+)?",
|
|
38
48
|
re.IGNORECASE,
|
|
39
49
|
)
|
|
50
|
+
CONNECTOR_VERTICAL = "│ "
|
|
51
|
+
CONNECTOR_EMPTY = " "
|
|
52
|
+
CONNECTOR_BRANCH = "├─ "
|
|
53
|
+
CONNECTOR_LAST = "└─ "
|
|
54
|
+
ROOT_MARKER = ""
|
|
55
|
+
SECRET_MASK = "••••••"
|
|
56
|
+
STATUS_GLYPHS = {
|
|
57
|
+
"success": ICON_STATUS_SUCCESS,
|
|
58
|
+
"failed": ICON_STATUS_FAILED,
|
|
59
|
+
"warning": ICON_STATUS_WARNING,
|
|
60
|
+
}
|
|
40
61
|
|
|
41
62
|
|
|
42
63
|
def _truncate_string(s: str, max_len: int) -> str:
|
|
@@ -50,7 +71,7 @@ def mask_secrets_in_string(text: str) -> str:
|
|
|
50
71
|
"""Mask sensitive information in a string."""
|
|
51
72
|
result = text
|
|
52
73
|
for pattern in SECRET_VALUE_PATTERNS:
|
|
53
|
-
result = re.sub(pattern,
|
|
74
|
+
result = re.sub(pattern, SECRET_MASK, result)
|
|
54
75
|
return result
|
|
55
76
|
|
|
56
77
|
|
|
@@ -71,7 +92,7 @@ def _redact_dict_values(text: dict) -> dict:
|
|
|
71
92
|
result = {}
|
|
72
93
|
for key, value in text.items():
|
|
73
94
|
if _is_sensitive_key(key):
|
|
74
|
-
result[key] =
|
|
95
|
+
result[key] = SECRET_MASK
|
|
75
96
|
elif _should_recurse_redaction(value):
|
|
76
97
|
result[key] = redact_sensitive(value)
|
|
77
98
|
else:
|
|
@@ -89,11 +110,11 @@ def _redact_string_content(text: str) -> str:
|
|
|
89
110
|
result = text
|
|
90
111
|
# First mask secrets
|
|
91
112
|
for pattern in SECRET_VALUE_PATTERNS:
|
|
92
|
-
result = re.sub(pattern,
|
|
113
|
+
result = re.sub(pattern, SECRET_MASK, result)
|
|
93
114
|
# Then redact sensitive patterns
|
|
94
115
|
result = re.sub(
|
|
95
116
|
SENSITIVE_PATTERNS,
|
|
96
|
-
lambda m: m.group(0).split("=")[0] + "
|
|
117
|
+
lambda m: m.group(0).split("=")[0] + "=" + SECRET_MASK,
|
|
97
118
|
result,
|
|
98
119
|
)
|
|
99
120
|
return result
|
|
@@ -113,6 +134,31 @@ def _should_recurse_redaction(value: Any) -> bool:
|
|
|
113
134
|
return isinstance(value, dict | list) or isinstance(value, str)
|
|
114
135
|
|
|
115
136
|
|
|
137
|
+
def glyph_for_status(icon_key: str | None) -> str | None:
|
|
138
|
+
"""Return glyph representing a step status icon key."""
|
|
139
|
+
if not icon_key:
|
|
140
|
+
return None
|
|
141
|
+
return STATUS_GLYPHS.get(icon_key)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def normalise_display_label(label: str | None) -> str:
|
|
145
|
+
"""Return a user facing label or the Unknown fallback."""
|
|
146
|
+
label = (label or "").strip()
|
|
147
|
+
return label or "Unknown step detail"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def build_connector_prefix(branch_state: tuple[bool, ...]) -> str:
|
|
151
|
+
"""Build connector prefix for a tree line based on ancestry state."""
|
|
152
|
+
if not branch_state:
|
|
153
|
+
return ROOT_MARKER
|
|
154
|
+
|
|
155
|
+
parts: list[str] = []
|
|
156
|
+
for ancestor_is_last in branch_state[:-1]:
|
|
157
|
+
parts.append(CONNECTOR_EMPTY if ancestor_is_last else CONNECTOR_VERTICAL)
|
|
158
|
+
parts.append(CONNECTOR_LAST if branch_state[-1] else CONNECTOR_BRANCH)
|
|
159
|
+
return "".join(parts)
|
|
160
|
+
|
|
161
|
+
|
|
116
162
|
def pretty_args(args: dict | None, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
|
|
117
163
|
"""Format arguments in a pretty way."""
|
|
118
164
|
if not args:
|
|
@@ -127,11 +173,9 @@ def pretty_args(args: dict | None, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
|
|
|
127
173
|
|
|
128
174
|
# Convert to JSON string and truncate if needed
|
|
129
175
|
try:
|
|
130
|
-
import json
|
|
131
|
-
|
|
132
176
|
args_str = json.dumps(masked_args, ensure_ascii=False, separators=(",", ":"))
|
|
133
177
|
return _truncate_string(args_str, max_len)
|
|
134
|
-
except
|
|
178
|
+
except Exception:
|
|
135
179
|
# Fallback to string representation if JSON serialization fails
|
|
136
180
|
args_str = str(masked_args)
|
|
137
181
|
return _truncate_string(args_str, max_len)
|
|
@@ -174,11 +218,11 @@ def get_spinner_char() -> str:
|
|
|
174
218
|
def get_step_icon(step_kind: str) -> str:
|
|
175
219
|
"""Get the appropriate icon for a step kind."""
|
|
176
220
|
if step_kind == "tool":
|
|
177
|
-
return
|
|
221
|
+
return ICON_TOOL_STEP
|
|
178
222
|
if step_kind == "delegate":
|
|
179
|
-
return
|
|
223
|
+
return ICON_DELEGATE
|
|
180
224
|
if step_kind == "agent":
|
|
181
|
-
return
|
|
225
|
+
return ICON_AGENT_STEP
|
|
182
226
|
return ""
|
|
183
227
|
|
|
184
228
|
|
|
@@ -30,19 +30,32 @@ class Step:
|
|
|
30
30
|
context_id: str | None = None
|
|
31
31
|
started_at: float = field(default_factory=monotonic)
|
|
32
32
|
duration_ms: int | None = None
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
duration_source: str | None = None
|
|
34
|
+
display_label: str | None = None
|
|
35
|
+
status_icon: str | None = None
|
|
36
|
+
failure_reason: str | None = None
|
|
37
|
+
branch_failed: bool = False
|
|
38
|
+
is_parallel: bool = False
|
|
39
|
+
server_started_at: float | None = None
|
|
40
|
+
server_finished_at: float | None = None
|
|
41
|
+
duration_unknown: bool = False
|
|
42
|
+
|
|
43
|
+
def finish(self, duration_raw: float | None, *, source: str | None = None) -> None:
|
|
35
44
|
"""Mark the step as finished and calculate duration.
|
|
36
45
|
|
|
37
46
|
Args:
|
|
38
47
|
duration_raw: Raw duration in seconds, or None to calculate from started_at
|
|
48
|
+
source: Optional duration source tag
|
|
39
49
|
"""
|
|
50
|
+
self.duration_unknown = False
|
|
40
51
|
if isinstance(duration_raw, int | float) and duration_raw > 0:
|
|
41
52
|
# Use provided duration if it's a positive number (even if very small)
|
|
42
53
|
self.duration_ms = round(float(duration_raw) * 1000)
|
|
54
|
+
self.duration_source = source or self.duration_source or "provided"
|
|
43
55
|
else:
|
|
44
56
|
# Calculate from started_at if duration_raw is None, negative, or zero
|
|
45
57
|
self.duration_ms = int((monotonic() - self.started_at) * 1000)
|
|
58
|
+
self.duration_source = source or self.duration_source or "monotonic"
|
|
46
59
|
self.status = "finished"
|
|
47
60
|
|
|
48
61
|
|
|
@@ -35,7 +35,6 @@ def make_silent_renderer() -> RichStreamRenderer:
|
|
|
35
35
|
cfg = RendererConfig(
|
|
36
36
|
live=False,
|
|
37
37
|
persist_live=False,
|
|
38
|
-
show_delegate_tool_panels=False,
|
|
39
38
|
render_thinking=False,
|
|
40
39
|
)
|
|
41
40
|
return RichStreamRenderer(
|
|
@@ -51,7 +50,6 @@ def make_minimal_renderer() -> RichStreamRenderer:
|
|
|
51
50
|
cfg = RendererConfig(
|
|
52
51
|
live=False,
|
|
53
52
|
persist_live=False,
|
|
54
|
-
show_delegate_tool_panels=False,
|
|
55
53
|
render_thinking=False,
|
|
56
54
|
)
|
|
57
55
|
return RichStreamRenderer(console=Console(), cfg=cfg)
|