Skip to content

Commit 005bb35

Browse files
committed
feat(conversation): Group settings into General, Tools, and Reasoning; support reasoning in edit block
- Add General group (max tokens, temperature, top P, stream mode) - Add Tools group (web search) before General - Add Reasoning group (mode, adaptive, budget, effort, show blocks) - Hide groups when model/engine does not support them - Edit block: load/save reasoning and content via format_response_for_display and split_reasoning_and_content_from_display - Edit block: refresh response when "Show reasoning blocks" checkbox changes
1 parent bbd2c93 commit 005bb35

File tree

5 files changed

+172
-100
lines changed

5 files changed

+172
-100
lines changed

basilisk/conversation/content_utils.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
END_REASONING = "</think>"
99

1010
_THINK_BLOCK_PATTERN = re.compile(r"```think\s*\n(.*?)\n```\s*", re.DOTALL)
11+
_REASONING_BLOCK_PATTERN = re.compile(
12+
rf"{re.escape(START_BLOCK_REASONING)}\s*\n(.*?)\n{re.escape(END_REASONING)}\s*",
13+
re.DOTALL,
14+
)
1115

1216

1317
def split_reasoning_and_content(text: str) -> tuple[str | None, str]:
@@ -30,3 +34,36 @@ def split_reasoning_and_content(text: str) -> tuple[str | None, str]:
3034
reasoning = match.group(1).strip()
3135
content = (_THINK_BLOCK_PATTERN.sub("", text) or "").strip()
3236
return reasoning or None, content
37+
38+
39+
def format_response_for_display(
40+
reasoning: str | None, content: str, show_reasoning: bool
41+
) -> str:
42+
"""Format response for display (reasoning + content or content only)."""
43+
if show_reasoning and reasoning:
44+
return f"{START_BLOCK_REASONING}\n{reasoning}\n{END_REASONING}\n\n{content}"
45+
return content
46+
47+
48+
def split_reasoning_and_content_from_display(
49+
text: str,
50+
) -> tuple[str | None, str]:
51+
"""Split display text (<think>...</think> format) into reasoning and content.
52+
53+
Used when parsing user-edited response text (e.g. in edit block dialog).
54+
55+
Args:
56+
text: Display text that may contain <think>...</think> block.
57+
58+
Returns:
59+
Tuple of (reasoning, content). If no block, returns (None, text).
60+
"""
61+
if not text:
62+
return None, text or ""
63+
# Try <think> format first, then legacy ```think
64+
match = _REASONING_BLOCK_PATTERN.search(text)
65+
if not match:
66+
return split_reasoning_and_content(text)
67+
reasoning = match.group(1).strip()
68+
content = (_REASONING_BLOCK_PATTERN.sub("", text) or "").strip()
69+
return reasoning or None, content

basilisk/presenters/edit_block_presenter.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414
import basilisk.config as config
1515
from basilisk.completion_handler import CompletionHandler
16+
from basilisk.conversation.content_utils import (
17+
format_response_for_display,
18+
split_reasoning_and_content_from_display,
19+
)
1620
from basilisk.conversation.conversation_model import (
1721
Conversation,
1822
Message,
@@ -204,7 +208,11 @@ def save_block(self) -> bool:
204208

205209
# Update response if present
206210
if self.block.response:
207-
self.block.response.content = self.view.response_txt.GetValue()
211+
text = self.view.response_txt.GetValue()
212+
reasoning, content = split_reasoning_and_content_from_display(text)
213+
self.block.response = self.block.response.model_copy(
214+
update={"reasoning": reasoning, "content": content}
215+
)
208216

209217
self.block.updated_at = datetime.now()
210218
if self.service is not None:
@@ -266,7 +274,12 @@ def _on_non_stream_finish(
266274
system_message: Optional system message used for this completion.
267275
"""
268276
self.block.response = new_block.response
269-
self.view.response_txt.SetValue(new_block.response.content)
277+
reasoning = getattr(new_block.response, "reasoning", None)
278+
content = new_block.response.content
279+
display = format_response_for_display(
280+
reasoning, content, self.view.get_effective_show_reasoning_blocks()
281+
)
282+
self.view.response_txt.SetValue(display)
270283
audio_data = getattr(new_block.response, "audio_data", None)
271284
if audio_data:
272285
from basilisk.audio_utils import play_audio_from_base64

basilisk/views/base_conversation.py

Lines changed: 90 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -447,12 +447,77 @@ def create_stream_widget(self):
447447
)
448448
self.stream_mode.SetValue(True)
449449

450+
def create_general_group(self):
451+
"""Create grouped general settings (max tokens, temperature, top P, stream, web search).
452+
453+
Returns a StaticBoxSizer containing all general widgets. Add to layout
454+
after model list. Visibility controlled by update_parameter_controls_visibility.
455+
"""
456+
# Translators: Group label for general model settings
457+
box = wx.StaticBox(self, label=_("General"))
458+
sizer = wx.StaticBoxSizer(box, wx.VERTICAL)
459+
self.create_max_tokens_widget()
460+
sizer.Add(self.max_tokens_spin_label, 0, wx.ALL, 2)
461+
sizer.Add(self.max_tokens_spin_ctrl, 0, wx.ALL | wx.EXPAND, 2)
462+
self.create_temperature_widget()
463+
sizer.Add(self.temperature_spinner_label, 0, wx.ALL, 2)
464+
sizer.Add(self.temperature_spinner, 0, wx.ALL | wx.EXPAND, 2)
465+
self.create_top_p_widget()
466+
sizer.Add(self.top_p_spinner_label, 0, wx.ALL, 2)
467+
sizer.Add(self.top_p_spinner, 0, wx.ALL | wx.EXPAND, 2)
468+
self.create_stream_widget()
469+
sizer.Add(self.stream_mode, 0, wx.ALL, 2)
470+
471+
self.general_group_box = box
472+
self.general_group_sizer = sizer
473+
return sizer
474+
475+
def create_tools_group(self):
476+
"""Create grouped tool settings (web search, etc.).
477+
478+
Returns a StaticBoxSizer containing tool widgets. Add to layout after
479+
general group. Visibility controlled by update_parameter_controls_visibility.
480+
"""
481+
# Translators: Group label for model tools (web search, etc.)
482+
box = wx.StaticBox(self, label=_("Tools"))
483+
sizer = wx.StaticBoxSizer(box, wx.VERTICAL)
484+
self.create_web_search_widget()
485+
sizer.Add(self.web_search_mode, 0, wx.ALL, 2)
486+
487+
self.tools_group_box = box
488+
self.tools_group_sizer = sizer
489+
return sizer
490+
450491
def get_effective_show_reasoning_blocks(self) -> bool:
451492
"""Effective value: per-tab override if set, else default from config."""
452493
if self._show_reasoning_blocks_override is not None:
453494
return self._show_reasoning_blocks_override
454495
return config.conf().conversation.show_reasoning_blocks
455496

497+
def create_reasoning_group(self):
498+
"""Create grouped reasoning settings (mode, adaptive, budget, effort, display).
499+
500+
Returns a StaticBoxSizer containing all reasoning widgets. Add to
501+
layout after model list. Visibility of sub-widgets controlled by
502+
update_parameter_controls_visibility; show_reasoning_blocks is always visible.
503+
"""
504+
# Translators: Group label for reasoning/thinking settings
505+
box = wx.StaticBox(self, label=_("Reasoning"))
506+
sizer = wx.StaticBoxSizer(box, wx.VERTICAL)
507+
self.create_reasoning_widget()
508+
sizer.Add(self.reasoning_mode, 0, wx.ALL, 2)
509+
sizer.Add(self.reasoning_adaptive, 0, wx.ALL, 2)
510+
sizer.Add(self.reasoning_budget_label, 0, wx.ALL, 2)
511+
sizer.Add(self.reasoning_budget_spin, 0, wx.ALL | wx.EXPAND, 2)
512+
sizer.Add(self.reasoning_effort_label, 0, wx.ALL, 2)
513+
sizer.Add(self.reasoning_effort_choice, 0, wx.ALL | wx.EXPAND, 2)
514+
self.create_show_reasoning_blocks_widget()
515+
sizer.Add(self.show_reasoning_blocks, 0, wx.ALL, 2)
516+
517+
self.reasoning_group_box = box
518+
self.reasoning_group_sizer = sizer
519+
return sizer
520+
456521
def create_show_reasoning_blocks_widget(self):
457522
"""Create checkbox to show/hide reasoning (think) blocks in responses.
458523
@@ -678,6 +743,20 @@ def _apply_parameter_visibility_state(
678743
self, state: ParameterVisibilityState
679744
) -> None:
680745
"""Apply visibility state to widgets. Thin view layer."""
746+
self._apply_general_visibility(state)
747+
self._apply_audio_visibility(state)
748+
self._apply_tools_visibility(state)
749+
self._apply_reasoning_visibility(state)
750+
751+
def _apply_general_visibility(
752+
self, state: ParameterVisibilityState
753+
) -> None:
754+
"""Apply general settings group visibility.
755+
756+
Hides the entire group when no model is selected or advanced mode is off.
757+
"""
758+
if hasattr(self, "general_group_box"):
759+
self.general_group_box.Show(state.stream_visible)
681760
for ctrl in (self.temperature_spinner_label, self.temperature_spinner):
682761
ctrl.Enable(state.temperature_visible)
683762
ctrl.Show(state.temperature_visible)
@@ -690,10 +769,6 @@ def _apply_parameter_visibility_state(
690769
self.stream_mode.Enable(state.stream_visible)
691770
self.stream_mode.Show(state.stream_visible)
692771

693-
self._apply_audio_visibility(state)
694-
self._apply_web_search_visibility(state)
695-
self._apply_reasoning_visibility(state)
696-
697772
def _apply_audio_visibility(self, state: ParameterVisibilityState) -> None:
698773
"""Apply audio output visibility state."""
699774
if not hasattr(self, "audio_output_group_box"):
@@ -715,18 +790,24 @@ def _apply_audio_visibility(self, state: ParameterVisibilityState) -> None:
715790
idx = voices.index(old_val) if old_val in voices else 0
716791
self.audio_voice_choice.SetSelection(idx)
717792

718-
def _apply_web_search_visibility(
719-
self, state: ParameterVisibilityState
720-
) -> None:
721-
"""Apply web search visibility state."""
793+
def _apply_tools_visibility(self, state: ParameterVisibilityState) -> None:
794+
"""Apply tools group visibility (web search, etc.)."""
795+
if hasattr(self, "tools_group_box"):
796+
self.tools_group_box.Show(state.web_search_visible)
722797
if hasattr(self, "web_search_mode"):
723798
self.web_search_mode.Enable(state.web_search_visible)
724799
self.web_search_mode.Show(state.web_search_visible)
725800

726801
def _apply_reasoning_visibility(
727802
self, state: ParameterVisibilityState
728803
) -> None:
729-
"""Apply reasoning controls visibility state."""
804+
"""Apply reasoning controls visibility state.
805+
806+
Hides the entire reasoning group when no model is selected or the
807+
model does not support reasoning (e.g. web search models).
808+
"""
809+
if hasattr(self, "reasoning_group_box"):
810+
self.reasoning_group_box.Show(state.reasoning_mode_visible)
730811
if not hasattr(self, "reasoning_mode"):
731812
return
732813
self.reasoning_mode.Enable(state.reasoning_mode_visible)

basilisk/views/conversation_tab.py

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -279,28 +279,12 @@ def init_ui(self):
279279
sizer.Add(self.model_list, proportion=0, flag=wx.ALL | wx.EXPAND)
280280
self.create_audio_output_group()
281281
sizer.Add(self.audio_output_group_sizer, proportion=0, flag=wx.EXPAND)
282-
self.create_reasoning_widget()
283-
sizer.Add(self.reasoning_mode, proportion=0, flag=wx.EXPAND)
284-
sizer.Add(self.reasoning_adaptive, proportion=0, flag=wx.EXPAND)
285-
sizer.Add(self.reasoning_budget_label, proportion=0, flag=wx.EXPAND)
286-
sizer.Add(self.reasoning_budget_spin, proportion=0, flag=wx.EXPAND)
287-
sizer.Add(self.reasoning_effort_label, proportion=0, flag=wx.EXPAND)
288-
sizer.Add(self.reasoning_effort_choice, proportion=0, flag=wx.EXPAND)
289-
self.create_web_search_widget()
290-
sizer.Add(self.web_search_mode, proportion=0, flag=wx.EXPAND)
291-
self.create_max_tokens_widget()
292-
sizer.Add(self.max_tokens_spin_label, proportion=0, flag=wx.EXPAND)
293-
sizer.Add(self.max_tokens_spin_ctrl, proportion=0, flag=wx.EXPAND)
294-
self.create_temperature_widget()
295-
sizer.Add(self.temperature_spinner_label, proportion=0, flag=wx.EXPAND)
296-
sizer.Add(self.temperature_spinner, proportion=0, flag=wx.EXPAND)
297-
self.create_top_p_widget()
298-
sizer.Add(self.top_p_spinner_label, proportion=0, flag=wx.EXPAND)
299-
sizer.Add(self.top_p_spinner, proportion=0, flag=wx.EXPAND)
300-
self.create_stream_widget()
301-
sizer.Add(self.stream_mode, proportion=0, flag=wx.EXPAND)
302-
self.create_show_reasoning_blocks_widget()
303-
sizer.Add(self.show_reasoning_blocks, proportion=0, flag=wx.EXPAND)
282+
self.create_reasoning_group()
283+
sizer.Add(self.reasoning_group_sizer, proportion=0, flag=wx.EXPAND)
284+
self.create_tools_group()
285+
sizer.Add(self.tools_group_sizer, proportion=0, flag=wx.EXPAND)
286+
self.create_general_group()
287+
sizer.Add(self.general_group_sizer, proportion=0, flag=wx.EXPAND)
304288

305289
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
306290

basilisk/views/edit_block_dialog.py

Lines changed: 24 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import wx
1414

1515
import basilisk.config as config
16+
from basilisk.conversation.content_utils import format_response_for_display
1617
from basilisk.conversation.conversation_model import Conversation, SystemMessage
1718
from basilisk.presenters.edit_block_presenter import EditBlockPresenter
1819

@@ -173,83 +174,23 @@ def init_ui(self):
173174
border=10,
174175
)
175176

176-
params_sizer = wx.BoxSizer(wx.HORIZONTAL)
177-
178-
self.create_temperature_widget()
179-
temp_sizer = wx.BoxSizer(wx.VERTICAL)
180-
temp_sizer.Add(
181-
self.temperature_spinner_label, proportion=0, flag=wx.EXPAND
182-
)
183-
temp_sizer.Add(self.temperature_spinner, proportion=0, flag=wx.EXPAND)
184-
params_sizer.Add(
185-
temp_sizer, proportion=1, flag=wx.EXPAND | wx.RIGHT, border=10
186-
)
187-
188-
self.create_max_tokens_widget()
189-
tokens_sizer = wx.BoxSizer(wx.VERTICAL)
190-
tokens_sizer.Add(
191-
self.max_tokens_spin_label, proportion=0, flag=wx.EXPAND
192-
)
193-
tokens_sizer.Add(
194-
self.max_tokens_spin_ctrl, proportion=0, flag=wx.EXPAND
195-
)
196-
params_sizer.Add(
197-
tokens_sizer, proportion=1, flag=wx.EXPAND | wx.RIGHT, border=10
198-
)
199-
200-
self.create_top_p_widget()
201-
top_p_sizer = wx.BoxSizer(wx.VERTICAL)
202-
top_p_sizer.Add(self.top_p_spinner_label, proportion=0, flag=wx.EXPAND)
203-
top_p_sizer.Add(self.top_p_spinner, proportion=0, flag=wx.EXPAND)
204-
params_sizer.Add(top_p_sizer, proportion=1, flag=wx.EXPAND)
205-
206-
sizer.Add(
207-
params_sizer,
208-
proportion=0,
209-
flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT,
210-
border=10,
211-
)
212-
213-
self.create_stream_widget()
214-
sizer.Add(
215-
self.stream_mode,
216-
proportion=0,
217-
flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP,
218-
border=10,
219-
)
220-
self.create_reasoning_widget()
221-
sizer.Add(
222-
self.reasoning_mode,
223-
proportion=0,
224-
flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP,
225-
border=10,
226-
)
177+
self.create_tools_group()
227178
sizer.Add(
228-
self.reasoning_adaptive,
179+
self.tools_group_sizer,
229180
proportion=0,
230181
flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP,
231182
border=10,
232183
)
184+
self.create_general_group()
233185
sizer.Add(
234-
self.reasoning_budget_label,
186+
self.general_group_sizer,
235187
proportion=0,
236188
flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP,
237189
border=10,
238190
)
191+
self.create_reasoning_group()
239192
sizer.Add(
240-
self.reasoning_budget_spin,
241-
proportion=0,
242-
flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP,
243-
border=10,
244-
)
245-
sizer.Add(
246-
self.reasoning_effort_label,
247-
proportion=0,
248-
flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP,
249-
border=10,
250-
)
251-
sizer.Add(
252-
self.reasoning_effort_choice,
193+
self.reasoning_group_sizer,
253194
proportion=0,
254195
flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP,
255196
border=10,
@@ -362,7 +303,12 @@ def load_message_block_data(self):
362303
if self.system_message:
363304
self.system_prompt_txt.SetValue(self.system_message.content)
364305
if self.block.response:
365-
self.response_txt.SetValue(self.block.response.content)
306+
reasoning = getattr(self.block.response, "reasoning", None)
307+
content = self.block.response.content
308+
display = format_response_for_display(
309+
reasoning, content, self.get_effective_show_reasoning_blocks()
310+
)
311+
self.response_txt.SetValue(display)
366312
self.prompt_panel.prompt_text = self.block.request.content
367313

368314
if self.block.request.attachments:
@@ -380,6 +326,17 @@ def load_message_block_data(self):
380326
self._load_audio_params()
381327
self._load_reasoning_params()
382328

329+
def _on_show_reasoning_blocks_change(self, event: wx.Event | None):
330+
"""Refresh response display when show reasoning checkbox toggles."""
331+
super()._on_show_reasoning_blocks_change(event)
332+
if hasattr(self, "response_txt") and self.block and self.block.response:
333+
reasoning = getattr(self.block.response, "reasoning", None)
334+
content = self.block.response.content
335+
display = format_response_for_display(
336+
reasoning, content, self.get_effective_show_reasoning_blocks()
337+
)
338+
self.response_txt.SetValue(display)
339+
383340
def on_account_change(self, event):
384341
"""Handle account selection changes.
385342

0 commit comments

Comments
 (0)