glaip-sdk 0.1.0__py3-none-any.whl → 0.6.10__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 (156) hide show
  1. glaip_sdk/__init__.py +5 -2
  2. glaip_sdk/_version.py +10 -3
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1191 -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 +265 -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.py +251 -173
  12. glaip_sdk/cli/commands/common_config.py +101 -0
  13. glaip_sdk/cli/commands/configure.py +735 -143
  14. glaip_sdk/cli/commands/mcps.py +266 -134
  15. glaip_sdk/cli/commands/models.py +13 -9
  16. glaip_sdk/cli/commands/tools.py +67 -88
  17. glaip_sdk/cli/commands/transcripts.py +755 -0
  18. glaip_sdk/cli/commands/update.py +3 -8
  19. glaip_sdk/cli/config.py +49 -7
  20. glaip_sdk/cli/constants.py +38 -0
  21. glaip_sdk/cli/context.py +8 -0
  22. glaip_sdk/cli/core/__init__.py +79 -0
  23. glaip_sdk/cli/core/context.py +124 -0
  24. glaip_sdk/cli/core/output.py +846 -0
  25. glaip_sdk/cli/core/prompting.py +649 -0
  26. glaip_sdk/cli/core/rendering.py +187 -0
  27. glaip_sdk/cli/display.py +45 -32
  28. glaip_sdk/cli/hints.py +57 -0
  29. glaip_sdk/cli/io.py +14 -17
  30. glaip_sdk/cli/main.py +232 -143
  31. glaip_sdk/cli/masking.py +21 -33
  32. glaip_sdk/cli/mcp_validators.py +5 -15
  33. glaip_sdk/cli/pager.py +12 -19
  34. glaip_sdk/cli/parsers/__init__.py +1 -3
  35. glaip_sdk/cli/parsers/json_input.py +11 -22
  36. glaip_sdk/cli/resolution.py +3 -9
  37. glaip_sdk/cli/rich_helpers.py +1 -3
  38. glaip_sdk/cli/slash/__init__.py +0 -9
  39. glaip_sdk/cli/slash/accounts_controller.py +578 -0
  40. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  41. glaip_sdk/cli/slash/agent_session.py +65 -29
  42. glaip_sdk/cli/slash/prompt.py +24 -10
  43. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  44. glaip_sdk/cli/slash/session.py +807 -225
  45. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  46. glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
  47. glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
  48. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  49. glaip_sdk/cli/slash/tui/loading.py +58 -0
  50. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  51. glaip_sdk/cli/transcript/__init__.py +12 -52
  52. glaip_sdk/cli/transcript/cache.py +258 -60
  53. glaip_sdk/cli/transcript/capture.py +72 -21
  54. glaip_sdk/cli/transcript/history.py +815 -0
  55. glaip_sdk/cli/transcript/launcher.py +1 -3
  56. glaip_sdk/cli/transcript/viewer.py +79 -499
  57. glaip_sdk/cli/update_notifier.py +177 -24
  58. glaip_sdk/cli/utils.py +242 -1308
  59. glaip_sdk/cli/validators.py +16 -18
  60. glaip_sdk/client/__init__.py +2 -1
  61. glaip_sdk/client/_agent_payloads.py +53 -37
  62. glaip_sdk/client/agent_runs.py +147 -0
  63. glaip_sdk/client/agents.py +320 -92
  64. glaip_sdk/client/base.py +78 -35
  65. glaip_sdk/client/main.py +19 -10
  66. glaip_sdk/client/mcps.py +123 -15
  67. glaip_sdk/client/run_rendering.py +136 -101
  68. glaip_sdk/client/shared.py +21 -0
  69. glaip_sdk/client/tools.py +163 -34
  70. glaip_sdk/client/validators.py +20 -48
  71. glaip_sdk/config/constants.py +11 -0
  72. glaip_sdk/exceptions.py +1 -3
  73. glaip_sdk/mcps/__init__.py +21 -0
  74. glaip_sdk/mcps/base.py +345 -0
  75. glaip_sdk/models/__init__.py +90 -0
  76. glaip_sdk/models/agent.py +47 -0
  77. glaip_sdk/models/agent_runs.py +116 -0
  78. glaip_sdk/models/common.py +42 -0
  79. glaip_sdk/models/mcp.py +33 -0
  80. glaip_sdk/models/tool.py +33 -0
  81. glaip_sdk/payload_schemas/__init__.py +1 -13
  82. glaip_sdk/payload_schemas/agent.py +1 -3
  83. glaip_sdk/registry/__init__.py +55 -0
  84. glaip_sdk/registry/agent.py +164 -0
  85. glaip_sdk/registry/base.py +139 -0
  86. glaip_sdk/registry/mcp.py +253 -0
  87. glaip_sdk/registry/tool.py +232 -0
  88. glaip_sdk/rich_components.py +58 -2
  89. glaip_sdk/runner/__init__.py +59 -0
  90. glaip_sdk/runner/base.py +84 -0
  91. glaip_sdk/runner/deps.py +115 -0
  92. glaip_sdk/runner/langgraph.py +706 -0
  93. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  94. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  95. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  96. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  97. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  98. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  99. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
  100. glaip_sdk/tools/__init__.py +22 -0
  101. glaip_sdk/tools/base.py +435 -0
  102. glaip_sdk/utils/__init__.py +58 -12
  103. glaip_sdk/utils/a2a/__init__.py +34 -0
  104. glaip_sdk/utils/a2a/event_processor.py +188 -0
  105. glaip_sdk/utils/agent_config.py +4 -14
  106. glaip_sdk/utils/bundler.py +267 -0
  107. glaip_sdk/utils/client.py +111 -0
  108. glaip_sdk/utils/client_utils.py +46 -28
  109. glaip_sdk/utils/datetime_helpers.py +58 -0
  110. glaip_sdk/utils/discovery.py +78 -0
  111. glaip_sdk/utils/display.py +25 -21
  112. glaip_sdk/utils/export.py +143 -0
  113. glaip_sdk/utils/general.py +1 -36
  114. glaip_sdk/utils/import_export.py +15 -16
  115. glaip_sdk/utils/import_resolver.py +492 -0
  116. glaip_sdk/utils/instructions.py +101 -0
  117. glaip_sdk/utils/rendering/__init__.py +115 -1
  118. glaip_sdk/utils/rendering/formatting.py +7 -35
  119. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  120. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
  121. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
  122. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  123. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  124. glaip_sdk/utils/rendering/models.py +3 -6
  125. glaip_sdk/utils/rendering/renderer/__init__.py +9 -49
  126. glaip_sdk/utils/rendering/renderer/base.py +258 -1577
  127. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  128. glaip_sdk/utils/rendering/renderer/debug.py +30 -34
  129. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  130. glaip_sdk/utils/rendering/renderer/stream.py +10 -51
  131. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  132. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  133. glaip_sdk/utils/rendering/renderer/toggle.py +1 -3
  134. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  135. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  136. glaip_sdk/utils/rendering/state.py +204 -0
  137. glaip_sdk/utils/rendering/step_tree_state.py +1 -3
  138. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  139. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +76 -517
  140. glaip_sdk/utils/rendering/steps/format.py +176 -0
  141. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  142. glaip_sdk/utils/rendering/timing.py +36 -0
  143. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  144. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  145. glaip_sdk/utils/resource_refs.py +29 -26
  146. glaip_sdk/utils/runtime_config.py +425 -0
  147. glaip_sdk/utils/serialization.py +32 -46
  148. glaip_sdk/utils/sync.py +142 -0
  149. glaip_sdk/utils/tool_detection.py +33 -0
  150. glaip_sdk/utils/validation.py +20 -28
  151. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/METADATA +42 -4
  152. glaip_sdk-0.6.10.dist-info/RECORD +159 -0
  153. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/WHEEL +1 -1
  154. glaip_sdk/models.py +0 -259
  155. glaip_sdk-0.1.0.dist-info/RECORD +0 -82
  156. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/entry_points.txt +0 -0
@@ -8,7 +8,6 @@ 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
 
@@ -47,11 +46,6 @@ SENSITIVE_PATTERNS = re.compile(
47
46
  r"(?:password|secret|token|key|api_key)(?:\s*[:=]\s*[^\s,}]+)?",
48
47
  re.IGNORECASE,
49
48
  )
50
- CONNECTOR_VERTICAL = "│ "
51
- CONNECTOR_EMPTY = " "
52
- CONNECTOR_BRANCH = "├─ "
53
- CONNECTOR_LAST = "└─ "
54
- ROOT_MARKER = ""
55
49
  SECRET_MASK = "••••••"
56
50
  STATUS_GLYPHS = {
57
51
  "success": ICON_STATUS_SUCCESS,
@@ -123,15 +117,12 @@ def _redact_string_content(text: str) -> str:
123
117
  def _is_sensitive_key(key: str) -> bool:
124
118
  """Check if a key contains sensitive information."""
125
119
  key_lower = key.lower()
126
- return any(
127
- sensitive in key_lower
128
- for sensitive in ["password", "secret", "token", "key", "api_key"]
129
- )
120
+ return any(sensitive in key_lower for sensitive in ["password", "secret", "token", "key", "api_key"])
130
121
 
131
122
 
132
123
  def _should_recurse_redaction(value: Any) -> bool:
133
124
  """Check if a value should be recursively processed."""
134
- return isinstance(value, dict | list) or isinstance(value, str)
125
+ return isinstance(value, (dict, list)) or isinstance(value, str)
135
126
 
136
127
 
137
128
  def glyph_for_status(icon_key: str | None) -> str | None:
@@ -143,20 +134,11 @@ def glyph_for_status(icon_key: str | None) -> str | None:
143
134
 
144
135
  def normalise_display_label(label: str | None) -> str:
145
136
  """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)
137
+ if not isinstance(label, str):
138
+ text = ""
139
+ else:
140
+ text = label.strip()
141
+ return text or "Unknown step detail"
160
142
 
161
143
 
162
144
  def pretty_args(args: dict | None, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
@@ -205,16 +187,6 @@ def pretty_out(output: any, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
205
187
  return _truncate_string(output_str, max_len)
206
188
 
207
189
 
208
- def get_spinner_char() -> str:
209
- """Get the next character for a spinner animation.
210
-
211
- Returns:
212
- A single character from the spinner frames based on current time
213
- """
214
- frames = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
215
- return frames[int(time.time() * 10) % len(frames)]
216
-
217
-
218
190
  def get_step_icon(step_kind: str) -> str:
219
191
  """Get the appropriate icon for a step kind."""
220
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