glaip-sdk 0.0.7__py3-none-any.whl → 0.6.5b6__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/__init__.py +6 -3
- glaip_sdk/_version.py +12 -5
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1126 -0
- glaip_sdk/branding.py +79 -15
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +699 -0
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +503 -183
- glaip_sdk/cli/commands/common_config.py +101 -0
- glaip_sdk/cli/commands/configure.py +774 -137
- glaip_sdk/cli/commands/mcps.py +1124 -181
- glaip_sdk/cli/commands/models.py +25 -10
- glaip_sdk/cli/commands/tools.py +144 -92
- 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 +846 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +143 -53
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +24 -18
- glaip_sdk/cli/main.py +420 -145
- 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 +28 -21
- glaip_sdk/cli/rich_helpers.py +27 -0
- glaip_sdk/cli/slash/__init__.py +15 -0
- glaip_sdk/cli/slash/accounts_controller.py +500 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +282 -0
- glaip_sdk/cli/slash/prompt.py +245 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +1679 -0
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +872 -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 +372 -0
- glaip_sdk/cli/update_notifier.py +290 -0
- glaip_sdk/cli/utils.py +247 -1238
- glaip_sdk/cli/validators.py +16 -18
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +520 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +940 -574
- glaip_sdk/client/base.py +163 -48
- glaip_sdk/client/main.py +35 -12
- glaip_sdk/client/mcps.py +126 -18
- glaip_sdk/client/run_rendering.py +415 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +195 -37
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/config/constants.py +15 -5
- glaip_sdk/exceptions.py +16 -9
- glaip_sdk/icons.py +25 -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 +231 -0
- glaip_sdk/rich_components.py +98 -2
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +115 -0
- glaip_sdk/runner/langgraph.py +597 -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 +158 -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 +177 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +59 -13
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +53 -40
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +58 -26
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +65 -32
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +1 -36
- glaip_sdk/utils/import_export.py +20 -25
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +85 -43
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +51 -19
- 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 +39 -7
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
- glaip_sdk/utils/rendering/renderer/base.py +672 -759
- glaip_sdk/utils/rendering/renderer/config.py +4 -10
- glaip_sdk/utils/rendering/renderer/debug.py +75 -22
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +13 -54
- 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 +29 -26
- glaip_sdk/utils/runtime_config.py +422 -0
- glaip_sdk/utils/serialization.py +184 -51
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +21 -30
- {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/METADATA +58 -12
- glaip_sdk-0.6.5b6.dist-info/RECORD +159 -0
- {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/WHEEL +1 -1
- glaip_sdk/models.py +0 -250
- glaip_sdk/utils/rendering/renderer/progress.py +0 -118
- glaip_sdk/utils/rendering/steps.py +0 -232
- glaip_sdk/utils/rich_utils.py +0 -29
- glaip_sdk-0.0.7.dist-info/RECORD +0 -55
- {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/entry_points.txt +0 -0
|
@@ -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))
|
|
@@ -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
|