soothe-cli 0.1.0__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.
- soothe_cli/__init__.py +5 -0
- soothe_cli/cli/__init__.py +1 -0
- soothe_cli/cli/commands/__init__.py +1 -0
- soothe_cli/cli/commands/autopilot_cmd.py +410 -0
- soothe_cli/cli/commands/config_cmd.py +277 -0
- soothe_cli/cli/commands/run_cmd.py +87 -0
- soothe_cli/cli/commands/status_cmd.py +121 -0
- soothe_cli/cli/commands/subagent_names.py +17 -0
- soothe_cli/cli/commands/thread_cmd.py +657 -0
- soothe_cli/cli/execution/__init__.py +6 -0
- soothe_cli/cli/execution/daemon.py +194 -0
- soothe_cli/cli/execution/headless.py +99 -0
- soothe_cli/cli/execution/launcher.py +31 -0
- soothe_cli/cli/main.py +509 -0
- soothe_cli/cli/renderer.py +444 -0
- soothe_cli/cli/stream/__init__.py +17 -0
- soothe_cli/cli/stream/context.py +138 -0
- soothe_cli/cli/stream/display_line.py +83 -0
- soothe_cli/cli/stream/formatter.py +412 -0
- soothe_cli/cli/stream/pipeline.py +521 -0
- soothe_cli/cli/utils.py +46 -0
- soothe_cli/config/__init__.py +5 -0
- soothe_cli/config/cli_config.py +155 -0
- soothe_cli/plan/__init__.py +5 -0
- soothe_cli/plan/rich_tree.py +54 -0
- soothe_cli/shared/__init__.py +107 -0
- soothe_cli/shared/command_router.py +246 -0
- soothe_cli/shared/config_loader.py +68 -0
- soothe_cli/shared/display_policy.py +413 -0
- soothe_cli/shared/essential_events.py +68 -0
- soothe_cli/shared/event_processor.py +823 -0
- soothe_cli/shared/message_processing.py +393 -0
- soothe_cli/shared/presentation_engine.py +173 -0
- soothe_cli/shared/processor_state.py +80 -0
- soothe_cli/shared/renderer_protocol.py +158 -0
- soothe_cli/shared/rendering.py +43 -0
- soothe_cli/shared/slash_commands.py +354 -0
- soothe_cli/shared/subagent_routing.py +63 -0
- soothe_cli/shared/suppression_state.py +188 -0
- soothe_cli/shared/tool_formatters/__init__.py +27 -0
- soothe_cli/shared/tool_formatters/base.py +109 -0
- soothe_cli/shared/tool_formatters/execution.py +297 -0
- soothe_cli/shared/tool_formatters/fallback.py +128 -0
- soothe_cli/shared/tool_formatters/file_ops.py +299 -0
- soothe_cli/shared/tool_formatters/goal_formatter.py +331 -0
- soothe_cli/shared/tool_formatters/media.py +291 -0
- soothe_cli/shared/tool_formatters/structured.py +202 -0
- soothe_cli/shared/tool_formatters/web.py +143 -0
- soothe_cli/shared/tool_output_formatter.py +227 -0
- soothe_cli/shared/tui_trace_log.py +40 -0
- soothe_cli/tui/__init__.py +5 -0
- soothe_cli/tui/_ask_user_types.py +50 -0
- soothe_cli/tui/_cli_context.py +27 -0
- soothe_cli/tui/_env_vars.py +56 -0
- soothe_cli/tui/_session_stats.py +114 -0
- soothe_cli/tui/_version.py +21 -0
- soothe_cli/tui/app.py +4992 -0
- soothe_cli/tui/app.tcss +302 -0
- soothe_cli/tui/command_registry.py +310 -0
- soothe_cli/tui/config.py +2381 -0
- soothe_cli/tui/daemon_session.py +233 -0
- soothe_cli/tui/file_ops.py +409 -0
- soothe_cli/tui/formatting.py +28 -0
- soothe_cli/tui/hooks.py +23 -0
- soothe_cli/tui/input.py +782 -0
- soothe_cli/tui/media_utils.py +471 -0
- soothe_cli/tui/model_config.py +518 -0
- soothe_cli/tui/output.py +69 -0
- soothe_cli/tui/project_utils.py +188 -0
- soothe_cli/tui/sessions.py +1248 -0
- soothe_cli/tui/skills/__init__.py +5 -0
- soothe_cli/tui/skills/invocation.py +74 -0
- soothe_cli/tui/skills/load.py +93 -0
- soothe_cli/tui/textual_adapter.py +1430 -0
- soothe_cli/tui/theme.py +838 -0
- soothe_cli/tui/tool_display.py +297 -0
- soothe_cli/tui/unicode_security.py +502 -0
- soothe_cli/tui/update_check.py +447 -0
- soothe_cli/tui/widgets/__init__.py +9 -0
- soothe_cli/tui/widgets/_links.py +63 -0
- soothe_cli/tui/widgets/approval.py +430 -0
- soothe_cli/tui/widgets/ask_user.py +392 -0
- soothe_cli/tui/widgets/autocomplete.py +666 -0
- soothe_cli/tui/widgets/autopilot_dashboard.py +308 -0
- soothe_cli/tui/widgets/autopilot_screen.py +64 -0
- soothe_cli/tui/widgets/chat_input.py +1834 -0
- soothe_cli/tui/widgets/clipboard.py +128 -0
- soothe_cli/tui/widgets/diff.py +240 -0
- soothe_cli/tui/widgets/editor.py +140 -0
- soothe_cli/tui/widgets/history.py +221 -0
- soothe_cli/tui/widgets/loading.py +194 -0
- soothe_cli/tui/widgets/mcp_viewer.py +352 -0
- soothe_cli/tui/widgets/message_store.py +693 -0
- soothe_cli/tui/widgets/messages.py +1720 -0
- soothe_cli/tui/widgets/model_selector.py +988 -0
- soothe_cli/tui/widgets/notification_settings.py +155 -0
- soothe_cli/tui/widgets/status.py +403 -0
- soothe_cli/tui/widgets/theme_selector.py +158 -0
- soothe_cli/tui/widgets/thread_selector.py +1865 -0
- soothe_cli/tui/widgets/tool_renderers.py +148 -0
- soothe_cli/tui/widgets/tool_widgets.py +254 -0
- soothe_cli/tui/widgets/tools.py +165 -0
- soothe_cli/tui/widgets/welcome.py +330 -0
- soothe_cli-0.1.0.dist-info/METADATA +100 -0
- soothe_cli-0.1.0.dist-info/RECORD +107 -0
- soothe_cli-0.1.0.dist-info/WHEEL +4 -0
- soothe_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""Unified Display Policy Module for CLI and TUI.
|
|
2
|
+
|
|
3
|
+
This module centralizes all event filtering, content processing, and display
|
|
4
|
+
policy decisions in one place. Both CLI and TUI renderers use this policy
|
|
5
|
+
to determine:
|
|
6
|
+
|
|
7
|
+
1. Which events to show/hide based on verbosity
|
|
8
|
+
2. Which content to filter from assistant text
|
|
9
|
+
3. Which message types are internal vs user-facing
|
|
10
|
+
4. How to handle different event categories
|
|
11
|
+
|
|
12
|
+
Design Principles:
|
|
13
|
+
- Event-based filtering over content-based filtering
|
|
14
|
+
- Explicit policy rules over implicit pattern matching
|
|
15
|
+
- Centralized configuration for consistency
|
|
16
|
+
- Easy to extend without modifying multiple files
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
from soothe_cli.shared.display_policy import DisplayPolicy
|
|
20
|
+
|
|
21
|
+
policy = DisplayPolicy(verbosity="normal")
|
|
22
|
+
|
|
23
|
+
if policy.should_show_event(event_type, data):
|
|
24
|
+
render_event(data)
|
|
25
|
+
|
|
26
|
+
if policy.should_show_assistant_text(text, is_main=True):
|
|
27
|
+
display_text(policy.filter_content(text))
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import re
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
from soothe_sdk.internal import (
|
|
37
|
+
INTERNAL_JSON_KEYS,
|
|
38
|
+
filter_confused_responses,
|
|
39
|
+
filter_json_code_blocks,
|
|
40
|
+
filter_plain_json,
|
|
41
|
+
filter_search_data_tags,
|
|
42
|
+
normalize_internal_whitespace,
|
|
43
|
+
)
|
|
44
|
+
from soothe_sdk.verbosity import (
|
|
45
|
+
VerbosityLevel,
|
|
46
|
+
VerbosityTier,
|
|
47
|
+
classify_event_to_tier,
|
|
48
|
+
should_show,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# =============================================================================
|
|
52
|
+
# Type Definitions
|
|
53
|
+
# =============================================================================
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def normalize_verbosity(verbosity: str) -> VerbosityLevel:
|
|
57
|
+
"""Normalize external verbosity values to canonical internal names."""
|
|
58
|
+
if verbosity == "minimal":
|
|
59
|
+
return "normal"
|
|
60
|
+
if verbosity in {"quiet", "normal", "detailed", "debug"}:
|
|
61
|
+
return verbosity
|
|
62
|
+
return "normal"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# =============================================================================
|
|
66
|
+
# Policy Configuration Constants
|
|
67
|
+
# =============================================================================
|
|
68
|
+
|
|
69
|
+
# Event types that should NEVER be shown (internal implementation details)
|
|
70
|
+
INTERNAL_EVENT_TYPES = frozenset(
|
|
71
|
+
{
|
|
72
|
+
"soothe.capability.research.internal_llm.run",
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Event types to skip in progress display (handled by plan update mechanism or not rendered)
|
|
77
|
+
SKIP_EVENT_TYPES = frozenset(
|
|
78
|
+
{
|
|
79
|
+
# Plan events handled by renderer's plan update mechanism
|
|
80
|
+
"soothe.cognition.plan.batch.started",
|
|
81
|
+
"soothe.cognition.plan.step.started",
|
|
82
|
+
"soothe.cognition.plan.step.completed",
|
|
83
|
+
"soothe.cognition.plan.step.failed",
|
|
84
|
+
# Policy events not rendered (RFC-0019)
|
|
85
|
+
"soothe.protocol.policy.checked",
|
|
86
|
+
"soothe.protocol.policy.denied",
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
PLAN_EVENT_TYPES = frozenset(
|
|
91
|
+
{
|
|
92
|
+
"soothe.cognition.plan.created",
|
|
93
|
+
"soothe.cognition.plan.reflected",
|
|
94
|
+
"soothe.cognition.plan.step.started",
|
|
95
|
+
"soothe.cognition.plan.step.completed",
|
|
96
|
+
"soothe.cognition.plan.step.failed",
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
MILESTONE_EVENT_TYPES = frozenset(
|
|
101
|
+
{
|
|
102
|
+
"soothe.cognition.plan.step.completed",
|
|
103
|
+
"soothe.cognition.plan.step.failed",
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
QUIET_SENTENCE_MAX_LEN = 120
|
|
108
|
+
QUIET_FALLBACK_MAX_LEN = 160
|
|
109
|
+
QUIET_TRUNCATED_MAX_LEN = 157
|
|
110
|
+
TRAILING_EMBELLISHMENT_WORDS = frozenset(
|
|
111
|
+
{
|
|
112
|
+
"beautiful",
|
|
113
|
+
"historic",
|
|
114
|
+
"wonderful",
|
|
115
|
+
"amazing",
|
|
116
|
+
"great",
|
|
117
|
+
"lovely",
|
|
118
|
+
"fantastic",
|
|
119
|
+
"famous",
|
|
120
|
+
"vibrant",
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
DECORATIVE_FILLER_PATTERNS = (
|
|
125
|
+
r"\n?\s*Let me know if you(?:'d| would)? like .*?$",
|
|
126
|
+
r"\n?\s*If you(?:'d| would) like, I can .*?$",
|
|
127
|
+
r"\n?\s*Feel free to ask if .*?$",
|
|
128
|
+
r"\n?\s*I(?:'m| am) happy to help you with .*?$",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# =============================================================================
|
|
133
|
+
# Display Policy Class
|
|
134
|
+
# =============================================================================
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class DisplayPolicy:
|
|
139
|
+
"""Unified display policy for CLI and TUI.
|
|
140
|
+
|
|
141
|
+
This class centralizes all decisions about what to show/hide,
|
|
142
|
+
what content to filter, and how to process events for display.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
verbosity: VerbosityLevel = "normal"
|
|
146
|
+
|
|
147
|
+
def __post_init__(self) -> None:
|
|
148
|
+
"""Normalize compatibility aliases after initialization."""
|
|
149
|
+
self.verbosity = normalize_verbosity(self.verbosity)
|
|
150
|
+
|
|
151
|
+
# Track internal context state
|
|
152
|
+
internal_context_active: bool = field(default=False, repr=False)
|
|
153
|
+
internal_context_types: set[str] = field(default_factory=set, repr=False)
|
|
154
|
+
|
|
155
|
+
# ==========================================================================
|
|
156
|
+
# Event Filtering
|
|
157
|
+
# ==========================================================================
|
|
158
|
+
|
|
159
|
+
def should_show_event(
|
|
160
|
+
self,
|
|
161
|
+
event_type: str,
|
|
162
|
+
data: dict[str, Any] | None = None, # noqa: ARG002
|
|
163
|
+
namespace: tuple[str, ...] = (),
|
|
164
|
+
) -> bool:
|
|
165
|
+
"""Determine if an event should be displayed.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
event_type: The event type string (e.g., "soothe.tool.research.analyze")
|
|
169
|
+
data: Optional event data dict
|
|
170
|
+
namespace: Subagent namespace tuple
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
True if the event should be shown, False otherwise
|
|
174
|
+
"""
|
|
175
|
+
# Internal events are NEVER shown
|
|
176
|
+
if event_type in INTERNAL_EVENT_TYPES:
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
# Skip certain event types (handled by plan update mechanism)
|
|
180
|
+
if event_type in SKIP_EVENT_TYPES:
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
# Classify and check verbosity
|
|
184
|
+
tier = self._classify_event(event_type, namespace)
|
|
185
|
+
return self._should_show_tier(tier)
|
|
186
|
+
|
|
187
|
+
def _classify_event(
|
|
188
|
+
self,
|
|
189
|
+
event_type: str,
|
|
190
|
+
namespace: tuple[str, ...] = (),
|
|
191
|
+
) -> VerbosityTier:
|
|
192
|
+
"""Classify an event directly to a VerbosityTier."""
|
|
193
|
+
return classify_event_to_tier(event_type, namespace)
|
|
194
|
+
|
|
195
|
+
def _should_show_tier(self, tier: VerbosityTier) -> bool:
|
|
196
|
+
"""Check if a tier should be shown at current verbosity."""
|
|
197
|
+
return should_show(tier, self.verbosity)
|
|
198
|
+
|
|
199
|
+
# ==========================================================================
|
|
200
|
+
# Internal Context Tracking
|
|
201
|
+
# ==========================================================================
|
|
202
|
+
|
|
203
|
+
def enter_internal_context(self, context_type: str) -> None:
|
|
204
|
+
"""Mark entry into an internal processing context.
|
|
205
|
+
|
|
206
|
+
Call this when starting internal LLM calls (e.g., research analysis).
|
|
207
|
+
"""
|
|
208
|
+
self.internal_context_active = True
|
|
209
|
+
self.internal_context_types.add(context_type)
|
|
210
|
+
|
|
211
|
+
def exit_internal_context(self) -> None:
|
|
212
|
+
"""Mark exit from internal processing context."""
|
|
213
|
+
self.internal_context_active = False
|
|
214
|
+
self.internal_context_types.clear()
|
|
215
|
+
|
|
216
|
+
def is_in_internal_context(self) -> bool:
|
|
217
|
+
"""Check if currently in an internal processing context."""
|
|
218
|
+
return self.internal_context_active
|
|
219
|
+
|
|
220
|
+
# ==========================================================================
|
|
221
|
+
# Assistant Text Filtering
|
|
222
|
+
# ==========================================================================
|
|
223
|
+
|
|
224
|
+
def should_show_assistant_text(
|
|
225
|
+
self,
|
|
226
|
+
text: str, # noqa: ARG002
|
|
227
|
+
*,
|
|
228
|
+
is_main: bool,
|
|
229
|
+
is_multi_step_active: bool = False,
|
|
230
|
+
) -> bool:
|
|
231
|
+
"""Determine if assistant text should be displayed.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
text: The text content
|
|
235
|
+
is_main: True if from main agent
|
|
236
|
+
is_multi_step_active: True if in multi-step plan execution
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
True if the text should be shown
|
|
240
|
+
"""
|
|
241
|
+
# During internal context, suppress non-main agent text
|
|
242
|
+
if self.internal_context_active and not is_main:
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
# During multi-step plans, suppress intermediate main agent text
|
|
246
|
+
if is_multi_step_active and is_main:
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
# Check verbosity
|
|
250
|
+
return self._should_show_tier(VerbosityTier.QUIET)
|
|
251
|
+
|
|
252
|
+
def filter_content(self, text: str, *, preserve_boundary_whitespace: bool = False) -> str:
|
|
253
|
+
"""Filter internal content from text for display.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
text: Text to filter.
|
|
257
|
+
preserve_boundary_whitespace: If True, preserve leading/trailing whitespace
|
|
258
|
+
for proper streaming chunk concatenation.
|
|
259
|
+
"""
|
|
260
|
+
# Preserve leading/trailing whitespace for streaming chunks
|
|
261
|
+
if preserve_boundary_whitespace:
|
|
262
|
+
leading_ws = len(text) - len(text.lstrip())
|
|
263
|
+
trailing_ws = len(text) - len(text.rstrip())
|
|
264
|
+
lead = text[:leading_ws]
|
|
265
|
+
trail = text[len(text) - trailing_ws :] if trailing_ws > 0 else ""
|
|
266
|
+
|
|
267
|
+
text = filter_json_code_blocks(text)
|
|
268
|
+
text = filter_plain_json(text)
|
|
269
|
+
text = filter_confused_responses(text)
|
|
270
|
+
text = filter_search_data_tags(text)
|
|
271
|
+
text = self._filter_decorative_filler(text)
|
|
272
|
+
text = normalize_internal_whitespace(text)
|
|
273
|
+
text = self._strip_sentence_embellishment(text)
|
|
274
|
+
text = self._normalize_factual_ending(text)
|
|
275
|
+
|
|
276
|
+
if preserve_boundary_whitespace:
|
|
277
|
+
# Restore boundary whitespace for streaming concatenation
|
|
278
|
+
return lead + text.strip() + trail
|
|
279
|
+
return text.strip()
|
|
280
|
+
|
|
281
|
+
def _filter_decorative_filler(self, text: str) -> str:
|
|
282
|
+
"""Remove polite trailing filler that adds no user value."""
|
|
283
|
+
for pattern in DECORATIVE_FILLER_PATTERNS:
|
|
284
|
+
text = re.sub(pattern, "", text, flags=re.IGNORECASE | re.MULTILINE)
|
|
285
|
+
return text
|
|
286
|
+
|
|
287
|
+
def extract_quiet_answer(self, text: str) -> str:
|
|
288
|
+
"""Extract a compact answer for quiet mode with safe fallback."""
|
|
289
|
+
cleaned = self.filter_content(text)
|
|
290
|
+
if not cleaned:
|
|
291
|
+
return ""
|
|
292
|
+
|
|
293
|
+
single_line = re.sub(r"\s+", " ", cleaned).strip()
|
|
294
|
+
if re.fullmatch(r"[-+]?\d+(?:\.\d+)?", single_line):
|
|
295
|
+
return single_line
|
|
296
|
+
|
|
297
|
+
if re.fullmatch(
|
|
298
|
+
r"[-+]?\d+(?:\.\d+)?\s*[+\-*/]\s*[-+]?\d+(?:\.\d+)?\s*=\s*([-+]?\d+(?:\.\d+)?)",
|
|
299
|
+
single_line,
|
|
300
|
+
):
|
|
301
|
+
equation_match = re.search(r"=\s*([-+]?\d+(?:\.\d+)?)$", single_line)
|
|
302
|
+
if equation_match:
|
|
303
|
+
return equation_match.group(1)
|
|
304
|
+
|
|
305
|
+
numeric_match = re.search(
|
|
306
|
+
r"\b(?:that(?:'s| is)|it(?:'s| is)|answer(?: is)?|result(?: is)?)\s+([-+]?\d+(?:\.\d+)?)\b",
|
|
307
|
+
single_line,
|
|
308
|
+
re.IGNORECASE,
|
|
309
|
+
)
|
|
310
|
+
if numeric_match:
|
|
311
|
+
return numeric_match.group(1)
|
|
312
|
+
|
|
313
|
+
sentences = [
|
|
314
|
+
part.strip() for part in re.split(r"(?<=[.!?])\s+", single_line) if part.strip()
|
|
315
|
+
]
|
|
316
|
+
if sentences:
|
|
317
|
+
first = self._strip_sentence_embellishment(sentences[0])
|
|
318
|
+
if len(first) <= QUIET_SENTENCE_MAX_LEN:
|
|
319
|
+
return first
|
|
320
|
+
|
|
321
|
+
if len(single_line) <= QUIET_FALLBACK_MAX_LEN:
|
|
322
|
+
return self._strip_sentence_embellishment(single_line)
|
|
323
|
+
return (
|
|
324
|
+
self._strip_sentence_embellishment(
|
|
325
|
+
single_line[:QUIET_TRUNCATED_MAX_LEN].rsplit(" ", 1)[0]
|
|
326
|
+
)
|
|
327
|
+
+ "..."
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def _strip_sentence_embellishment(self, text: str) -> str:
|
|
331
|
+
"""Remove lightweight trailing flourish from otherwise factual answers."""
|
|
332
|
+
text = re.sub(r"\s*[🇦-🇿✨🎉👍😊😄😃😀😉🙌]+$", "", text).strip()
|
|
333
|
+
|
|
334
|
+
inline_match = re.match(r"^(.*?),\s+(?:a|an)\s+(.+?)([.!?])$", text, flags=re.IGNORECASE)
|
|
335
|
+
if inline_match:
|
|
336
|
+
descriptor_words = re.findall(r"[A-Za-z']+", inline_match.group(2).lower())
|
|
337
|
+
if descriptor_words and any(
|
|
338
|
+
word in TRAILING_EMBELLISHMENT_WORDS for word in descriptor_words
|
|
339
|
+
):
|
|
340
|
+
return inline_match.group(1) + inline_match.group(3)
|
|
341
|
+
|
|
342
|
+
sentence_match = re.match(r"^(.*?\.)\s+(?:a|an)\s+(.+)$", text, flags=re.IGNORECASE)
|
|
343
|
+
if not sentence_match:
|
|
344
|
+
return text
|
|
345
|
+
|
|
346
|
+
descriptor_words = re.findall(r"[A-Za-z']+", sentence_match.group(2).lower())
|
|
347
|
+
if descriptor_words and any(
|
|
348
|
+
word in TRAILING_EMBELLISHMENT_WORDS for word in descriptor_words
|
|
349
|
+
):
|
|
350
|
+
return sentence_match.group(1)
|
|
351
|
+
return text
|
|
352
|
+
|
|
353
|
+
def _normalize_factual_ending(self, text: str) -> str:
|
|
354
|
+
"""Convert lightweight factual exclamation endings to periods."""
|
|
355
|
+
if re.search(
|
|
356
|
+
r"\b(?:capital|answer|result|sum|total|equals|is)\b", text, flags=re.IGNORECASE
|
|
357
|
+
):
|
|
358
|
+
return re.sub(r"!$", ".", text)
|
|
359
|
+
return text
|
|
360
|
+
|
|
361
|
+
def _normalize_whitespace(self, text: str) -> str:
|
|
362
|
+
"""Normalize excessive whitespace."""
|
|
363
|
+
return normalize_internal_whitespace(text)
|
|
364
|
+
|
|
365
|
+
# ==========================================================================
|
|
366
|
+
# Event Type Helpers
|
|
367
|
+
# ==========================================================================
|
|
368
|
+
|
|
369
|
+
def is_plan_event(self, event_type: str) -> bool:
|
|
370
|
+
"""Check if this is a plan-related event."""
|
|
371
|
+
return event_type.startswith("soothe.cognition.plan.")
|
|
372
|
+
|
|
373
|
+
def is_research_event(self, event_type: str) -> bool:
|
|
374
|
+
"""Check if this is a research subagent event."""
|
|
375
|
+
return event_type.startswith("soothe.subagent.research.")
|
|
376
|
+
|
|
377
|
+
def is_internal_event(self, event_type: str) -> bool:
|
|
378
|
+
"""Check if this is an internal (never-shown) event."""
|
|
379
|
+
return event_type in INTERNAL_EVENT_TYPES or "internal" in event_type
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# =============================================================================
|
|
383
|
+
# Factory Function
|
|
384
|
+
# =============================================================================
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def create_display_policy(
|
|
388
|
+
verbosity: VerbosityLevel = "normal",
|
|
389
|
+
) -> DisplayPolicy:
|
|
390
|
+
"""Create a display policy with the given verbosity level.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
verbosity: Verbosity level for filtering
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Configured DisplayPolicy instance
|
|
397
|
+
"""
|
|
398
|
+
return DisplayPolicy(verbosity=verbosity)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# =============================================================================
|
|
402
|
+
# Exports
|
|
403
|
+
# =============================================================================
|
|
404
|
+
|
|
405
|
+
__all__ = [
|
|
406
|
+
"INTERNAL_EVENT_TYPES",
|
|
407
|
+
"INTERNAL_JSON_KEYS",
|
|
408
|
+
"SKIP_EVENT_TYPES",
|
|
409
|
+
"DisplayPolicy",
|
|
410
|
+
"VerbosityLevel",
|
|
411
|
+
"VerbosityTier",
|
|
412
|
+
"create_display_policy",
|
|
413
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Shared essential event-type filtering for UX surfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Final
|
|
6
|
+
|
|
7
|
+
GOAL_START_EVENT_TYPES: Final[frozenset[str]] = frozenset(
|
|
8
|
+
{
|
|
9
|
+
"soothe.cognition.agent_loop.started",
|
|
10
|
+
"soothe.cognition.plan.creating",
|
|
11
|
+
}
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
STEP_START_EVENT_TYPES: Final[frozenset[str]] = frozenset(
|
|
15
|
+
{
|
|
16
|
+
"soothe.cognition.plan.step.started",
|
|
17
|
+
"soothe.cognition.agent_loop.step.started",
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
STEP_COMPLETE_EVENT_TYPES: Final[frozenset[str]] = frozenset(
|
|
22
|
+
{
|
|
23
|
+
"soothe.cognition.plan.step.completed",
|
|
24
|
+
"soothe.cognition.agent_loop.step.completed",
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
LOOP_REASON_EVENT_TYPE: Final[str] = "soothe.cognition.agent_loop.reasoned"
|
|
29
|
+
|
|
30
|
+
ESSENTIAL_PROGRESS_EVENT_TYPES: Final[frozenset[str]] = frozenset(
|
|
31
|
+
set(GOAL_START_EVENT_TYPES)
|
|
32
|
+
| set(STEP_START_EVENT_TYPES)
|
|
33
|
+
| set(STEP_COMPLETE_EVENT_TYPES)
|
|
34
|
+
| {LOOP_REASON_EVENT_TYPE}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def is_essential_progress_event_type(event_type: str) -> bool:
|
|
39
|
+
"""Return whether an event type is part of essential progress output."""
|
|
40
|
+
return event_type in ESSENTIAL_PROGRESS_EVENT_TYPES
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_goal_start_event_type(event_type: str) -> bool:
|
|
44
|
+
"""Return whether an event starts a goal header display."""
|
|
45
|
+
return event_type in GOAL_START_EVENT_TYPES
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_step_start_event_type(event_type: str) -> bool:
|
|
49
|
+
"""Return whether an event starts a step header display."""
|
|
50
|
+
return event_type in STEP_START_EVENT_TYPES
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_step_complete_event_type(event_type: str) -> bool:
|
|
54
|
+
"""Return whether an event marks step completion."""
|
|
55
|
+
return event_type in STEP_COMPLETE_EVENT_TYPES
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
"ESSENTIAL_PROGRESS_EVENT_TYPES",
|
|
60
|
+
"GOAL_START_EVENT_TYPES",
|
|
61
|
+
"LOOP_REASON_EVENT_TYPE",
|
|
62
|
+
"STEP_COMPLETE_EVENT_TYPES",
|
|
63
|
+
"STEP_START_EVENT_TYPES",
|
|
64
|
+
"is_essential_progress_event_type",
|
|
65
|
+
"is_goal_start_event_type",
|
|
66
|
+
"is_step_complete_event_type",
|
|
67
|
+
"is_step_start_event_type",
|
|
68
|
+
]
|