glaip-sdk 0.6.15b2__py3-none-any.whl → 0.6.15b3__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/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1196 -0
- glaip_sdk/cli/__init__.py +9 -0
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +78 -0
- glaip_sdk/cli/auth.py +699 -0
- glaip_sdk/cli/commands/__init__.py +5 -0
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +1509 -0
- glaip_sdk/cli/commands/common_config.py +104 -0
- glaip_sdk/cli/commands/configure.py +896 -0
- glaip_sdk/cli/commands/mcps.py +1356 -0
- glaip_sdk/cli/commands/models.py +69 -0
- glaip_sdk/cli/commands/tools.py +576 -0
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/commands/update.py +61 -0
- glaip_sdk/cli/config.py +95 -0
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +150 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +851 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +355 -0
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +112 -0
- glaip_sdk/cli/main.py +615 -0
- glaip_sdk/cli/masking.py +136 -0
- glaip_sdk/cli/mcp_validators.py +287 -0
- glaip_sdk/cli/pager.py +266 -0
- glaip_sdk/cli/parsers/__init__.py +7 -0
- glaip_sdk/cli/parsers/json_input.py +177 -0
- glaip_sdk/cli/resolution.py +67 -0
- glaip_sdk/cli/rich_helpers.py +27 -0
- glaip_sdk/cli/slash/__init__.py +15 -0
- 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 +285 -0
- glaip_sdk/cli/slash/prompt.py +256 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +1708 -0
- glaip_sdk/cli/slash/tui/__init__.py +9 -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 +31 -0
- glaip_sdk/cli/transcript/cache.py +536 -0
- glaip_sdk/cli/transcript/capture.py +329 -0
- glaip_sdk/cli/transcript/export.py +38 -0
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +77 -0
- glaip_sdk/cli/transcript/viewer.py +374 -0
- glaip_sdk/cli/update_notifier.py +290 -0
- glaip_sdk/cli/utils.py +263 -0
- glaip_sdk/cli/validators.py +238 -0
- glaip_sdk/client/__init__.py +11 -0
- glaip_sdk/client/_agent_payloads.py +520 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +1335 -0
- glaip_sdk/client/base.py +502 -0
- glaip_sdk/client/main.py +249 -0
- glaip_sdk/client/mcps.py +370 -0
- glaip_sdk/client/run_rendering.py +700 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +661 -0
- glaip_sdk/client/validators.py +198 -0
- glaip_sdk/config/constants.py +52 -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 +7 -0
- glaip_sdk/payload_schemas/agent.py +85 -0
- 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/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +112 -0
- glaip_sdk/runner/langgraph.py +782 -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 +86 -0
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +194 -0
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +486 -0
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +135 -0
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +61 -0
- glaip_sdk/utils/import_export.py +168 -0
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -0
- glaip_sdk/utils/rendering/formatting.py +264 -0
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/layout/panels.py +156 -0
- glaip_sdk/utils/rendering/layout/progress.py +202 -0
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +85 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +55 -0
- glaip_sdk/utils/rendering/renderer/base.py +1024 -0
- glaip_sdk/utils/rendering/renderer/config.py +27 -0
- glaip_sdk/utils/rendering/renderer/console.py +55 -0
- glaip_sdk/utils/rendering/renderer/debug.py +178 -0
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +202 -0
- 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/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 +195 -0
- glaip_sdk/utils/run_renderer.py +41 -0
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +424 -0
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +264 -0
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.15b3.dist-info}/METADATA +1 -1
- glaip_sdk-0.6.15b3.dist-info/RECORD +160 -0
- glaip_sdk-0.6.15b2.dist-info/RECORD +0 -12
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.15b3.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.15b3.dist-info}/entry_points.txt +0 -0
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.15b3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Formatting helpers for renderer.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from glaip_sdk.icons import (
|
|
15
|
+
ICON_AGENT_STEP,
|
|
16
|
+
ICON_DELEGATE,
|
|
17
|
+
ICON_STATUS_FAILED,
|
|
18
|
+
ICON_STATUS_SUCCESS,
|
|
19
|
+
ICON_STATUS_WARNING,
|
|
20
|
+
ICON_TOOL_STEP,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Constants for argument formatting
|
|
24
|
+
DEFAULT_ARGS_MAX_LEN = 100
|
|
25
|
+
IMPORTANT_PARAMETER_KEYS = [
|
|
26
|
+
"model",
|
|
27
|
+
"temperature",
|
|
28
|
+
"max_tokens",
|
|
29
|
+
"top_p",
|
|
30
|
+
"frequency_penalty",
|
|
31
|
+
"presence_penalty",
|
|
32
|
+
"query",
|
|
33
|
+
"url",
|
|
34
|
+
]
|
|
35
|
+
SECRET_VALUE_PATTERNS = [
|
|
36
|
+
re.compile(r"sk-[a-zA-Z0-9]{20,}"), # OpenAI API keys (at least 20 chars)
|
|
37
|
+
re.compile(r"ya29\.[a-zA-Z0-9_-]+"), # Google OAuth tokens
|
|
38
|
+
re.compile(r"ghp_[a-zA-Z0-9]{20,}"), # GitHub tokens (at least 20 chars)
|
|
39
|
+
re.compile(r"gho_[a-zA-Z0-9]{20,}"), # GitHub tokens (at least 20 chars)
|
|
40
|
+
re.compile(r"ghu_[a-zA-Z0-9]{20,}"), # GitHub tokens (at least 20 chars)
|
|
41
|
+
re.compile(r"ghs_[a-zA-Z0-9]{20,}"), # GitHub tokens (at least 20 chars)
|
|
42
|
+
re.compile(r"ghr_[a-zA-Z0-9]{20,}"), # GitHub tokens (at least 20 chars)
|
|
43
|
+
re.compile(r"eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+"), # JWT tokens
|
|
44
|
+
]
|
|
45
|
+
SENSITIVE_PATTERNS = re.compile(
|
|
46
|
+
r"(?:password|secret|token|key|api_key)(?:\s*[:=]\s*[^\s,}]+)?",
|
|
47
|
+
re.IGNORECASE,
|
|
48
|
+
)
|
|
49
|
+
SECRET_MASK = "••••••"
|
|
50
|
+
STATUS_GLYPHS = {
|
|
51
|
+
"success": ICON_STATUS_SUCCESS,
|
|
52
|
+
"failed": ICON_STATUS_FAILED,
|
|
53
|
+
"warning": ICON_STATUS_WARNING,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _truncate_string(s: str, max_len: int) -> str:
|
|
58
|
+
"""Truncate a string to a maximum length."""
|
|
59
|
+
if len(s) <= max_len:
|
|
60
|
+
return s
|
|
61
|
+
return s[: max_len - 3] + "…"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def mask_secrets_in_string(text: str) -> str:
|
|
65
|
+
"""Mask sensitive information in a string."""
|
|
66
|
+
result = text
|
|
67
|
+
for pattern in SECRET_VALUE_PATTERNS:
|
|
68
|
+
result = re.sub(pattern, SECRET_MASK, result)
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def redact_sensitive(text: str | dict | list) -> str | dict | list:
|
|
73
|
+
"""Redact sensitive information in a string, dict, or list."""
|
|
74
|
+
if isinstance(text, dict):
|
|
75
|
+
return _redact_dict_values(text)
|
|
76
|
+
elif isinstance(text, list):
|
|
77
|
+
return _redact_list_items(text)
|
|
78
|
+
elif isinstance(text, str):
|
|
79
|
+
return _redact_string_content(text)
|
|
80
|
+
else:
|
|
81
|
+
return text
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _redact_dict_values(text: dict) -> dict:
|
|
85
|
+
"""Recursively process dictionary values and redact sensitive keys."""
|
|
86
|
+
result = {}
|
|
87
|
+
for key, value in text.items():
|
|
88
|
+
if _is_sensitive_key(key):
|
|
89
|
+
result[key] = SECRET_MASK
|
|
90
|
+
elif _should_recurse_redaction(value):
|
|
91
|
+
result[key] = redact_sensitive(value)
|
|
92
|
+
else:
|
|
93
|
+
result[key] = value
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _redact_list_items(text: list) -> list:
|
|
98
|
+
"""Recursively process list items."""
|
|
99
|
+
return [redact_sensitive(item) for item in text]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _redact_string_content(text: str) -> str:
|
|
103
|
+
"""Process string - first mask secrets, then redact sensitive patterns."""
|
|
104
|
+
result = text
|
|
105
|
+
# First mask secrets
|
|
106
|
+
for pattern in SECRET_VALUE_PATTERNS:
|
|
107
|
+
result = re.sub(pattern, SECRET_MASK, result)
|
|
108
|
+
# Then redact sensitive patterns
|
|
109
|
+
result = re.sub(
|
|
110
|
+
SENSITIVE_PATTERNS,
|
|
111
|
+
lambda m: m.group(0).split("=")[0] + "=" + SECRET_MASK,
|
|
112
|
+
result,
|
|
113
|
+
)
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _is_sensitive_key(key: str) -> bool:
|
|
118
|
+
"""Check if a key contains sensitive information."""
|
|
119
|
+
key_lower = key.lower()
|
|
120
|
+
return any(sensitive in key_lower for sensitive in ["password", "secret", "token", "key", "api_key"])
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _should_recurse_redaction(value: Any) -> bool:
|
|
124
|
+
"""Check if a value should be recursively processed."""
|
|
125
|
+
return isinstance(value, (dict, list)) or isinstance(value, str)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def glyph_for_status(icon_key: str | None) -> str | None:
|
|
129
|
+
"""Return glyph representing a step status icon key."""
|
|
130
|
+
if not icon_key:
|
|
131
|
+
return None
|
|
132
|
+
return STATUS_GLYPHS.get(icon_key)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def normalise_display_label(label: str | None) -> str:
|
|
136
|
+
"""Return a user facing label or the Unknown fallback."""
|
|
137
|
+
if not isinstance(label, str):
|
|
138
|
+
text = ""
|
|
139
|
+
else:
|
|
140
|
+
text = label.strip()
|
|
141
|
+
return text or "Unknown step detail"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def pretty_args(args: dict | None, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
|
|
145
|
+
"""Format arguments in a pretty way."""
|
|
146
|
+
if not args:
|
|
147
|
+
return "{}"
|
|
148
|
+
|
|
149
|
+
# Mask secrets first by recursively processing the structure
|
|
150
|
+
try:
|
|
151
|
+
masked_args = redact_sensitive(args)
|
|
152
|
+
except Exception:
|
|
153
|
+
# Fallback to original args if redact_sensitive fails
|
|
154
|
+
masked_args = args
|
|
155
|
+
|
|
156
|
+
# Convert to JSON string and truncate if needed
|
|
157
|
+
try:
|
|
158
|
+
args_str = json.dumps(masked_args, ensure_ascii=False, separators=(",", ":"))
|
|
159
|
+
return _truncate_string(args_str, max_len)
|
|
160
|
+
except Exception:
|
|
161
|
+
# Fallback to string representation if JSON serialization fails
|
|
162
|
+
args_str = str(masked_args)
|
|
163
|
+
return _truncate_string(args_str, max_len)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def pretty_out(output: any, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
|
|
167
|
+
"""Format output in a pretty way."""
|
|
168
|
+
if output is None:
|
|
169
|
+
return "None"
|
|
170
|
+
|
|
171
|
+
if isinstance(output, str):
|
|
172
|
+
# Mask secrets in string output
|
|
173
|
+
masked_output = mask_secrets_in_string(output)
|
|
174
|
+
|
|
175
|
+
# Remove LaTeX commands (common math expressions)
|
|
176
|
+
masked_output = re.sub(r"\\[a-zA-Z]+\{[^}]*\}", "", masked_output)
|
|
177
|
+
masked_output = re.sub(r"\\[a-zA-Z]+", "", masked_output)
|
|
178
|
+
|
|
179
|
+
# Strip leading/trailing whitespace but preserve internal spacing
|
|
180
|
+
masked_output = masked_output.strip()
|
|
181
|
+
# Replace newlines with spaces to preserve formatting
|
|
182
|
+
masked_output = masked_output.replace("\n", " ")
|
|
183
|
+
return _truncate_string(masked_output, max_len)
|
|
184
|
+
|
|
185
|
+
# For other types, convert to string and truncate
|
|
186
|
+
output_str = str(output)
|
|
187
|
+
return _truncate_string(output_str, max_len)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_step_icon(step_kind: str) -> str:
|
|
191
|
+
"""Get the appropriate icon for a step kind."""
|
|
192
|
+
if step_kind == "tool":
|
|
193
|
+
return ICON_TOOL_STEP
|
|
194
|
+
if step_kind == "delegate":
|
|
195
|
+
return ICON_DELEGATE
|
|
196
|
+
if step_kind == "agent":
|
|
197
|
+
return ICON_AGENT_STEP
|
|
198
|
+
return ""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def is_step_finished(step: Any) -> bool:
|
|
202
|
+
"""Check if a step is finished.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
step: The step object to check
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
True if the step status is "finished", False otherwise
|
|
209
|
+
"""
|
|
210
|
+
return getattr(step, "status", None) == "finished"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def format_main_title(
|
|
214
|
+
header_text: str,
|
|
215
|
+
has_running_steps: bool,
|
|
216
|
+
get_spinner_char: Callable[[], str],
|
|
217
|
+
) -> str:
|
|
218
|
+
"""Generate the main panel title with dynamic status indicators.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
header_text: The header text from the renderer
|
|
222
|
+
has_running_steps: Whether there are running steps
|
|
223
|
+
get_spinner_char: Function to get spinner character
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
A formatted title string showing the agent name and status.
|
|
227
|
+
"""
|
|
228
|
+
# base name
|
|
229
|
+
name = (header_text or "").strip() or "Assistant"
|
|
230
|
+
# strip leading rule emojis if present
|
|
231
|
+
name = name.replace("—", " ").strip()
|
|
232
|
+
# spinner if still working
|
|
233
|
+
mark = "✓" if not has_running_steps else get_spinner_char()
|
|
234
|
+
return f"{name} {mark}"
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def print_header_once(
|
|
238
|
+
console: Any,
|
|
239
|
+
text: str,
|
|
240
|
+
last_header: str,
|
|
241
|
+
rules_enabled: bool,
|
|
242
|
+
style: str | None = None,
|
|
243
|
+
) -> str:
|
|
244
|
+
"""Print header text only when it changes to avoid duplicate output.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
console: Rich console instance
|
|
248
|
+
text: The header text to display
|
|
249
|
+
last_header: The last header text that was printed
|
|
250
|
+
rules_enabled: Whether header rules are enabled
|
|
251
|
+
style: Optional Rich style for the header rule
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
The updated last_header value
|
|
255
|
+
"""
|
|
256
|
+
if not rules_enabled:
|
|
257
|
+
return text
|
|
258
|
+
if text and text != last_header:
|
|
259
|
+
try:
|
|
260
|
+
console.rule(text, style=style)
|
|
261
|
+
except Exception:
|
|
262
|
+
console.print(text)
|
|
263
|
+
return text
|
|
264
|
+
return last_header
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Layout utilities exposed for renderer/viewer consumers.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from glaip_sdk.utils.rendering.layout.panels import (
|
|
8
|
+
create_context_panel,
|
|
9
|
+
create_final_panel,
|
|
10
|
+
create_main_panel,
|
|
11
|
+
create_tool_panel,
|
|
12
|
+
)
|
|
13
|
+
from glaip_sdk.utils.rendering.layout.progress import (
|
|
14
|
+
TrailingSpinnerLine,
|
|
15
|
+
build_progress_footer,
|
|
16
|
+
format_elapsed_time,
|
|
17
|
+
format_tool_title,
|
|
18
|
+
format_working_indicator,
|
|
19
|
+
get_spinner,
|
|
20
|
+
get_spinner_char,
|
|
21
|
+
is_delegation_tool,
|
|
22
|
+
)
|
|
23
|
+
from glaip_sdk.utils.rendering.layout.transcript import (
|
|
24
|
+
DEFAULT_TRANSCRIPT_THEME,
|
|
25
|
+
TranscriptGlyphs,
|
|
26
|
+
TranscriptRow,
|
|
27
|
+
TranscriptSnapshot,
|
|
28
|
+
build_final_panel,
|
|
29
|
+
build_transcript_snapshot,
|
|
30
|
+
build_transcript_view,
|
|
31
|
+
extract_query_from_meta,
|
|
32
|
+
format_final_panel_title,
|
|
33
|
+
render_final_panel,
|
|
34
|
+
)
|
|
35
|
+
from glaip_sdk.utils.rendering.layout.summary import render_summary_panels
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# Panels
|
|
39
|
+
"create_context_panel",
|
|
40
|
+
"create_final_panel",
|
|
41
|
+
"create_main_panel",
|
|
42
|
+
"create_tool_panel",
|
|
43
|
+
"render_summary_panels",
|
|
44
|
+
# Progress
|
|
45
|
+
"TrailingSpinnerLine",
|
|
46
|
+
"build_progress_footer",
|
|
47
|
+
"format_elapsed_time",
|
|
48
|
+
"format_tool_title",
|
|
49
|
+
"format_working_indicator",
|
|
50
|
+
"get_spinner",
|
|
51
|
+
"get_spinner_char",
|
|
52
|
+
"is_delegation_tool",
|
|
53
|
+
# Transcript
|
|
54
|
+
"DEFAULT_TRANSCRIPT_THEME",
|
|
55
|
+
"TranscriptGlyphs",
|
|
56
|
+
"TranscriptRow",
|
|
57
|
+
"TranscriptSnapshot",
|
|
58
|
+
"build_final_panel",
|
|
59
|
+
"build_transcript_snapshot",
|
|
60
|
+
"build_transcript_view",
|
|
61
|
+
"extract_query_from_meta",
|
|
62
|
+
"format_final_panel_title",
|
|
63
|
+
"render_final_panel",
|
|
64
|
+
]
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Panel rendering utilities for the renderer package.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from rich.align import Align
|
|
11
|
+
from rich.markdown import Markdown
|
|
12
|
+
from rich.spinner import Spinner
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
from glaip_sdk.branding import INFO, PRIMARY, SUCCESS, WARNING
|
|
16
|
+
from glaip_sdk.rich_components import AIPPanel
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _spinner_renderable(message: str = "Processing...") -> Align:
|
|
20
|
+
"""Build a Rich spinner renderable for loading placeholders."""
|
|
21
|
+
spinner = Spinner(
|
|
22
|
+
"dots",
|
|
23
|
+
text=Text(f" {message}", style="dim"),
|
|
24
|
+
style=INFO,
|
|
25
|
+
)
|
|
26
|
+
return Align.left(spinner)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def create_main_panel(content: str, title: str, theme: str = "dark") -> AIPPanel:
|
|
30
|
+
"""Create a main content panel.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
content: The content to display
|
|
34
|
+
title: Panel title
|
|
35
|
+
theme: Color theme ("dark" or "light")
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Rich Panel instance
|
|
39
|
+
"""
|
|
40
|
+
if content.strip():
|
|
41
|
+
return AIPPanel(
|
|
42
|
+
Markdown(content, code_theme=("monokai" if theme == "dark" else "github")),
|
|
43
|
+
title=title,
|
|
44
|
+
border_style=SUCCESS,
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
return AIPPanel(
|
|
48
|
+
_spinner_renderable(),
|
|
49
|
+
title=title,
|
|
50
|
+
border_style=SUCCESS,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def create_tool_panel(
|
|
55
|
+
title: str,
|
|
56
|
+
content: str,
|
|
57
|
+
status: str = "running",
|
|
58
|
+
theme: str = "dark",
|
|
59
|
+
is_delegation: bool = False,
|
|
60
|
+
*,
|
|
61
|
+
spinner_message: str | None = None,
|
|
62
|
+
) -> AIPPanel:
|
|
63
|
+
"""Create a tool execution panel.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
title: Tool name/title
|
|
67
|
+
content: Tool output content
|
|
68
|
+
status: Tool execution status
|
|
69
|
+
theme: Color theme
|
|
70
|
+
is_delegation: Whether this is a delegation tool
|
|
71
|
+
spinner_message: Optional custom message to show alongside the spinner
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Rich Panel instance
|
|
75
|
+
"""
|
|
76
|
+
mark = "✓" if status == "finished" else ""
|
|
77
|
+
border_style = WARNING if is_delegation else PRIMARY
|
|
78
|
+
|
|
79
|
+
if content:
|
|
80
|
+
body_renderable = Markdown(
|
|
81
|
+
content,
|
|
82
|
+
code_theme=("monokai" if theme == "dark" else "github"),
|
|
83
|
+
)
|
|
84
|
+
elif status == "running":
|
|
85
|
+
body_renderable = _spinner_renderable(spinner_message or f"{title} running...")
|
|
86
|
+
else:
|
|
87
|
+
body_renderable = Text("No output yet.", style="dim")
|
|
88
|
+
|
|
89
|
+
title_text = f"{title} {mark}".rstrip()
|
|
90
|
+
|
|
91
|
+
return AIPPanel(
|
|
92
|
+
body_renderable,
|
|
93
|
+
title=title_text,
|
|
94
|
+
border_style=border_style,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def create_context_panel(
|
|
99
|
+
title: str,
|
|
100
|
+
content: str,
|
|
101
|
+
status: str = "running",
|
|
102
|
+
theme: str = "dark",
|
|
103
|
+
is_delegation: bool = False,
|
|
104
|
+
) -> AIPPanel:
|
|
105
|
+
"""Create a context/sub-agent panel.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
title: Context title
|
|
109
|
+
content: Context content
|
|
110
|
+
status: Execution status
|
|
111
|
+
theme: Color theme
|
|
112
|
+
is_delegation: Whether this is a delegation context
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Rich Panel instance
|
|
116
|
+
"""
|
|
117
|
+
mark = "✓" if status == "finished" else ""
|
|
118
|
+
border_style = WARNING if is_delegation else INFO
|
|
119
|
+
|
|
120
|
+
title_text = f"{title} {mark}".rstrip()
|
|
121
|
+
|
|
122
|
+
return AIPPanel(
|
|
123
|
+
Markdown(
|
|
124
|
+
content,
|
|
125
|
+
code_theme=("monokai" if theme == "dark" else "github"),
|
|
126
|
+
),
|
|
127
|
+
title=title_text,
|
|
128
|
+
border_style=border_style,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def create_final_panel(content: str, title: str = "Final Result", theme: str = "dark") -> AIPPanel:
|
|
133
|
+
"""Create a final result panel.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
content: Final result content
|
|
137
|
+
title: Panel title
|
|
138
|
+
theme: Color theme
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Rich Panel instance
|
|
142
|
+
"""
|
|
143
|
+
return AIPPanel(
|
|
144
|
+
Markdown(content, code_theme=("monokai" if theme == "dark" else "github")),
|
|
145
|
+
title=title,
|
|
146
|
+
border_style=SUCCESS,
|
|
147
|
+
padding=(0, 1),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
__all__ = [
|
|
152
|
+
"create_main_panel",
|
|
153
|
+
"create_tool_panel",
|
|
154
|
+
"create_context_panel",
|
|
155
|
+
"create_final_panel",
|
|
156
|
+
]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Progress and timing utilities for the renderer package.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from time import monotonic
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from rich.console import Console as RichConsole
|
|
13
|
+
from rich.console import Group
|
|
14
|
+
from rich.measure import Measurement
|
|
15
|
+
from rich.spinner import Spinner
|
|
16
|
+
from rich.text import Text
|
|
17
|
+
|
|
18
|
+
from glaip_sdk.utils.rendering.steps.manager import StepManager
|
|
19
|
+
|
|
20
|
+
_SPINNER_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _spinner_time() -> float:
|
|
24
|
+
"""Return the monotonic time used for spinner animation."""
|
|
25
|
+
return monotonic()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_spinner() -> str:
|
|
29
|
+
"""Return the current animated spinner character for visual feedback."""
|
|
30
|
+
return get_spinner_char()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_spinner_char() -> str:
|
|
34
|
+
"""Return the spinner frame based on elapsed time."""
|
|
35
|
+
frame_index = int(_spinner_time() * 10) % len(_SPINNER_FRAMES)
|
|
36
|
+
return _SPINNER_FRAMES[frame_index]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TrailingSpinnerLine:
|
|
40
|
+
"""Render a text line with a trailing animated Rich spinner."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, base_text: Text, spinner: Spinner) -> None:
|
|
43
|
+
"""Initialize spinner line with base text and spinner component."""
|
|
44
|
+
self._base_text = base_text
|
|
45
|
+
self._spinner = spinner
|
|
46
|
+
|
|
47
|
+
def __rich_console__(self, console: RichConsole, options: Any) -> Any: # type: ignore[override]
|
|
48
|
+
"""Render the text with trailing animated spinner."""
|
|
49
|
+
spinner_render = self._spinner.render(console.get_time())
|
|
50
|
+
combined = Text.assemble(self._base_text.copy(), " ", spinner_render)
|
|
51
|
+
yield combined
|
|
52
|
+
|
|
53
|
+
def __rich_measure__(self, console: RichConsole, options: Any) -> Measurement: # type: ignore[override]
|
|
54
|
+
"""Measure the combined text and spinner dimensions."""
|
|
55
|
+
snapshot = self._spinner.render(0)
|
|
56
|
+
combined = Text.assemble(self._base_text.copy(), " ", snapshot)
|
|
57
|
+
return Measurement.get(console, options, combined)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _resolve_elapsed_time(
|
|
61
|
+
started_at: float | None,
|
|
62
|
+
server_elapsed_time: float | None,
|
|
63
|
+
streaming_started_at: float | None,
|
|
64
|
+
) -> float | None:
|
|
65
|
+
"""Return the elapsed seconds using server data when available."""
|
|
66
|
+
if server_elapsed_time is not None and streaming_started_at is not None:
|
|
67
|
+
return server_elapsed_time
|
|
68
|
+
if started_at is None:
|
|
69
|
+
return None
|
|
70
|
+
try:
|
|
71
|
+
return monotonic() - started_at
|
|
72
|
+
except Exception:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _format_elapsed_suffix(elapsed: float) -> str:
|
|
77
|
+
"""Return formatting suffix for elapsed timing."""
|
|
78
|
+
if elapsed >= 1:
|
|
79
|
+
return f"{elapsed:.2f}s"
|
|
80
|
+
elapsed_ms = int(elapsed * 1000)
|
|
81
|
+
return f"{elapsed_ms}ms" if elapsed_ms > 0 else "<1ms"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def format_working_indicator(
|
|
85
|
+
started_at: float | None,
|
|
86
|
+
server_elapsed_time: float | None = None,
|
|
87
|
+
streaming_started_at: float | None = None,
|
|
88
|
+
) -> str:
|
|
89
|
+
"""Format a working indicator with elapsed time."""
|
|
90
|
+
base_message = "Working..."
|
|
91
|
+
|
|
92
|
+
if started_at is None and (server_elapsed_time is None or streaming_started_at is None):
|
|
93
|
+
return base_message
|
|
94
|
+
|
|
95
|
+
spinner_chip = f"{get_spinner_char()} {base_message}"
|
|
96
|
+
elapsed = _resolve_elapsed_time(started_at, server_elapsed_time, streaming_started_at)
|
|
97
|
+
if elapsed is None:
|
|
98
|
+
return spinner_chip
|
|
99
|
+
|
|
100
|
+
suffix = _format_elapsed_suffix(elapsed)
|
|
101
|
+
return f"{spinner_chip} ({suffix})"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def format_elapsed_time(elapsed_seconds: float) -> str:
|
|
105
|
+
"""Format elapsed time in a human-readable format.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
elapsed_seconds: Time in seconds
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Formatted time string
|
|
112
|
+
"""
|
|
113
|
+
if elapsed_seconds >= 60:
|
|
114
|
+
minutes = int(elapsed_seconds // 60)
|
|
115
|
+
seconds = elapsed_seconds % 60
|
|
116
|
+
return f"{minutes}m {seconds:.1f}s"
|
|
117
|
+
elif elapsed_seconds >= 1:
|
|
118
|
+
return f"{elapsed_seconds:.2f}s"
|
|
119
|
+
else:
|
|
120
|
+
ms = int(elapsed_seconds * 1000)
|
|
121
|
+
return f"{ms}ms" if ms > 0 else "<1ms"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def is_delegation_tool(tool_name: str) -> bool:
|
|
125
|
+
"""Check if a tool name indicates delegation functionality.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
tool_name: The name of the tool to check
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
True if this is a delegation tool
|
|
132
|
+
"""
|
|
133
|
+
return tool_name.startswith("delegate_to_") or tool_name.startswith("delegate_") or "sub_agent" in tool_name.lower()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _delegation_tool_title(tool_name: str) -> str | None:
|
|
137
|
+
"""Return delegation-aware title or ``None`` when not applicable."""
|
|
138
|
+
if tool_name.startswith("delegate_to_"):
|
|
139
|
+
sub_agent_name = tool_name.replace("delegate_to_", "", 1)
|
|
140
|
+
return f"Sub-Agent: {sub_agent_name}"
|
|
141
|
+
if tool_name.startswith("delegate_"):
|
|
142
|
+
sub_agent_name = tool_name.replace("delegate_", "", 1)
|
|
143
|
+
return f"Sub-Agent: {sub_agent_name}"
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _strip_path_and_extension(tool_name: str) -> str:
|
|
148
|
+
"""Return tool name without path segments or extensions."""
|
|
149
|
+
filename = tool_name.rsplit("/", 1)[-1]
|
|
150
|
+
base_name = filename.split(".", 1)[0]
|
|
151
|
+
return base_name
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def format_tool_title(tool_name: str) -> str:
|
|
155
|
+
"""Format tool name for panel title display.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
tool_name: The full tool name (may include file paths)
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Formatted title string suitable for panel display
|
|
162
|
+
"""
|
|
163
|
+
# Check if this is a delegation tool
|
|
164
|
+
if is_delegation_tool(tool_name):
|
|
165
|
+
delegation_title = _delegation_tool_title(tool_name)
|
|
166
|
+
if delegation_title:
|
|
167
|
+
return delegation_title
|
|
168
|
+
|
|
169
|
+
# For regular tools, clean up the name
|
|
170
|
+
# Remove file path prefixes if present
|
|
171
|
+
clean_name = _strip_path_and_extension(tool_name)
|
|
172
|
+
|
|
173
|
+
# Convert snake_case to Title Case
|
|
174
|
+
return clean_name.replace("_", " ").title()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _has_running_steps(steps: StepManager) -> bool:
|
|
178
|
+
for step in steps.by_id.values():
|
|
179
|
+
if getattr(step, "status", None) not in {"finished", "failed", "stopped"}:
|
|
180
|
+
return True
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def build_progress_footer(
|
|
185
|
+
*,
|
|
186
|
+
state: Any,
|
|
187
|
+
steps: StepManager,
|
|
188
|
+
started_at: float | None,
|
|
189
|
+
server_elapsed_time: float | None,
|
|
190
|
+
) -> Group | None:
|
|
191
|
+
"""Return a trailing progress indicator when work is ongoing."""
|
|
192
|
+
if not _has_running_steps(steps):
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
indicator = format_working_indicator(
|
|
196
|
+
started_at,
|
|
197
|
+
server_elapsed_time,
|
|
198
|
+
getattr(state, "streaming_started_at", None),
|
|
199
|
+
)
|
|
200
|
+
text = Text(indicator, style="dim")
|
|
201
|
+
spinner = Spinner("dots", style="dim")
|
|
202
|
+
return Group(TrailingSpinnerLine(text, spinner))
|