glaip-sdk 0.0.0b99__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.
Files changed (207) hide show
  1. glaip_sdk/__init__.py +52 -0
  2. glaip_sdk/_version.py +81 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1227 -0
  5. glaip_sdk/branding.py +211 -0
  6. glaip_sdk/cli/__init__.py +9 -0
  7. glaip_sdk/cli/account_store.py +540 -0
  8. glaip_sdk/cli/agent_config.py +78 -0
  9. glaip_sdk/cli/auth.py +705 -0
  10. glaip_sdk/cli/commands/__init__.py +5 -0
  11. glaip_sdk/cli/commands/accounts.py +746 -0
  12. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  13. glaip_sdk/cli/commands/agents/_common.py +561 -0
  14. glaip_sdk/cli/commands/agents/create.py +151 -0
  15. glaip_sdk/cli/commands/agents/delete.py +64 -0
  16. glaip_sdk/cli/commands/agents/get.py +89 -0
  17. glaip_sdk/cli/commands/agents/list.py +129 -0
  18. glaip_sdk/cli/commands/agents/run.py +264 -0
  19. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  20. glaip_sdk/cli/commands/agents/update.py +112 -0
  21. glaip_sdk/cli/commands/common_config.py +104 -0
  22. glaip_sdk/cli/commands/configure.py +895 -0
  23. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  24. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  25. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  26. glaip_sdk/cli/commands/mcps/create.py +152 -0
  27. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  28. glaip_sdk/cli/commands/mcps/get.py +212 -0
  29. glaip_sdk/cli/commands/mcps/list.py +69 -0
  30. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  31. glaip_sdk/cli/commands/mcps/update.py +190 -0
  32. glaip_sdk/cli/commands/models.py +67 -0
  33. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  34. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  35. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  36. glaip_sdk/cli/commands/tools/_common.py +80 -0
  37. glaip_sdk/cli/commands/tools/create.py +228 -0
  38. glaip_sdk/cli/commands/tools/delete.py +61 -0
  39. glaip_sdk/cli/commands/tools/get.py +103 -0
  40. glaip_sdk/cli/commands/tools/list.py +69 -0
  41. glaip_sdk/cli/commands/tools/script.py +49 -0
  42. glaip_sdk/cli/commands/tools/update.py +102 -0
  43. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  44. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  45. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  46. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  47. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  48. glaip_sdk/cli/commands/update.py +192 -0
  49. glaip_sdk/cli/config.py +95 -0
  50. glaip_sdk/cli/constants.py +38 -0
  51. glaip_sdk/cli/context.py +150 -0
  52. glaip_sdk/cli/core/__init__.py +79 -0
  53. glaip_sdk/cli/core/context.py +124 -0
  54. glaip_sdk/cli/core/output.py +851 -0
  55. glaip_sdk/cli/core/prompting.py +649 -0
  56. glaip_sdk/cli/core/rendering.py +187 -0
  57. glaip_sdk/cli/display.py +355 -0
  58. glaip_sdk/cli/hints.py +57 -0
  59. glaip_sdk/cli/io.py +112 -0
  60. glaip_sdk/cli/main.py +686 -0
  61. glaip_sdk/cli/masking.py +136 -0
  62. glaip_sdk/cli/mcp_validators.py +287 -0
  63. glaip_sdk/cli/pager.py +266 -0
  64. glaip_sdk/cli/parsers/__init__.py +7 -0
  65. glaip_sdk/cli/parsers/json_input.py +177 -0
  66. glaip_sdk/cli/resolution.py +68 -0
  67. glaip_sdk/cli/rich_helpers.py +27 -0
  68. glaip_sdk/cli/slash/__init__.py +15 -0
  69. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  70. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  71. glaip_sdk/cli/slash/agent_session.py +285 -0
  72. glaip_sdk/cli/slash/prompt.py +256 -0
  73. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  74. glaip_sdk/cli/slash/session.py +1724 -0
  75. glaip_sdk/cli/slash/tui/__init__.py +34 -0
  76. glaip_sdk/cli/slash/tui/accounts.tcss +88 -0
  77. glaip_sdk/cli/slash/tui/accounts_app.py +933 -0
  78. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  79. glaip_sdk/cli/slash/tui/clipboard.py +147 -0
  80. glaip_sdk/cli/slash/tui/context.py +59 -0
  81. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  82. glaip_sdk/cli/slash/tui/loading.py +58 -0
  83. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  84. glaip_sdk/cli/slash/tui/terminal.py +402 -0
  85. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  86. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  87. glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
  88. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  89. glaip_sdk/cli/slash/tui/toast.py +123 -0
  90. glaip_sdk/cli/transcript/__init__.py +31 -0
  91. glaip_sdk/cli/transcript/cache.py +536 -0
  92. glaip_sdk/cli/transcript/capture.py +329 -0
  93. glaip_sdk/cli/transcript/export.py +38 -0
  94. glaip_sdk/cli/transcript/history.py +815 -0
  95. glaip_sdk/cli/transcript/launcher.py +77 -0
  96. glaip_sdk/cli/transcript/viewer.py +374 -0
  97. glaip_sdk/cli/update_notifier.py +369 -0
  98. glaip_sdk/cli/validators.py +238 -0
  99. glaip_sdk/client/__init__.py +12 -0
  100. glaip_sdk/client/_schedule_payloads.py +89 -0
  101. glaip_sdk/client/agent_runs.py +147 -0
  102. glaip_sdk/client/agents.py +1353 -0
  103. glaip_sdk/client/base.py +502 -0
  104. glaip_sdk/client/main.py +253 -0
  105. glaip_sdk/client/mcps.py +401 -0
  106. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  107. glaip_sdk/client/payloads/agent/requests.py +495 -0
  108. glaip_sdk/client/payloads/agent/responses.py +43 -0
  109. glaip_sdk/client/run_rendering.py +747 -0
  110. glaip_sdk/client/schedules.py +439 -0
  111. glaip_sdk/client/shared.py +21 -0
  112. glaip_sdk/client/tools.py +690 -0
  113. glaip_sdk/client/validators.py +198 -0
  114. glaip_sdk/config/constants.py +52 -0
  115. glaip_sdk/exceptions.py +113 -0
  116. glaip_sdk/hitl/__init__.py +15 -0
  117. glaip_sdk/hitl/local.py +151 -0
  118. glaip_sdk/icons.py +25 -0
  119. glaip_sdk/mcps/__init__.py +21 -0
  120. glaip_sdk/mcps/base.py +345 -0
  121. glaip_sdk/models/__init__.py +107 -0
  122. glaip_sdk/models/agent.py +47 -0
  123. glaip_sdk/models/agent_runs.py +117 -0
  124. glaip_sdk/models/common.py +42 -0
  125. glaip_sdk/models/mcp.py +33 -0
  126. glaip_sdk/models/schedule.py +224 -0
  127. glaip_sdk/models/tool.py +33 -0
  128. glaip_sdk/payload_schemas/__init__.py +7 -0
  129. glaip_sdk/payload_schemas/agent.py +85 -0
  130. glaip_sdk/registry/__init__.py +55 -0
  131. glaip_sdk/registry/agent.py +164 -0
  132. glaip_sdk/registry/base.py +139 -0
  133. glaip_sdk/registry/mcp.py +253 -0
  134. glaip_sdk/registry/tool.py +393 -0
  135. glaip_sdk/rich_components.py +125 -0
  136. glaip_sdk/runner/__init__.py +59 -0
  137. glaip_sdk/runner/base.py +84 -0
  138. glaip_sdk/runner/deps.py +112 -0
  139. glaip_sdk/runner/langgraph.py +870 -0
  140. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  141. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  142. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  143. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  144. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  145. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  146. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
  147. glaip_sdk/schedules/__init__.py +22 -0
  148. glaip_sdk/schedules/base.py +291 -0
  149. glaip_sdk/tools/__init__.py +22 -0
  150. glaip_sdk/tools/base.py +466 -0
  151. glaip_sdk/utils/__init__.py +86 -0
  152. glaip_sdk/utils/a2a/__init__.py +34 -0
  153. glaip_sdk/utils/a2a/event_processor.py +188 -0
  154. glaip_sdk/utils/agent_config.py +194 -0
  155. glaip_sdk/utils/bundler.py +267 -0
  156. glaip_sdk/utils/client.py +111 -0
  157. glaip_sdk/utils/client_utils.py +486 -0
  158. glaip_sdk/utils/datetime_helpers.py +58 -0
  159. glaip_sdk/utils/discovery.py +78 -0
  160. glaip_sdk/utils/display.py +135 -0
  161. glaip_sdk/utils/export.py +143 -0
  162. glaip_sdk/utils/general.py +61 -0
  163. glaip_sdk/utils/import_export.py +168 -0
  164. glaip_sdk/utils/import_resolver.py +530 -0
  165. glaip_sdk/utils/instructions.py +101 -0
  166. glaip_sdk/utils/rendering/__init__.py +115 -0
  167. glaip_sdk/utils/rendering/formatting.py +264 -0
  168. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  169. glaip_sdk/utils/rendering/layout/panels.py +156 -0
  170. glaip_sdk/utils/rendering/layout/progress.py +202 -0
  171. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  172. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  173. glaip_sdk/utils/rendering/models.py +85 -0
  174. glaip_sdk/utils/rendering/renderer/__init__.py +55 -0
  175. glaip_sdk/utils/rendering/renderer/base.py +1082 -0
  176. glaip_sdk/utils/rendering/renderer/config.py +27 -0
  177. glaip_sdk/utils/rendering/renderer/console.py +55 -0
  178. glaip_sdk/utils/rendering/renderer/debug.py +178 -0
  179. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  180. glaip_sdk/utils/rendering/renderer/stream.py +202 -0
  181. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  182. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  183. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  184. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  185. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  186. glaip_sdk/utils/rendering/state.py +204 -0
  187. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  188. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  189. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  190. glaip_sdk/utils/rendering/steps/format.py +176 -0
  191. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  192. glaip_sdk/utils/rendering/timing.py +36 -0
  193. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  194. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  195. glaip_sdk/utils/resource_refs.py +195 -0
  196. glaip_sdk/utils/run_renderer.py +41 -0
  197. glaip_sdk/utils/runtime_config.py +425 -0
  198. glaip_sdk/utils/serialization.py +424 -0
  199. glaip_sdk/utils/sync.py +142 -0
  200. glaip_sdk/utils/tool_detection.py +33 -0
  201. glaip_sdk/utils/tool_storage_provider.py +140 -0
  202. glaip_sdk/utils/validation.py +264 -0
  203. glaip_sdk-0.0.0b99.dist-info/METADATA +239 -0
  204. glaip_sdk-0.0.0b99.dist-info/RECORD +207 -0
  205. glaip_sdk-0.0.0b99.dist-info/WHEEL +5 -0
  206. glaip_sdk-0.0.0b99.dist-info/entry_points.txt +2 -0
  207. glaip_sdk-0.0.0b99.dist-info/top_level.txt +1 -0
@@ -0,0 +1,115 @@
1
+ """Rendering utilities package (formatting, models, steps, debug).
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from datetime import datetime
8
+ from typing import Any
9
+
10
+ from rich.console import Console
11
+
12
+ from glaip_sdk.models.agent_runs import RunWithOutput
13
+ from glaip_sdk.utils.rendering.renderer.debug import render_debug_event, render_debug_event_stream
14
+
15
+
16
+ def _parse_event_received_timestamp(event: dict[str, Any]) -> datetime | None:
17
+ """Parse received_at timestamp from SSE event.
18
+
19
+ Args:
20
+ event: SSE event dictionary
21
+
22
+ Returns:
23
+ Parsed datetime or None if not available
24
+ """
25
+ received_at = event.get("received_at")
26
+ if not received_at:
27
+ return None
28
+
29
+ if isinstance(received_at, datetime):
30
+ return received_at
31
+
32
+ if isinstance(received_at, str):
33
+ try:
34
+ # Try ISO format first
35
+ return datetime.fromisoformat(received_at.replace("Z", "+00:00"))
36
+ except ValueError:
37
+ try:
38
+ # Try common formats
39
+ return datetime.strptime(received_at, "%Y-%m-%dT%H:%M:%S.%fZ")
40
+ except ValueError:
41
+ return None
42
+
43
+ return None
44
+
45
+
46
+ def render_remote_sse_transcript(
47
+ run: RunWithOutput,
48
+ console: Console,
49
+ *,
50
+ show_metadata: bool = True,
51
+ ) -> None:
52
+ """Render remote SSE transcript events for a RunWithOutput.
53
+
54
+ Args:
55
+ run: RunWithOutput instance containing events
56
+ console: Rich console to render to
57
+ show_metadata: Whether to show run metadata summary
58
+ """
59
+ if show_metadata:
60
+ # Render metadata summary
61
+ console.print(f"[bold]Run: {run.id}[/bold]")
62
+ console.print(f"[dim]Agent: {run.agent_id}[/dim]")
63
+ console.print(f"[dim]Status: {run.status}[/dim]")
64
+ console.print(f"[dim]Type: {run.run_type}[/dim]")
65
+ if run.schedule_id:
66
+ console.print(f"[dim]Schedule ID: {run.schedule_id}[/dim]")
67
+ else:
68
+ console.print("[dim]Schedule: —[/dim]")
69
+ console.print(f"[dim]Started: {run.started_at.isoformat()}[/dim]")
70
+ if run.completed_at:
71
+ console.print(f"[dim]Completed: {run.completed_at.isoformat()}[/dim]")
72
+ console.print(f"[dim]Duration: {run.duration_formatted()}[/dim]")
73
+ console.print()
74
+
75
+ # Render events
76
+ if not run.output:
77
+ console.print("[dim]No SSE events available for this run.[/dim]")
78
+ return
79
+
80
+ console.print("[bold]SSE Events[/bold]")
81
+ console.print("[dim]────────────────────────────────────────────────────────[/dim]")
82
+
83
+ render_debug_event_stream(
84
+ run.output,
85
+ console,
86
+ resolve_timestamp=_parse_event_received_timestamp,
87
+ )
88
+ console.print()
89
+
90
+
91
+ class RemoteSSETranscriptRenderer:
92
+ """Renderer for remote SSE transcripts from RunWithOutput."""
93
+
94
+ def __init__(self, console: Console | None = None):
95
+ """Initialize the renderer.
96
+
97
+ Args:
98
+ console: Rich console instance (creates default if None)
99
+ """
100
+ self.console = console or Console()
101
+
102
+ def render(self, run: RunWithOutput, *, show_metadata: bool = True) -> None:
103
+ """Render a remote run transcript.
104
+
105
+ Args:
106
+ run: RunWithOutput instance to render
107
+ show_metadata: Whether to show run metadata summary
108
+ """
109
+ render_remote_sse_transcript(run, self.console, show_metadata=show_metadata)
110
+
111
+
112
+ __all__ = [
113
+ "render_remote_sse_transcript",
114
+ "RemoteSSETranscriptRenderer",
115
+ ]
@@ -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
+ ]