glaip-sdk 0.0.20__py3-none-any.whl → 0.7.7__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 (216) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +10 -3
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1250 -0
  5. glaip_sdk/branding.py +15 -6
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/agent_config.py +2 -6
  8. glaip_sdk/cli/auth.py +271 -45
  9. glaip_sdk/cli/commands/__init__.py +2 -2
  10. glaip_sdk/cli/commands/accounts.py +746 -0
  11. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  12. glaip_sdk/cli/commands/agents/_common.py +561 -0
  13. glaip_sdk/cli/commands/agents/create.py +151 -0
  14. glaip_sdk/cli/commands/agents/delete.py +64 -0
  15. glaip_sdk/cli/commands/agents/get.py +89 -0
  16. glaip_sdk/cli/commands/agents/list.py +129 -0
  17. glaip_sdk/cli/commands/agents/run.py +264 -0
  18. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  19. glaip_sdk/cli/commands/agents/update.py +112 -0
  20. glaip_sdk/cli/commands/common_config.py +104 -0
  21. glaip_sdk/cli/commands/configure.py +734 -143
  22. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  23. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  24. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  25. glaip_sdk/cli/commands/mcps/create.py +152 -0
  26. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  27. glaip_sdk/cli/commands/mcps/get.py +212 -0
  28. glaip_sdk/cli/commands/mcps/list.py +69 -0
  29. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  30. glaip_sdk/cli/commands/mcps/update.py +190 -0
  31. glaip_sdk/cli/commands/models.py +14 -12
  32. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  33. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  34. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  35. glaip_sdk/cli/commands/tools/_common.py +80 -0
  36. glaip_sdk/cli/commands/tools/create.py +228 -0
  37. glaip_sdk/cli/commands/tools/delete.py +61 -0
  38. glaip_sdk/cli/commands/tools/get.py +103 -0
  39. glaip_sdk/cli/commands/tools/list.py +69 -0
  40. glaip_sdk/cli/commands/tools/script.py +49 -0
  41. glaip_sdk/cli/commands/tools/update.py +102 -0
  42. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  43. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  44. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  45. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  46. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  47. glaip_sdk/cli/commands/update.py +164 -23
  48. glaip_sdk/cli/config.py +49 -7
  49. glaip_sdk/cli/constants.py +38 -0
  50. glaip_sdk/cli/context.py +8 -0
  51. glaip_sdk/cli/core/__init__.py +79 -0
  52. glaip_sdk/cli/core/context.py +124 -0
  53. glaip_sdk/cli/core/output.py +851 -0
  54. glaip_sdk/cli/core/prompting.py +649 -0
  55. glaip_sdk/cli/core/rendering.py +187 -0
  56. glaip_sdk/cli/display.py +45 -32
  57. glaip_sdk/cli/entrypoint.py +20 -0
  58. glaip_sdk/cli/hints.py +57 -0
  59. glaip_sdk/cli/io.py +14 -17
  60. glaip_sdk/cli/main.py +344 -167
  61. glaip_sdk/cli/masking.py +21 -33
  62. glaip_sdk/cli/mcp_validators.py +5 -15
  63. glaip_sdk/cli/pager.py +15 -22
  64. glaip_sdk/cli/parsers/__init__.py +1 -3
  65. glaip_sdk/cli/parsers/json_input.py +11 -22
  66. glaip_sdk/cli/resolution.py +5 -10
  67. glaip_sdk/cli/rich_helpers.py +1 -3
  68. glaip_sdk/cli/slash/__init__.py +0 -9
  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 +65 -29
  72. glaip_sdk/cli/slash/prompt.py +24 -10
  73. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  74. glaip_sdk/cli/slash/session.py +827 -232
  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 +12 -52
  91. glaip_sdk/cli/transcript/cache.py +258 -60
  92. glaip_sdk/cli/transcript/capture.py +72 -21
  93. glaip_sdk/cli/transcript/history.py +815 -0
  94. glaip_sdk/cli/transcript/launcher.py +1 -3
  95. glaip_sdk/cli/transcript/viewer.py +79 -329
  96. glaip_sdk/cli/update_notifier.py +385 -24
  97. glaip_sdk/cli/validators.py +16 -18
  98. glaip_sdk/client/__init__.py +3 -1
  99. glaip_sdk/client/_schedule_payloads.py +89 -0
  100. glaip_sdk/client/agent_runs.py +147 -0
  101. glaip_sdk/client/agents.py +370 -100
  102. glaip_sdk/client/base.py +78 -35
  103. glaip_sdk/client/hitl.py +136 -0
  104. glaip_sdk/client/main.py +25 -10
  105. glaip_sdk/client/mcps.py +166 -27
  106. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  107. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +65 -74
  108. glaip_sdk/client/payloads/agent/responses.py +43 -0
  109. glaip_sdk/client/run_rendering.py +583 -79
  110. glaip_sdk/client/schedules.py +439 -0
  111. glaip_sdk/client/shared.py +21 -0
  112. glaip_sdk/client/tools.py +214 -56
  113. glaip_sdk/client/validators.py +20 -48
  114. glaip_sdk/config/constants.py +11 -0
  115. glaip_sdk/exceptions.py +1 -3
  116. glaip_sdk/hitl/__init__.py +48 -0
  117. glaip_sdk/hitl/base.py +64 -0
  118. glaip_sdk/hitl/callback.py +43 -0
  119. glaip_sdk/hitl/local.py +121 -0
  120. glaip_sdk/hitl/remote.py +523 -0
  121. glaip_sdk/icons.py +9 -3
  122. glaip_sdk/mcps/__init__.py +21 -0
  123. glaip_sdk/mcps/base.py +345 -0
  124. glaip_sdk/models/__init__.py +107 -0
  125. glaip_sdk/models/agent.py +47 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/mcp.py +33 -0
  129. glaip_sdk/models/schedule.py +224 -0
  130. glaip_sdk/models/tool.py +33 -0
  131. glaip_sdk/payload_schemas/__init__.py +1 -13
  132. glaip_sdk/payload_schemas/agent.py +1 -3
  133. glaip_sdk/registry/__init__.py +55 -0
  134. glaip_sdk/registry/agent.py +164 -0
  135. glaip_sdk/registry/base.py +139 -0
  136. glaip_sdk/registry/mcp.py +253 -0
  137. glaip_sdk/registry/tool.py +445 -0
  138. glaip_sdk/rich_components.py +58 -2
  139. glaip_sdk/runner/__init__.py +76 -0
  140. glaip_sdk/runner/base.py +84 -0
  141. glaip_sdk/runner/deps.py +112 -0
  142. glaip_sdk/runner/langgraph.py +872 -0
  143. glaip_sdk/runner/logging_config.py +77 -0
  144. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  145. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  146. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  147. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  148. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  149. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  150. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  151. glaip_sdk/schedules/__init__.py +22 -0
  152. glaip_sdk/schedules/base.py +291 -0
  153. glaip_sdk/tools/__init__.py +22 -0
  154. glaip_sdk/tools/base.py +468 -0
  155. glaip_sdk/utils/__init__.py +59 -12
  156. glaip_sdk/utils/a2a/__init__.py +34 -0
  157. glaip_sdk/utils/a2a/event_processor.py +188 -0
  158. glaip_sdk/utils/agent_config.py +4 -14
  159. glaip_sdk/utils/bundler.py +403 -0
  160. glaip_sdk/utils/client.py +111 -0
  161. glaip_sdk/utils/client_utils.py +46 -28
  162. glaip_sdk/utils/datetime_helpers.py +58 -0
  163. glaip_sdk/utils/discovery.py +78 -0
  164. glaip_sdk/utils/display.py +25 -21
  165. glaip_sdk/utils/export.py +143 -0
  166. glaip_sdk/utils/general.py +1 -36
  167. glaip_sdk/utils/import_export.py +15 -16
  168. glaip_sdk/utils/import_resolver.py +524 -0
  169. glaip_sdk/utils/instructions.py +101 -0
  170. glaip_sdk/utils/rendering/__init__.py +115 -1
  171. glaip_sdk/utils/rendering/formatting.py +38 -23
  172. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  173. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
  174. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
  175. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  176. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  177. glaip_sdk/utils/rendering/models.py +18 -8
  178. glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
  179. glaip_sdk/utils/rendering/renderer/base.py +534 -882
  180. glaip_sdk/utils/rendering/renderer/config.py +4 -10
  181. glaip_sdk/utils/rendering/renderer/debug.py +30 -34
  182. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  183. glaip_sdk/utils/rendering/renderer/stream.py +13 -54
  184. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  185. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  186. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  187. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  188. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  189. glaip_sdk/utils/rendering/state.py +204 -0
  190. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  191. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  192. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  193. glaip_sdk/utils/rendering/steps/format.py +176 -0
  194. glaip_sdk/utils/rendering/{steps.py → steps/manager.py} +122 -26
  195. glaip_sdk/utils/rendering/timing.py +36 -0
  196. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  197. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  198. glaip_sdk/utils/resource_refs.py +29 -26
  199. glaip_sdk/utils/runtime_config.py +425 -0
  200. glaip_sdk/utils/serialization.py +32 -46
  201. glaip_sdk/utils/sync.py +162 -0
  202. glaip_sdk/utils/tool_detection.py +301 -0
  203. glaip_sdk/utils/tool_storage_provider.py +140 -0
  204. glaip_sdk/utils/validation.py +20 -28
  205. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/METADATA +78 -23
  206. glaip_sdk-0.7.7.dist-info/RECORD +213 -0
  207. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
  208. glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
  209. glaip_sdk-0.7.7.dist-info/top_level.txt +1 -0
  210. glaip_sdk/cli/commands/agents.py +0 -1412
  211. glaip_sdk/cli/commands/mcps.py +0 -1225
  212. glaip_sdk/cli/commands/tools.py +0 -597
  213. glaip_sdk/cli/utils.py +0 -1330
  214. glaip_sdk/models.py +0 -259
  215. glaip_sdk-0.0.20.dist-info/RECORD +0 -80
  216. glaip_sdk-0.0.20.dist-info/entry_points.txt +0 -3
@@ -1 +1,115 @@
1
- """Rendering utilities package (formatting, models, steps, debug)."""
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
+ ]
@@ -8,11 +8,17 @@ from __future__ import annotations
8
8
 
9
9
  import json
10
10
  import re
11
- import time
12
11
  from collections.abc import Callable
13
12
  from typing import Any
14
13
 
15
- from glaip_sdk.icons import ICON_AGENT_STEP, ICON_DELEGATE, ICON_TOOL_STEP
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
+ )
16
22
 
17
23
  # Constants for argument formatting
18
24
  DEFAULT_ARGS_MAX_LEN = 100
@@ -37,9 +43,15 @@ SECRET_VALUE_PATTERNS = [
37
43
  re.compile(r"eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+"), # JWT tokens
38
44
  ]
39
45
  SENSITIVE_PATTERNS = re.compile(
40
- r"password\s*[:=]\s*[^\s,}]+|secret\s*[:=]\s*[^\s,}]+|token\s*[:=]\s*[^\s,}]+|key\s*[:=]\s*[^\s,}]+|api_key\s*[:=]\s*[^\s,}]+|^password$|^secret$|^token$|^key$|^api_key$",
46
+ r"(?:password|secret|token|key|api_key)(?:\s*[:=]\s*[^\s,}]+)?",
41
47
  re.IGNORECASE,
42
48
  )
49
+ SECRET_MASK = "••••••"
50
+ STATUS_GLYPHS = {
51
+ "success": ICON_STATUS_SUCCESS,
52
+ "failed": ICON_STATUS_FAILED,
53
+ "warning": ICON_STATUS_WARNING,
54
+ }
43
55
 
44
56
 
45
57
  def _truncate_string(s: str, max_len: int) -> str:
@@ -53,7 +65,7 @@ def mask_secrets_in_string(text: str) -> str:
53
65
  """Mask sensitive information in a string."""
54
66
  result = text
55
67
  for pattern in SECRET_VALUE_PATTERNS:
56
- result = re.sub(pattern, "••••••", result)
68
+ result = re.sub(pattern, SECRET_MASK, result)
57
69
  return result
58
70
 
59
71
 
@@ -74,7 +86,7 @@ def _redact_dict_values(text: dict) -> dict:
74
86
  result = {}
75
87
  for key, value in text.items():
76
88
  if _is_sensitive_key(key):
77
- result[key] = "••••••"
89
+ result[key] = SECRET_MASK
78
90
  elif _should_recurse_redaction(value):
79
91
  result[key] = redact_sensitive(value)
80
92
  else:
@@ -92,11 +104,11 @@ def _redact_string_content(text: str) -> str:
92
104
  result = text
93
105
  # First mask secrets
94
106
  for pattern in SECRET_VALUE_PATTERNS:
95
- result = re.sub(pattern, "••••••", result)
107
+ result = re.sub(pattern, SECRET_MASK, result)
96
108
  # Then redact sensitive patterns
97
109
  result = re.sub(
98
110
  SENSITIVE_PATTERNS,
99
- lambda m: m.group(0).split("=")[0] + "=••••••",
111
+ lambda m: m.group(0).split("=")[0] + "=" + SECRET_MASK,
100
112
  result,
101
113
  )
102
114
  return result
@@ -105,15 +117,28 @@ def _redact_string_content(text: str) -> str:
105
117
  def _is_sensitive_key(key: str) -> bool:
106
118
  """Check if a key contains sensitive information."""
107
119
  key_lower = key.lower()
108
- return any(
109
- sensitive in key_lower
110
- for sensitive in ["password", "secret", "token", "key", "api_key"]
111
- )
120
+ return any(sensitive in key_lower for sensitive in ["password", "secret", "token", "key", "api_key"])
112
121
 
113
122
 
114
123
  def _should_recurse_redaction(value: Any) -> bool:
115
124
  """Check if a value should be recursively processed."""
116
- return isinstance(value, dict | list) or isinstance(value, str)
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"
117
142
 
118
143
 
119
144
  def pretty_args(args: dict | None, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
@@ -132,7 +157,7 @@ def pretty_args(args: dict | None, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
132
157
  try:
133
158
  args_str = json.dumps(masked_args, ensure_ascii=False, separators=(",", ":"))
134
159
  return _truncate_string(args_str, max_len)
135
- except (TypeError, ValueError, Exception):
160
+ except Exception:
136
161
  # Fallback to string representation if JSON serialization fails
137
162
  args_str = str(masked_args)
138
163
  return _truncate_string(args_str, max_len)
@@ -162,16 +187,6 @@ def pretty_out(output: any, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
162
187
  return _truncate_string(output_str, max_len)
163
188
 
164
189
 
165
- def get_spinner_char() -> str:
166
- """Get the next character for a spinner animation.
167
-
168
- Returns:
169
- A single character from the spinner frames based on current time
170
- """
171
- frames = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
172
- return frames[int(time.time() * 10) % len(frames)]
173
-
174
-
175
190
  def get_step_icon(step_kind: str) -> str:
176
191
  """Get the appropriate icon for a step kind."""
177
192
  if step_kind == "tool":
@@ -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
+ ]
@@ -6,6 +6,7 @@ Authors:
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+
9
10
  from rich.align import Align
10
11
  from rich.markdown import Markdown
11
12
  from rich.spinner import Spinner
@@ -128,9 +129,7 @@ def create_context_panel(
128
129
  )
129
130
 
130
131
 
131
- def create_final_panel(
132
- content: str, title: str = "Final Result", theme: str = "dark"
133
- ) -> AIPPanel:
132
+ def create_final_panel(content: str, title: str = "Final Result", theme: str = "dark") -> AIPPanel:
134
133
  """Create a final result panel.
135
134
 
136
135
  Args:
@@ -147,3 +146,11 @@ def create_final_panel(
147
146
  border_style=SUCCESS,
148
147
  padding=(0, 1),
149
148
  )
149
+
150
+
151
+ __all__ = [
152
+ "create_main_panel",
153
+ "create_tool_panel",
154
+ "create_context_panel",
155
+ "create_final_panel",
156
+ ]
@@ -7,8 +7,22 @@ Authors:
7
7
  from __future__ import annotations
8
8
 
9
9
  from time import monotonic
10
+ from typing import Any
10
11
 
11
- from glaip_sdk.utils.rendering.formatting import get_spinner_char
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()
12
26
 
13
27
 
14
28
  def get_spinner() -> str:
@@ -16,6 +30,33 @@ def get_spinner() -> str:
16
30
  return get_spinner_char()
17
31
 
18
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
+
19
60
  def _resolve_elapsed_time(
20
61
  started_at: float | None,
21
62
  server_elapsed_time: float | None,
@@ -48,15 +89,11 @@ def format_working_indicator(
48
89
  """Format a working indicator with elapsed time."""
49
90
  base_message = "Working..."
50
91
 
51
- if started_at is None and (
52
- server_elapsed_time is None or streaming_started_at is None
53
- ):
92
+ if started_at is None and (server_elapsed_time is None or streaming_started_at is None):
54
93
  return base_message
55
94
 
56
95
  spinner_chip = f"{get_spinner_char()} {base_message}"
57
- elapsed = _resolve_elapsed_time(
58
- started_at, server_elapsed_time, streaming_started_at
59
- )
96
+ elapsed = _resolve_elapsed_time(started_at, server_elapsed_time, streaming_started_at)
60
97
  if elapsed is None:
61
98
  return spinner_chip
62
99
 
@@ -93,11 +130,7 @@ def is_delegation_tool(tool_name: str) -> bool:
93
130
  Returns:
94
131
  True if this is a delegation tool
95
132
  """
96
- return (
97
- tool_name.startswith("delegate_to_")
98
- or tool_name.startswith("delegate_")
99
- or "sub_agent" in tool_name.lower()
100
- )
133
+ return tool_name.startswith("delegate_to_") or tool_name.startswith("delegate_") or "sub_agent" in tool_name.lower()
101
134
 
102
135
 
103
136
  def _delegation_tool_title(tool_name: str) -> str | None:
@@ -139,3 +172,31 @@ def format_tool_title(tool_name: str) -> str:
139
172
 
140
173
  # Convert snake_case to Title Case
141
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))
@@ -0,0 +1,74 @@
1
+ """Summary panel helpers shared between renderer and viewer.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Mapping
10
+ from typing import Any
11
+
12
+ from glaip_sdk.utils.rendering.layout.transcript import (
13
+ DEFAULT_TRANSCRIPT_THEME,
14
+ build_transcript_snapshot,
15
+ build_transcript_view,
16
+ normalise_meta_payload,
17
+ )
18
+ from glaip_sdk.utils.rendering.state import RendererState
19
+ from glaip_sdk.utils.rendering.steps import StepManager
20
+
21
+
22
+ def render_summary_panels(
23
+ state: RendererState,
24
+ steps: StepManager,
25
+ *,
26
+ theme: str | None = None,
27
+ summary_window: int | None = None,
28
+ include_query_panel: bool = True,
29
+ include_final_panel: bool = True,
30
+ step_status_overrides: dict[str, str] | None = None,
31
+ ) -> list[Any]:
32
+ """Return shared summary panels for renderer and offline viewer."""
33
+ resolved_theme = theme or DEFAULT_TRANSCRIPT_THEME
34
+ snapshot_source = state.to_snapshot() if hasattr(state, "to_snapshot") else state
35
+ if isinstance(snapshot_source, Mapping):
36
+ raw_meta = snapshot_source.get("meta")
37
+ else:
38
+ raw_meta = getattr(state, "meta", None)
39
+ snapshot_meta = normalise_meta_payload(raw_meta)
40
+ snapshot = build_transcript_snapshot(
41
+ snapshot_source,
42
+ steps,
43
+ meta=snapshot_meta,
44
+ summary_window=summary_window,
45
+ theme=resolved_theme,
46
+ step_status_overrides=step_status_overrides,
47
+ )
48
+ _header, body = build_transcript_view(snapshot, theme=resolved_theme)
49
+
50
+ return [
51
+ renderable
52
+ for renderable in body
53
+ if _should_include_summary_panel(
54
+ renderable,
55
+ include_query_panel=include_query_panel,
56
+ include_final_panel=include_final_panel,
57
+ )
58
+ ]
59
+
60
+
61
+ def _should_include_summary_panel(
62
+ renderable: Any,
63
+ *,
64
+ include_query_panel: bool,
65
+ include_final_panel: bool,
66
+ ) -> bool:
67
+ """Return True when the panel should be included in the summary list."""
68
+ title = getattr(renderable, "title", "")
69
+ normalised = title.lower() if isinstance(title, str) else ""
70
+ if not include_query_panel and normalised == "user request":
71
+ return False
72
+ if not include_final_panel and normalised.startswith("final result"):
73
+ return False
74
+ return True