Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding conversation profile #147

Merged
merged 77 commits into from
Oct 5, 2024
Merged

feat: adding conversation profile #147

merged 77 commits into from
Oct 5, 2024

Conversation

clementb49
Copy link
Collaborator

@clementb49 clementb49 commented Aug 10, 2024

Close #98

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced a structured GUI for managing conversation profiles, allowing users to create, edit, and manage profiles seamlessly.
    • Added functionality for selecting and applying conversation profiles directly within the conversation management interface.
    • Enhanced account management with new methods for handling user accounts and AI models.
    • Implemented a method to retrieve models by ID in the base engine.
    • New methods for managing images, including adding images from URLs and files.
    • Added a new dialog for creating and editing conversation profiles with comprehensive input fields.
  • Improvements

    • Streamlined conversation management and profile handling in the GUI, improving user interaction and accessibility.
    • Added type safety and clarity in handling account information and conversation profiles.
    • Enhanced the initialization and validation processes in the conversation profile dialog for better user experience.
    • Refined the user interface setup with dedicated methods for widget creation.
  • Bug Fixes

    • Refined logic for model selection and image management within conversations.

@clementb49 clementb49 force-pushed the conversationProfile branch from d2048f6 to bbef213 Compare August 10, 2024 21:07
@clementb49 clementb49 added this to the v0.1a5 milestone Aug 10, 2024
@clementb49 clementb49 force-pushed the conversationProfile branch 2 times, most recently from b63133b to 9dce652 Compare August 14, 2024 20:50
@clementb49 clementb49 modified the milestones: v0.1a5, v0.1a6 Aug 25, 2024
@clementb49 clementb49 force-pushed the conversationProfile branch 3 times, most recently from 998697c to 27fbfe1 Compare September 3, 2024 18:57
@clementb49 clementb49 modified the milestones: v0.1a6, v0.1a7 Sep 28, 2024
@clementb49 clementb49 modified the milestones: v0.1a7, v0.1a6 Oct 5, 2024
@clementb49 clementb49 marked this pull request as ready for review October 5, 2024 12:27
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE

📥 Commits

Files that changed from the base of the PR and between ba9bede and 15319bc.

📒 Files selected for processing (7)
  • basilisk/config/conversation_profile.py (1 hunks)
  • basilisk/gui/base_conversation.py (1 hunks)
  • basilisk/gui/conversation_profile_dialog.py (1 hunks)
  • basilisk/gui/conversation_tab.py (8 hunks)
  • basilisk/gui/main_frame.py (11 hunks)
  • basilisk/provider_ai_model.py (1 hunks)
  • basilisk/provider_engine/base_engine.py (2 hunks)
🔇 Additional comments (13)
basilisk/provider_ai_model.py (2)

36-44: Previous comment on adding a docstring to display_details method is still applicable


52-56: Previous comment on adding a docstring to effective_max_output_tokens property is still applicable

basilisk/gui/conversation_tab.py (11)

35-37: Imports are correctly added and utilized

The imported modules BaseConversation, show_html_view_window, and SearchDialog are necessary and appropriately used in the codebase.


62-62: Inheriting from BaseConversation enhances functionality

Adding BaseConversation to the inheritance list of ConversationTab promotes code reuse and modularity, aligning with object-oriented best practices.


71-74: Constructor updated to include optional profile parameter

Including profile: Optional[config.ConversationProfile] = None in the constructor allows for initializing the conversation with a specific profile, enhancing flexibility.


91-91: init_data method correctly initializes with profile

Passing the profile parameter to init_data ensures that conversation profiles are applied upon initialization, aligning with the PR objectives.


Line range hint 96-171: UI component setup refactored for better organization

Replacing direct manipulations of UI elements with dedicated methods like create_account_widget, create_system_prompt_widget, create_model_widget, etc., improves code readability and maintainability.


206-208: apply_profile method applied to initialize data

The init_data method now calls apply_profile(profile), ensuring that the conversation tab is initialized with the specified profile settings.


227-228: Superclass method on_account_change utilized properly

Using account = super().on_account_change(event) ensures that the base class logic is executed, promoting code reuse.


239-240: Accessing main frame using wx.GetTopLevelParent

Refactoring to use wx.GetTopLevelParent(self) enhances robustness by reducing dependency on UI hierarchy depth, addressing previous concerns.


762-762: Inserting previous prompt when prompt is empty

The condition if not self.prompt.GetValue(): ensures that the previous prompt is inserted only when the prompt field is empty, enhancing user experience by facilitating quick re-submission.


77-77: ⚠️ Potential issue

Constructor initializes base classes directly

Directly calling BaseConversation.__init__(self) and wx.Panel.__init__(self, parent) may not properly handle the method resolution order (MRO) in multiple inheritance. Using super().__init__(parent) ensures all base classes are initialized correctly.

Apply this diff to adjust the initialization:

-def __init__(
-    self,
-    parent: wx.Window,
-    title: str = _("Untitled conversation"),
-    profile: Optional[config.ConversationProfile] = None,
-):
-    wx.Panel.__init__(self, parent)
-    BaseConversation.__init__(self)
+def __init__(
+    self,
+    parent: wx.Window,
+    title: str = _("Untitled conversation"),
+    profile: Optional[config.ConversationProfile] = None,
+):
+    super().__init__(parent)

This change assumes that both wx.Panel and BaseConversation are compatible with super() and cooperate in the MRO, enhancing maintainability.

Likely invalid or redundant comment.


243-244: Redundant account check after handling no accounts

After addressing the scenario where no accounts are configured, the subsequent check if not account: may still be necessary if the account selection can result in None even when accounts exist. Verify if this additional check is required to handle cases where the user does not select an account.

Ensure the redundancy of the account check:

If account can legitimately be None, the check is necessary.

✅ Verification successful

The account check is necessary to handle cases where no account is selected.

  • The refresh_accounts method can result in account being None if GetSelection() returns wx.NOT_FOUND.
  • This ensures that the application gracefully handles scenarios where the user does not select an account.
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify if 'account' can be None after accounts are configured.

# Test: Search for all places where 'account' is set or modified
rg --type python --word-regexp 'account\s*='

# Test: Check if 'account_combo' can result in None selection
rg --type python 'self\.account_combo\.GetSelection\(\)'

Length of output: 167


Script:

#!/bin/bash
# Description: Verify if 'account' can be None after accounts are configured.

# Test: Search for all places where 'account' is set or modified
rg -g "*.py" --word-regexp 'account\s*='

# Test: Check if 'account_combo' can result in None selection
rg -g "*.py" 'self\.account_combo\.GetSelection\(\)'

Length of output: 1504


Script:

#!/bin/bash
# Description: Extract functions in conversation_tab.py that use GetSelection() and how they set 'account'.

# Extract function definitions that contain 'self.account_combo.GetSelection()'
ast-grep --lang python --pattern 'def $_(...) { $$$ self\.account_combo\.GetSelection\(\) $$$ }' basilisk/gui/conversation_tab.py

# Alternatively, extract lines around 'GetSelection()' to understand context
rg -C 5 'self\.account_combo\.GetSelection\(\)' basilisk/gui/conversation_tab.py

Length of output: 970

clementb49 and others added 4 commits October 5, 2024 14:39
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…e message

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE

📥 Commits

Files that changed from the base of the PR and between a7e25a7 and 566e91f.

📒 Files selected for processing (2)
  • basilisk/gui/conversation_profile_dialog.py (1 hunks)
  • basilisk/gui/main_frame.py (11 hunks)
🔇 Additional comments (9)
basilisk/gui/conversation_profile_dialog.py (4)

126-126: Ensure model_validate is called correctly.

The method model_validate is called on the class ConversationProfile. Ensure that model_validate is intended to be a class method. If it's an instance method, you should call it on the instance self.profile.

If model_validate is a class method, the code is correct. Otherwise, update the call as follows:

- ConversationProfile.model_validate(self.profile)
+ self.profile.model_validate()

286-286: Check the return value of wx.MessageBox.

Ensure that wx.MessageBox returns wx.ID_YES when the user confirms. While the code uses wx.ID_YES, it's good practice to verify that this is the correct constant for the comparison.


343-348: Ensure event is skipped after handling key down events.

In the on_list_profile_key_down method, you're calling event.Skip() after handling specific keys. Ensure that this behavior is intended. Skipping the event allows other handlers to process the event, which might lead to unexpected behavior if not managed carefully.


233-235: 🛠️ Refactor suggestion

Enhance readability by using list comprehension.

Replacing the map function with a list comprehension improves readability and is more Pythonic.

Apply this refactor to your code:

 def update_ui(self):
     self.list_profile_ctrl.DeleteAllItems()
-    for profile_name in map(lambda x: x.name, self.profiles):
+    for profile_name in [profile.name for profile in self.profiles]:
         self.list_profile_ctrl.Append([profile_name])

Likely invalid or redundant comment.

basilisk/gui/main_frame.py (5)

52-52: Initialize default conversation on startup

The call to self.on_new_default_conversation(None) ensures that a default conversation is created when the application starts, enhancing the user experience by providing an immediate conversation window.


74-76: Bind 'New conversation' menu item to the default handler

Updating the event binding to on_new_default_conversation correctly handles the creation of a new conversation using the default profile.


162-166: Correct menu item label formatting

The shortcut key in the menu item label is now properly formatted with a tab character \t, ensuring that "Ctrl+Shift+O" displays correctly for the "Manage conversation profiles" menu item.


419-437: Refactor profile retrieval into a helper method

The new method get_selected_profile_from_menu effectively encapsulates the logic for retrieving the selected profile from the menu, reducing code duplication and enhancing maintainability.


737-738: Properly destroy the context menu after use

Calling menu.Destroy() after self.PopupMenu(menu) ensures that resources are released properly, preventing potential memory leaks.

self.profile_name_txt.SetValue(profile.name)
if profile.account or profile.ai_model_info:
self.include_account_checkbox.SetValue(
self.profile.account is not None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use profile instead of self.profile within apply_profile method.

In the apply_profile method, you're using self.profile.account instead of profile.account. Since profile is passed as a parameter and may differ from self.profile, this could lead to incorrect behavior if self.profile and profile are not the same instance.

Apply this fix to use the correct reference:

- self.include_account_checkbox.SetValue(
-     self.profile.account is not None
- )
+ self.include_account_checkbox.SetValue(
+     profile.account is not None
+ )

Committable suggestion was skipped due to low confidence.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE

📥 Commits

Files that changed from the base of the PR and between 566e91f and d9acd7e.

📒 Files selected for processing (1)
  • basilisk/gui/conversation_profile_dialog.py (1 hunks)

Comment on lines +236 to +245
dialog = EditConversationProfileDialog(
self,
# translators: Dialog title to add a new conversation profile
title=_("Add Conversation Profile"),
)
if dialog.ShowModal() == wx.ID_OK:
self.profiles.add(dialog.profile)
self.update_ui()
self.profiles.save()
self.on_list_item_selected(None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Properly destroy dialogs to prevent resource leaks

In the on_add method, the EditConversationProfileDialog instance dialog is not destroyed after it is no longer needed. Failing to call dialog.Destroy() can lead to resource leaks in wxPython applications.

Apply this diff to destroy the dialog after use:

 def on_add(self, event):
     dialog = EditConversationProfileDialog(
         self,
         title=_("Add Conversation Profile"),
     )
     if dialog.ShowModal() == wx.ID_OK:
         self.profiles.add(dialog.profile)
         self.update_ui()
         self.profiles.save()
         self.on_list_item_selected(None)
         self.menu_update = True
+    dialog.Destroy()

Ensure that any dialogs created are properly destroyed after use.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
dialog = EditConversationProfileDialog(
self,
# translators: Dialog title to add a new conversation profile
title=_("Add Conversation Profile"),
)
if dialog.ShowModal() == wx.ID_OK:
self.profiles.add(dialog.profile)
self.update_ui()
self.profiles.save()
self.on_list_item_selected(None)
dialog = EditConversationProfileDialog(
self,
# translators: Dialog title to add a new conversation profile
title=_("Add Conversation Profile"),
)
if dialog.ShowModal() == wx.ID_OK:
self.profiles.add(dialog.profile)
self.update_ui()
self.profiles.save()
self.on_list_item_selected(None)
self.menu_update = True
dialog.Destroy()

Comment on lines +252 to +267
dialog = EditConversationProfileDialog(
self,
"Edit Conversation Profile",
profile=self.profiles[profile_index],
)
if dialog.ShowModal() == wx.ID_OK:
self.profiles[profile_index] = dialog.profile
self.profiles.save()
self.update_ui()
self.list_profile_ctrl.SetItemState(
profile_index,
wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED,
wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED,
)
self.on_list_item_selected(None)
self.menu_update = True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Properly destroy dialogs to prevent resource leaks

In the on_edit method, the EditConversationProfileDialog instance dialog is not destroyed after it is no longer needed. To prevent resource leaks, it's important to call dialog.Destroy() after you're done with the dialog.

Apply this diff to destroy the dialog after use:

 def on_edit(self, event):
     profile_index = self.current_profile_index
     if profile_index is None:
         return
     dialog = EditConversationProfileDialog(
         self,
         "Edit Conversation Profile",
         profile=self.profiles[profile_index],
     )
     if dialog.ShowModal() == wx.ID_OK:
         self.profiles[profile_index] = dialog.profile
         self.profiles.save()
         self.update_ui()
         self.list_profile_ctrl.SetItemState(
             profile_index,
             wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED,
             wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED,
         )
         self.on_list_item_selected(None)
         self.menu_update = True
+    dialog.Destroy()

Ensure that all dialogs are properly destroyed to manage resources effectively.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
dialog = EditConversationProfileDialog(
self,
"Edit Conversation Profile",
profile=self.profiles[profile_index],
)
if dialog.ShowModal() == wx.ID_OK:
self.profiles[profile_index] = dialog.profile
self.profiles.save()
self.update_ui()
self.list_profile_ctrl.SetItemState(
profile_index,
wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED,
wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED,
)
self.on_list_item_selected(None)
self.menu_update = True
dialog = EditConversationProfileDialog(
self,
"Edit Conversation Profile",
profile=self.profiles[profile_index],
)
if dialog.ShowModal() == wx.ID_OK:
self.profiles[profile_index] = dialog.profile
self.profiles.save()
self.update_ui()
self.list_profile_ctrl.SetItemState(
profile_index,
wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED,
wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED,
)
self.on_list_item_selected(None)
self.menu_update = True
dialog.Destroy()

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE

📥 Commits

Files that changed from the base of the PR and between d9acd7e and 320a6b5.

📒 Files selected for processing (2)
  • basilisk/gui/conversation_profile_dialog.py (1 hunks)
  • basilisk/gui/main_frame.py (11 hunks)
🔇 Additional comments (4)
basilisk/gui/main_frame.py (4)

52-52: Initialization with default conversation

The addition of self.on_new_default_conversation(None) ensures the application starts with a default conversation, enhancing user experience.


74-76: Binding 'New conversation' menu item to default conversation handler

The 'New conversation' menu item is correctly bound to self.on_new_default_conversation, allowing users to start a new conversation using the default profile.


162-172: Adding 'Manage conversation profiles' to the tools menu

The addition of the 'Manage conversation profiles' menu item provides users with easy access to manage their conversation profiles.


210-210: Binding context menu on notebook tabs

Binding the event EVT_CONTEXT_MENU to on_notebook_context_menu enables context-specific actions on conversation tabs, improving user interaction.

Comment on lines +324 to +327
if profile.system_prompt:
# translators: Summary of a conversation profile
summary += _("System prompt: %s\n") % profile.system_prompt
return summary
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider truncating long system prompts in profile summaries

When displaying the system prompt in the profile summary, a very long prompt can make the summary difficult to read. Consider truncating the system prompt or limiting it to a certain number of characters or lines to improve readability.

Apply this diff to implement the suggestion:

 if profile.system_prompt:
     # translators: Summary of a conversation profile
-    summary += _("System prompt: %s\n") % profile.system_prompt
+    # Truncate system prompt if it's longer than 100 characters
+    truncated_prompt = (profile.system_prompt[:100] + '...') if len(profile.system_prompt) > 100 else profile.system_prompt
+    summary += _("System prompt: %s\n") % truncated_prompt
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if profile.system_prompt:
# translators: Summary of a conversation profile
summary += _("System prompt: %s\n") % profile.system_prompt
return summary
if profile.system_prompt:
# translators: Summary of a conversation profile
# Truncate system prompt if it's longer than 100 characters
truncated_prompt = (profile.system_prompt[:100] + '...') if len(profile.system_prompt) > 100 else profile.system_prompt
summary += _("System prompt: %s\n") % truncated_prompt
return summary

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE

📥 Commits

Files that changed from the base of the PR and between 320a6b5 and aff8c11.

📒 Files selected for processing (3)
  • basilisk/gui/base_conversation.py (1 hunks)
  • basilisk/gui/conversation_profile_dialog.py (1 hunks)
  • basilisk/gui/conversation_tab.py (8 hunks)
🔇 Additional comments (16)
basilisk/gui/conversation_profile_dialog.py (8)

13-25: Initialization of EditConversationProfileDialog is correct

The __init__ method properly initializes the dialog and applies the profile correctly.


27-73: UI components are appropriately set up in init_ui

The user interface elements are correctly initialized and laid out, ensuring a good user experience.


74-85: Profile application logic in apply_profile is sound

The method accurately applies the profile settings to the UI components.


128-128: Consider validating the profile instance method

Instead of calling the class method ConversationProfile.model_validate(self.profile), it might be more appropriate to call an instance method for validation to ensure all instance-specific data is properly validated.

Check if model_validate should be an instance method. If so, apply this change:

-ConversationProfile.model_validate(self.profile)
+self.profile.validate()

Ensure that the validate method exists and performs the necessary validations on the profile instance.


347-352: Ensure event is skipped after key handling

In the on_list_profile_key_down method, it's good practice to ensure the event is skipped after handling specific keys to allow other controls to process the event if necessary.


234-238: 🧹 Nitpick (assertive)

Enhance readability by using list comprehension

Replacing the map function with a list comprehension in the update_ui method improves readability and is more Pythonic.

Apply this diff to update the code:

 def update_ui(self):
     self.list_profile_ctrl.DeleteAllItems()
-    for profile_name in map(lambda x: x.name, self.profiles):
+    for profile_name in [profile.name for profile in self.profiles]:
         self.list_profile_ctrl.Append([profile_name])

Likely invalid or redundant comment.


123-126: 🛠️ Refactor suggestion

Align top_p default value handling with temperature

In the on_ok method, for consistency and to accommodate models with different default top_p values, compare top_p against model.default_top_p instead of 1.0, similar to how temperature is handled.

Apply this diff to improve consistency:

 top_p = self.top_p_spinner.GetValue()
-if model and top_p != 1.0:
+if model and top_p != model.default_top_p:
     self.profile.top_p = top_p
 else:
     self.profile.top_p = None

Ensure that model.default_top_p is defined in your model class.

Likely invalid or redundant comment.


328-331: 🛠️ Refactor suggestion

Truncate long system prompts in profile summaries

Displaying very long system prompts can make the profile summary hard to read. Consider truncating the system prompt to improve readability.

Apply this diff to implement truncation:

 if profile.system_prompt:
     # translators: Summary of a conversation profile
-    summary += _("System prompt: %s\n") % profile.system_prompt
+    # Truncate system prompt if it's longer than 100 characters
+    truncated_prompt = (profile.system_prompt[:100] + '...') if len(profile.system_prompt) > 100 else profile.system_prompt
+    summary += _("System prompt: %s\n") % truncated_prompt

Likely invalid or redundant comment.

basilisk/gui/conversation_tab.py (8)

35-37: Imports added for new features

The necessary modules BaseConversation, show_html_view_window, and SearchDialog are appropriately imported to support the new functionalities.


62-62: Class now inherits from BaseConversation

The ConversationTab class correctly extends BaseConversation alongside wx.Panel, which aligns with the integration of conversation profiles.


77-77: Consider using super().__init__() for proper multiple inheritance

Directly calling BaseConversation.__init__(self) may not handle the method resolution order (MRO) correctly in multiple inheritance scenarios. Using super().__init__() ensures all base classes are initialized properly.


91-91: Initialize data with the provided profile

The call to self.init_data(profile) ensures that the conversation tab is initialized with the appropriate conversation profile.


171-171: Verify initialization of instance variables in create_stream_widget()

Since create_stream_widget() is not assigned to a variable, ensure that it properly initializes any instance variables (e.g., self.stream_mode) used later in the code.


206-208: Ensure apply_profile handles None profiles safely

When profile is None, calling self.apply_profile(profile, True) should not cause errors. Verify that the apply_profile method can handle None without issues.


243-244: Redundant account check after handling no accounts

As previously noted, the check if not account: may be redundant after addressing the scenario where no accounts are configured. Review if this additional check is necessary.


762-762: Insert previous prompt when current prompt is empty

The logic correctly checks if the current prompt is empty and inserts the previous prompt, enhancing user convenience.

Comment on lines +264 to +269
float_spin_accessible = FloatSpinTextCtrlAccessible(
win=self.temperature_spinner._textctrl,
name=label.GetLabel().replace("&", ""),
)
self.temperature_spinner._textctrl.SetAccessible(float_spin_accessible)
return label
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid accessing private attributes _textctrl

In create_temperature_widget and create_top_p_widget, you're accessing the private attribute _textctrl of FloatSpin. Accessing private attributes (prefixed with an underscore) is discouraged as it can lead to fragile code if the underlying library changes its implementation. This practice may result in compatibility issues with future versions of the library.

Consider the following options:

  • Option 1: Check if the library provides a public method or property to achieve the desired functionality.
  • Option 2: Subclass FloatSpin and add the necessary accessibility features without accessing private attributes.

Also applies to: 286-291

account = self.current_account
if not account:
return None
return self.accounts_engines[account.id]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle potential KeyError when accessing accounts_engines

In the current_engine property, accessing self.accounts_engines[account.id] may raise a KeyError if the engine for the current account hasn't been initialized yet. This situation can occur if on_account_change hasn't been called for the current account. Consider using the get method to safely retrieve the engine or handle the exception appropriately.

Apply this diff to prevent the potential error:

 def current_engine(self) -> Optional[BaseEngine]:
     account = self.current_account
     if not account:
         return None
-    return self.accounts_engines[account.id]
+    return self.accounts_engines.get(account.id)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return self.accounts_engines[account.id]
return self.accounts_engines.get(account.id)

Comment on lines +306 to +309
if fall_back_default_account and not profile:
log.debug("no profile, select default account")
self.select_default_account()
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add a check for None profile before accessing its attributes

In the apply_profile method, if profile is None and fall_back_default_account is False, the code continues to access profile.system_prompt and other attributes, which will raise an AttributeError. To prevent this, add a check to ensure profile is not None before proceeding.

Apply this diff to handle the case when profile is None:

 def apply_profile(
     self,
     profile: Optional[config.ConversationProfile],
     fall_back_default_account: bool = False,
 ):
+    if not profile:
+        return
     if fall_back_default_account and not profile:
         log.debug("no profile, select default account")
         self.select_default_account()
         return

Committable suggestion was skipped due to low confidence.

Comment on lines +174 to +175
self.max_tokens_spin_ctrl.SetMax(model.effective_max_output_tokens)
self.max_tokens_spin_ctrl.SetValue(0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Set a meaningful default value for max_tokens_spin_ctrl

In the on_model_change method, self.max_tokens_spin_ctrl is set to 0 when a new model is selected. Setting it to zero might not be meaningful to users and could lead to confusion. Consider setting it to the model's default maximum tokens or leaving it unchanged if appropriate.

Apply this diff to set a more appropriate default value:

 def on_model_change(self, event):
     model = self.current_model
     if not model:
         return
     self.temperature_spinner.SetMax(model.max_temperature)
     self.temperature_spinner.SetValue(model.default_temperature)
     self.max_tokens_spin_ctrl.SetMax(model.effective_max_output_tokens)
-    self.max_tokens_spin_ctrl.SetValue(0)
+    self.max_tokens_spin_ctrl.SetValue(model.default_max_tokens or 0)

Ensure that model.default_max_tokens is defined in the ProviderAIModel class and provides a reasonable default value.

Committable suggestion was skipped due to low confidence.

Comment on lines +298 to +303
profile = self.current_profile
if profile is None:
return
self.profiles.set_default_profile(profile)
self.profiles.save()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Update UI after setting a default profile

In the on_default method, after setting a profile as default, it's helpful to update the UI to reflect this change immediately.

Apply this diff to refresh the UI:

 def on_default(self, event):
     profile = self.current_profile
     if profile is None:
         return
     self.profiles.set_default_profile(profile)
     self.profiles.save()
+    self.update_ui()
+    self.on_list_item_selected(None)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
profile = self.current_profile
if profile is None:
return
self.profiles.set_default_profile(profile)
self.profiles.save()
profile = self.current_profile
if profile is None:
return
self.profiles.set_default_profile(profile)
self.profiles.save()
self.update_ui()
self.on_list_item_selected(None)

Comment on lines +86 to +130
def on_ok(self, event):
if not self.profile:
self.profile = ConversationProfile.model_construct()
self.profile.name = self.profile_name_txt.GetValue()
if not self.profile.name:
wx.MessageBox(
# translators: Message box title for a conversation profile name
_("Profile name cannot be empty"),
# translators: Message box title for a conversation profile name
_("Profile name error"),
style=wx.OK | wx.ICON_ERROR,
)
return
self.profile.system_prompt = self.system_prompt_txt.GetValue()
account = self.current_account
model = self.current_model
if self.include_account_checkbox.GetValue():
self.profile.set_account(account)
else:
self.profile.set_account(None)
if account and model:
self.profile.set_model_info(
self.current_account.provider.id, self.current_model.id
)
else:
self.profile.ai_model_info = None
max_tokens = self.max_tokens_spin_ctrl.GetValue()
if model and max_tokens > 0:
self.profile.max_tokens = max_tokens
else:
self.profile.max_tokens = None
temperature = self.temperature_spinner.GetValue()
if model and temperature != model.default_temperature:
self.profile.temperature = temperature
else:
self.profile.temperature = None
top_p = self.top_p_spinner.GetValue()
if model and top_p != 1.0:
self.profile.top_p = top_p
else:
self.profile.top_p = None
self.profile.stream_mode = self.stream_mode.GetValue()
ConversationProfile.model_validate(self.profile)
self.EndModal(wx.ID_OK)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential issue with using model_construct for profile creation

When creating a new profile in the on_ok method, consider using the standard constructor ConversationProfile() instead of ConversationProfile.model_construct(). Using the standard constructor ensures data validation occurs, which can prevent potential issues with invalid data.

Apply this diff to use the standard constructor:

 def on_ok(self, event):
     if not self.profile:
-        self.profile = ConversationProfile.model_construct()
+        self.profile = ConversationProfile()
     self.profile.name = self.profile_name_txt.GetValue()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def on_ok(self, event):
if not self.profile:
self.profile = ConversationProfile.model_construct()
self.profile.name = self.profile_name_txt.GetValue()
if not self.profile.name:
wx.MessageBox(
# translators: Message box title for a conversation profile name
_("Profile name cannot be empty"),
# translators: Message box title for a conversation profile name
_("Profile name error"),
style=wx.OK | wx.ICON_ERROR,
)
return
self.profile.system_prompt = self.system_prompt_txt.GetValue()
account = self.current_account
model = self.current_model
if self.include_account_checkbox.GetValue():
self.profile.set_account(account)
else:
self.profile.set_account(None)
if account and model:
self.profile.set_model_info(
self.current_account.provider.id, self.current_model.id
)
else:
self.profile.ai_model_info = None
max_tokens = self.max_tokens_spin_ctrl.GetValue()
if model and max_tokens > 0:
self.profile.max_tokens = max_tokens
else:
self.profile.max_tokens = None
temperature = self.temperature_spinner.GetValue()
if model and temperature != model.default_temperature:
self.profile.temperature = temperature
else:
self.profile.temperature = None
top_p = self.top_p_spinner.GetValue()
if model and top_p != 1.0:
self.profile.top_p = top_p
else:
self.profile.top_p = None
self.profile.stream_mode = self.stream_mode.GetValue()
ConversationProfile.model_validate(self.profile)
self.EndModal(wx.ID_OK)
def on_ok(self, event):
if not self.profile:
self.profile = ConversationProfile()
self.profile.name = self.profile_name_txt.GetValue()
if not self.profile.name:
wx.MessageBox(
# translators: Message box title for a conversation profile name
_("Profile name cannot be empty"),
# translators: Message box title for a conversation profile name
_("Profile name error"),
style=wx.OK | wx.ICON_ERROR,
)
return
self.profile.system_prompt = self.system_prompt_txt.GetValue()
account = self.current_account
model = self.current_model
if self.include_account_checkbox.GetValue():
self.profile.set_account(account)
else:
self.profile.set_account(None)
if account and model:
self.profile.set_model_info(
self.current_account.provider.id, self.current_model.id
)
else:
self.profile.ai_model_info = None
max_tokens = self.max_tokens_spin_ctrl.GetValue()
if model and max_tokens > 0:
self.profile.max_tokens = max_tokens
else:
self.profile.max_tokens = None
temperature = self.temperature_spinner.GetValue()
if model and temperature != model.default_temperature:
self.profile.temperature = temperature
else:
self.profile.temperature = None
top_p = self.top_p_spinner.GetValue()
if model and top_p != 1.0:
self.profile.top_p = top_p
else:
self.profile.top_p = None
self.profile.stream_mode = self.stream_mode.GetValue()
ConversationProfile.model_validate(self.profile)
self.EndModal(wx.ID_OK)

Comment on lines +159 to +171
label = self.create_model_widget()
sizer.Add(label, proportion=0, flag=wx.EXPAND)
self.model_list = wx.ListCtrl(self, style=wx.LC_REPORT)
# Translators: This label appears in the main window's list of models
self.model_list.InsertColumn(0, _("Name"))
# Translators: This label appears in the main window's list of models to indicate whether the model supports images
self.model_list.InsertColumn(1, _("Vision"))
# Translators: This label appears in the main window's list of models
self.model_list.InsertColumn(2, _("Context window"))
# Translators: This label appears in the main window's list of models
self.model_list.InsertColumn(3, _("Max tokens"))
self.model_list.SetColumnWidth(0, 200)
self.model_list.SetColumnWidth(1, 100)
self.model_list.SetColumnWidth(2, 100)
self.model_list.SetColumnWidth(3, 100)
sizer.Add(self.model_list, proportion=0, flag=wx.ALL | wx.EXPAND)
self.model_list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_model_change)
self.model_list.Bind(wx.EVT_KEY_DOWN, self.on_model_key_down)
self.model_list.Bind(wx.EVT_CONTEXT_MENU, self.on_model_context_menu)

self.max_tokens_label = wx.StaticText(
self,
# Translators: This is a label for max tokens in the main window
label=_("Max to&kens:"),
)
self.max_tokens_label = self.create_max_tokens_widget()
sizer.Add(self.max_tokens_label, proportion=0, flag=wx.EXPAND)
self.max_tokens_spin_ctrl = wx.SpinCtrl(
self, value='0', min=0, max=2000000
)
sizer.Add(self.max_tokens_spin_ctrl, proportion=0, flag=wx.EXPAND)

self.temperature_label = wx.StaticText(
self,
# Translators: This is a label for temperature in the main window
label=_("&Temperature:"),
)
self.temperature_label = self.create_temperature_widget()
sizer.Add(self.temperature_label, proportion=0, flag=wx.EXPAND)
self.temperature_spinner = FloatSpin(
self,
min_val=0.0,
max_val=2.0,
increment=0.01,
value=0.5,
digits=2,
name="temperature",
)
float_spin_accessible = FloatSpinTextCtrlAccessible(
win=self.temperature_spinner._textctrl,
name=self.temperature_label.GetLabel().replace("&", ""),
)
self.temperature_spinner._textctrl.SetAccessible(float_spin_accessible)
sizer.Add(self.temperature_spinner, proportion=0, flag=wx.EXPAND)

self.top_p_label = wx.StaticText(
self,
# Translators: This is a label for top P in the main window
label=_("Probabilit&y Mass (top P):"),
)
self.top_p_label = self.create_top_p_widget()
sizer.Add(self.top_p_label, proportion=0, flag=wx.EXPAND)
self.top_p_spinner = FloatSpin(
self,
min_val=0.0,
max_val=1.0,
increment=0.01,
value=1.0,
digits=2,
name="Top P",
)
float_spin_accessible = FloatSpinTextCtrlAccessible(
win=self.top_p_spinner._textctrl,
name=self.top_p_label.GetLabel().replace("&", ""),
)
self.top_p_spinner._textctrl.SetAccessible(float_spin_accessible)
sizer.Add(self.top_p_spinner, proportion=0, flag=wx.EXPAND)

self.stream_mode = wx.CheckBox(
self,
# Translators: This is a label for stream mode in the main window
label=_("&Stream mode"),
)
self.stream_mode.SetValue(True)
self.create_stream_widget()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Standardize widget creation and assignment

Some widget creation methods return values assigned to variables (e.g., label = self.create_model_widget()), while others like self.create_stream_widget() do not. For consistency, consider assigning all created widgets to instance variables or consistently returning them.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE

📥 Commits

Files that changed from the base of the PR and between aff8c11 and d22ef64.

📒 Files selected for processing (2)
  • basilisk/gui/account_dialog.py (1 hunks)
  • basilisk/gui/conversation_profile_dialog.py (1 hunks)
🔇 Additional comments (2)
basilisk/gui/account_dialog.py (2)

621-621: Minor UX improvement, indirectly supporting PR objectives.

While this change doesn't directly address the main objectives of enhancing control of system prompts and managing conversation profiles, it does contribute to an overall improvement in user experience. By adding the escape key functionality to close the dialog, the change makes the application more intuitive and user-friendly.

This improvement, although small, aligns with the broader goal of enhancing user interaction with the application. As you continue to implement features for better control of system prompts and conversation profiles, maintaining this focus on user experience will be crucial.

Consider applying similar UX improvements throughout the application, especially in the new features being developed for system prompt control and conversation profile management. This will ensure a consistent and user-friendly experience across all aspects of the application.


621-621: Improved dialog behavior with escape key functionality.

The addition of self.SetEscapeId(btn.GetId()) enhances the user experience by allowing the dialog to be closed using the escape key. This is a standard behavior in most GUI applications and aligns well with user expectations.

To ensure consistency across the application, let's check if other dialogs implement similar functionality:

This will help us identify if this improvement should be applied to other dialogs as well.

✅ Verification successful

Escape key functionality successfully added to AccountDialog.

The addition of self.SetEscapeId(btn.GetId()) in basilisk/gui/account_dialog.py ensures that the dialog can be closed using the escape key, enhancing the user experience. No other dialog classes in the codebase currently implement this functionality, which confirms that this change is correctly applied.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other dialogs that might benefit from escape key functionality
rg --type python 'class.*Dialog.*wx\.Dialog' -A 10 | rg 'SetEscapeId'

Length of output: 100


Script:

#!/bin/bash
# Corrected search for other dialogs that implement escape key functionality
rg --type py 'class\s+\w+Dialog\(wx\.Dialog\)' -A 10 | rg 'SetEscapeId'

Length of output: 71

Comment on lines +43 to +47
self.include_account_checkbox = wx.CheckBox(
self,
# translators: Label for including an account in a conversation profile
label=_("&Include account in profile"),
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add include_account_checkbox to the sizer to display it in the dialog

The include_account_checkbox is created but not added to the sizer, so it won't appear in the dialog. You need to add it to the sizer to ensure it is displayed.

Apply this diff to add the checkbox to the sizer:

 self.include_account_checkbox = wx.CheckBox(
     self,
     # translators: Label for including an account in a conversation profile
     label=_("&Include account in profile"),
 )
+self.sizer.Add(self.include_account_checkbox, 0, wx.ALL, 5)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.include_account_checkbox = wx.CheckBox(
self,
# translators: Label for including an account in a conversation profile
label=_("&Include account in profile"),
)
self.include_account_checkbox = wx.CheckBox(
self,
# translators: Label for including an account in a conversation profile
label=_("&Include account in profile"),
)
self.sizer.Add(self.include_account_checkbox, 0, wx.ALL, 5)

…ortcuts

- Added a key binding for Ctrl+P to select a conversation profile in ConversationTab.
- Removed `on_key_down` method and integrated key event handling in `on_char_hook`.
- Updated key combination for preferences in MainFrame menu from Ctrl+Shift+P to Ctrl+,.
- Removed unused ID for preferences in MainFrame.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE

📥 Commits

Files that changed from the base of the PR and between d22ef64 and fdee8ff.

📒 Files selected for processing (2)
  • basilisk/gui/conversation_tab.py (8 hunks)
  • basilisk/gui/main_frame.py (11 hunks)
🔇 Additional comments (6)
basilisk/gui/main_frame.py (3)

435-440: LGTM: New conversation creation with selected profile

The implementation correctly handles profile selection and new conversation creation. It properly uses the get_selected_profile_from_menu method and checks for a valid profile before creating the conversation.


475-481: LGTM: Updated new_conversation method

The new_conversation method has been correctly updated to accept an optional profile parameter and pass it to the ConversationTab constructor. This change aligns well with the new conversation profile functionality.


739-744: ⚠️ Potential issue

Check for open conversation when applying profile

The on_apply_conversation_profile method doesn't verify if a conversation is currently open before attempting to apply the profile. This could lead to errors if the user somehow triggers this action when no conversation is active.

Add a check for an open conversation:

 def on_apply_conversation_profile(self, event):
+	if not self.current_tab:
+		wx.MessageBox(
+			_("No conversation selected"),
+			_("Error"),
+			wx.OK | wx.ICON_ERROR
+		)
+		return
 	profile = self.get_selected_profile_from_menu(event)
 	if not profile:
 		return
 	log.info(f"Applying profile: {profile.name} to conversation")
 	self.current_tab.apply_profile(profile)

Likely invalid or redundant comment.

basilisk/gui/conversation_tab.py (3)

782-782: Potential issue with GetValue() method

In the condition if not self.prompt.GetValue():, ensure that GetValue() returns the expected data type (string) and that an empty prompt is correctly handled.

Run the following script to check for other usages of GetValue() on wx.TextCtrl to ensure consistent handling:

#!/bin/bash
# Description: Find all usages of GetValue() on wx.TextCtrl instances

# Test: Search for GetValue() method calls on wx.TextCtrl objects
rg --type python "wx\.TextCtrl.*\.GetValue\(\)"

This will help verify that GetValue() is used consistently across the codebase.


77-77: 🛠️ Refactor suggestion

Use super().__init__() for proper multiple inheritance

Directly calling BaseConversation.__init__(self) may not correctly handle the method resolution order (MRO) in multiple inheritance. Using super().__init__() ensures that all base classes are initialized properly.

Apply this diff to adjust the initialization:

 def __init__(
     self,
     parent: wx.Window,
     title: str = _("Untitled conversation"),
     profile: Optional[config.ConversationProfile] = None,
 ):
-    wx.Panel.__init__(self, parent)
-    BaseConversation.__init__(self)
+    super().__init__(parent)

This change assumes that both wx.Panel and BaseConversation are compatible with super() and cooperate correctly in the MRO.

Likely invalid or redundant comment.


206-206: Ensure event binding for on_char_hook is appropriate

Binding the wx.EVT_CHAR_HOOK event to self.on_char_hook may have unintended side effects if not handled properly, as it captures key events before they are processed by child controls.

Run the following script to check if other components are binding to wx.EVT_CHAR_HOOK and to ensure that this binding does not conflict:

This will help verify if there are potential conflicts with other event handlers.

✅ Verification successful

Action Required: Re-run the event binding search with corrected shell script

Please execute the following script to search for other bindings to wx.EVT_CHAR_HOOK without specifying the file type:


Verified: Event binding for on_char_hook is appropriate and no conflicting bindings found.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for other bindings to wx.EVT_CHAR_HOOK in the codebase

# Test: Find all occurrences of wx.EVT_CHAR_HOOK event bindings
rg --type python "Bind\(wx\.EVT_CHAR_HOOK" -A 2

Length of output: 157


Script:

#!/bin/bash
# Description: Search for other bindings to wx.EVT_CHAR_HOOK in the codebase

# Test: Find all occurrences of wx.EVT_CHAR_HOOK event bindings
rg "Bind\(wx\.EVT_CHAR_HOOK" -A 2

Length of output: 253

@AAClause AAClause merged commit e96d305 into master Oct 5, 2024
7 checks passed
@AAClause AAClause deleted the conversationProfile branch October 5, 2024 21:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Better control of system prompts
2 participants