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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
604f14e
feat(conversation_profile): Adding a pydantic model to store the conv…
clementb49 Aug 5, 2024
4f58926
feat(conversation_profile): Adding a bsic dialog to manage conversati…
clementb49 Aug 5, 2024
295d1b0
feat(conversation_profile): upadate conversation profile dialog
clementb49 Aug 7, 2024
74c610f
feat(conversation_profile): Implements default profile selection for …
clementb49 Sep 1, 2024
d35e122
feat(conversation_profile): adding menu item to select a conversation…
clementb49 Aug 10, 2024
3ff7470
feat(conversation_profile): Implement conversation creation from profile
clementb49 Sep 1, 2024
ff9438c
feat(conversation_profile): fix sub menu change for new conversation …
clementb49 Aug 11, 2024
f4f844a
feat(conversation_profile): adding context menu on conversation tab t…
clementb49 Aug 11, 2024
8e3369b
feat(conversation_profile): adding keyboard shortcut for conversation…
clementb49 Aug 11, 2024
8ec8b6f
feat(conversation_profile): use a empty system prompt for the default…
clementb49 Aug 15, 2024
7abb288
refactor(conversation_tab): create base_conversation to reuse widget …
clementb49 Sep 15, 2024
6252b20
refactor(conversation): Use the système prompt text box defined in ba…
clementb49 Sep 28, 2024
5cb0231
feat(conversation_profile): store an account for the profile
clementb49 Sep 28, 2024
04ad9ac
feat(conversation_profile): allow account selection in profile edit d…
clementb49 Sep 28, 2024
d8c6002
Merge branch 'master' into conversationProfile
clementb49 Sep 28, 2024
e2808ef
feat(conversation_profile): Allow model selection in conversation pro…
clementb49 Sep 28, 2024
012a813
refactor(provider): replace pydantic model by a dataclass which is be…
clementb49 Sep 28, 2024
82e3b49
feat(provider_engine): find a specific model by the ID
clementb49 Sep 29, 2024
0da432b
feat(conversation_profile): Store selected AI model in a conversation…
clementb49 Sep 29, 2024
fad3b38
feat(conversation_profile): save and restore AI model selection
clementb49 Sep 29, 2024
dbf4bd7
feat(conversation_profile): Add model parameters controls in profile …
clementb49 Sep 29, 2024
c02d133
feat(conversation_profle): Store model parameters in profile
clementb49 Sep 29, 2024
fd39816
feat(conversation_profile): Store the stream mode in conversation pro…
clementb49 Sep 29, 2024
ab069d4
refactor(conversation_profile): Move apply profile method in base con…
clementb49 Sep 29, 2024
8c76dc5
refactor(conversation_profile): Remove mandatory default profile
clementb49 Sep 29, 2024
be766b7
feat(conversation_profile): start conversation with default profile
clementb49 Sep 29, 2024
db7afcd
fix(conversation_profile): save default profile
clementb49 Sep 29, 2024
86b61c9
Merge remote-tracking branch 'origin/master' into conversationProfile
clementb49 Sep 29, 2024
12198a4
refactor(conversation_profile): Move show model detail in base conver…
clementb49 Sep 29, 2024
7406103
fix(conversation_profile): fix profile deletion
clementb49 Sep 29, 2024
df6ee7d
Merge branch 'master' into conversationProfile
clementb49 Sep 29, 2024
d6bd63f
fix(conversation_profile): fix pattern in model
clementb49 Sep 29, 2024
e14342f
fix: remove useless label btn
clementb49 Sep 29, 2024
6663d1d
fix(conversation_profile): fix store account
clementb49 Sep 29, 2024
ca67260
fix: pattern validation for ai model info
clementb49 Sep 29, 2024
e4ac95e
fix(conversation_profile): fix default selection of account
clementb49 Sep 29, 2024
d00e57a
feat(conversation_profile): add menu shortcut for manage conversation
clementb49 Sep 29, 2024
54bf77a
fix(conversation_profile): do not set model parameters if model is no…
clementb49 Sep 29, 2024
6635722
fix(conversation_profile): select default account if profile has no a…
clementb49 Sep 29, 2024
b8d1958
fix(conversation_profile): fix edit dialog
clementb49 Sep 29, 2024
0d235e7
refactor(conversation_profile): move name conversation in a menu
clementb49 Sep 29, 2024
05e0ad1
fix(conversation_profile): Show error when a profile name already exist
clementb49 Sep 29, 2024
1e483bc
refactor(conversation_profile): use ID instead of name to distinguish…
clementb49 Sep 29, 2024
69dff7d
feat(conversation_profile): Add profile summary text in conversation …
clementb49 Sep 30, 2024
83d527e
fix(conversation_profile): Adding translation for stream mode
clementb49 Oct 3, 2024
c0643ae
fix(conversation_profile): raise exception when profile is not found …
clementb49 Oct 3, 2024
3b7bf78
fix(conversation_profile): fix log message when srofile has errors
clementb49 Oct 3, 2024
efdc1bc
refactor(conversation_profile): Simplify conversation profile equality
clementb49 Oct 3, 2024
9402e5b
fix: sort alphabeticaly config export
clementb49 Oct 3, 2024
fcd46c6
fix(account_config): fix the get_account_from_info method
clementb49 Oct 3, 2024
e401d21
fix: fix translation of AI model detail
clementb49 Oct 3, 2024
6686cdf
fix(conversation_profile): Fix apply profile on the current tab
clementb49 Oct 3, 2024
3335f41
fix(conversation_profile): fix __setitem__ method
clementb49 Oct 3, 2024
2c4c46f
fix(conversation_profile): various fix in base_conversation
clementb49 Oct 3, 2024
1dbe35c
fix(conversation_profile): various fix in conversation profile dialogs
clementb49 Oct 3, 2024
ba9bede
refactor: get the main frame in a more robust maner
clementb49 Oct 3, 2024
6dedb6b
refactor: improve performance on get model method
clementb49 Oct 3, 2024
a959b22
fix: type annotation
clementb49 Oct 3, 2024
e72c953
fix(conversation_profile): adding error message if profile was not fo…
clementb49 Oct 3, 2024
4122050
fix(conversation_profile): fix index checks
clementb49 Oct 3, 2024
86a8648
fix: use a list instead of a generator to filter model list
clementb49 Oct 5, 2024
7250173
fix(conversation_tab): correct prompt behavior on Control+Up key press
AAClause Oct 5, 2024
ba47162
fix: change max io tokens property
clementb49 Oct 5, 2024
8b4a2e3
refactor: correct shortcut format
clementb49 Oct 5, 2024
82ea2e1
fix: don't select a model by default
clementb49 Oct 5, 2024
8622caa
fix(conversation_profile): fix new conversation profile menu
clementb49 Oct 5, 2024
15319bc
Merge remote-tracking branch 'origin/master' into conversationProfile
clementb49 Oct 5, 2024
a7e25a7
fix: Fix model detail label
clementb49 Oct 5, 2024
23cd1ee
fix(conversation_profile): use the correct wx ID to the confirm remov…
clementb49 Oct 5, 2024
8d1bba1
refactor(conversation_profile): remove code duplication to get a prof…
clementb49 Oct 5, 2024
566e91f
fix: correctly destroy notebook context menu
clementb49 Oct 5, 2024
d9acd7e
fix(conversation_profile): correct __init__ parent method call
clementb49 Oct 5, 2024
320a6b5
fix(conversation_profile): Fix wx object deletion
clementb49 Oct 5, 2024
aff8c11
feat(conversation_profile): implement fall back default account
clementb49 Oct 5, 2024
d22ef64
fix(conversation_profile): various fixes
clementb49 Oct 5, 2024
fdee8ff
feat(gui): Add key binding for profile application and update menu sh…
AAClause Oct 5, 2024
25d47ac
fix(conversation_profile): add missing checkbox on the conversation p…
clementb49 Oct 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions basilisk/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .account_config import Account, AccountOrganization
from .account_config import Account, AccountManager, AccountOrganization
from .account_config import get_account_config as accounts
from .config_enums import (
AccountSource,
Expand All @@ -8,19 +8,26 @@
ReleaseChannelEnum,
get_account_source_labels,
)
from .conversation_profile import ConversationProfile
from .conversation_profile import (
get_conversation_profile_config as conversation_profiles,
)
from .main_config import BasiliskConfig
from .main_config import get_basilisk_config as conf

__all__ = [
"accounts",
"Account",
"AccountManager",
"AccountOrganization",
"get_account_source_labels",
"AccountSource",
"accounts",
"AutomaticUpdateModeEnum",
"BasiliskConfig",
"conf",
"ConversationProfile",
"conversation_profiles",
"get_account_source_labels",
"KeyStorageMethodEnum",
"AccountSource",
"LogLevelEnum",
"ReleaseChannelEnum",
"AutomaticUpdateModeEnum",
"BasiliskConfig",
]
63 changes: 40 additions & 23 deletions basilisk/config/account_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
from functools import cache, cached_property
from os import getenv
from typing import Any, Iterable, Optional, Union
from typing import Annotated, Any, Iterable, Optional, Union
from uuid import UUID, uuid4

import keyring
Expand Down Expand Up @@ -82,6 +82,10 @@ def delete_keyring_password(self):
keyring.delete_password(APP_NAME, str(self.id))


AccountInfoStr = Annotated[str, Field(pattern="^env:[a-zA-Z]+")]
AccountInfo = Union[UUID4, AccountInfoStr]


class Account(BaseModel):
"""
Manage API key and organization key
Expand Down Expand Up @@ -196,8 +200,8 @@ def active_organization(self) -> Optional[AccountOrganization]:

def reset_active_organization(self):
try:
del self.active_organization
except AttributeError:
del self.__dict__["active_organization"]
except KeyError:
pass

@property
Expand All @@ -219,9 +223,24 @@ def delete_keyring_password(self):
if self.api_key_storage_method == KeyStorageMethodEnum.system:
keyring.delete_password(APP_NAME, str(self.id))

def get_account_info(self) -> AccountInfo:
if self.source == AccountSource.ENV_VAR:
return f"env:{self.provider.name}"
return self.id

def __eq__(self, value: Account) -> bool:
return self.id == value.id

@property
def display_name(self) -> str:
organization = (
self.active_organization.name
if self.active_organization
else _("Personal")
)
provider_name = self.provider.name
return f"{self.name} ({organization}) - {provider_name}"


config_file_name = "accounts.yml"

Expand All @@ -236,24 +255,28 @@ class AccountManager(BasiliskBaseSettings):

accounts: list[OnErrorOmit[Account]] = Field(default=list())

default_account_info: Optional[Union[UUID, str]] = Field(
default_account_info: Optional[AccountInfo] = Field(
default=None, union_mode="left_to_right"
)

@cached_property
def default_account(self) -> Account:
index = 0
if isinstance(self.default_account_info, UUID):
account = self.get_account_from_info(self.default_account_info)
if not account:
log.warning(
f"Default account not found for id {self.default_account_info} using the first account"
)
account = self[0]
return account

def get_account_from_info(self, value: AccountInfo) -> Optional[Account]:
if isinstance(value, UUID):
try:
return self[self.default_account_info]
return self[value]
except KeyError:
log.warning(
f"Default account not found for id {self.default_account_info} using the first account"
)
elif isinstance(
self.default_account_info, str
) and self.default_account_info.startswith("env:"):
provider_name = self.default_account_info[4:]
return None
elif isinstance(value, str):
provider_name = value[4:]
index = next(
locate(
self.accounts,
Expand All @@ -263,21 +286,15 @@ def default_account(self) -> Account:
None,
)
if index is None:
log.warning(
f"Default account not found in env variable for provider {provider_name} using the first account"
)
index = 0
return self.accounts[index]
return self.accounts[0]
return self.accounts[index]

def set_default_account(self, value: Optional[Account]):
if not value or not isinstance(value, Account):
self.default_account_info = None
del self.__dict__["default_account"]
return
if value.source == AccountSource.ENV_VAR:
self.default_account_info = f"env:{value.provider.name}"
else:
self.default_account_info = value.id
self.default_account_info = value.get_account_info()
self.__dict__["default_account"] = value

@field_validator("accounts", mode="after")
Expand Down
227 changes: 227 additions & 0 deletions basilisk/config/conversation_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
from __future__ import annotations

import logging
from functools import cache, cached_property
from typing import Any, Iterable, Optional, Union
from uuid import UUID, uuid4

from pydantic import (
UUID4,
BaseModel,
ConfigDict,
Field,
OnErrorOmit,
field_validator,
model_validator,
)

from basilisk.provider import Provider, get_provider

from .account_config import Account, AccountInfo, get_account_config
from .config_helper import (
BasiliskBaseSettings,
get_settings_config_dict,
save_config_file,
)

log = logging.getLogger(__name__)


class ConversationProfile(BaseModel):
model_config = ConfigDict(revalidate_instances="always")
id: UUID4 = Field(default_factory=uuid4)
name: str
system_prompt: str = Field(default="")
account_info: Optional[AccountInfo] = Field(default=None)
ai_model_info: Optional[str] = Field(
default=None, pattern=r"^[a-zA-Z]+/.+$"
)
max_tokens: Optional[int] = Field(default=None)
temperature: Optional[float] = Field(default=None)
top_p: Optional[float] = Field(default=None)
stream_mode: bool = Field(default=True)

def __init__(self, **data: Any):
try:
super().__init__(**data)
except Exception as e:
log.error(
f"Error in conversation profile {e}; the profile will not be accessible",
exc_info=e,
)
raise e

@field_validator("ai_model_info")
@classmethod
def provider_must_exist(cls, value: str):
if value is None:
return None
provider_id, model_id = value.split("/", 1)
get_provider(id=provider_id)
return value

@classmethod
def get_default(cls) -> ConversationProfile:
return cls(name="default", system_prompt="")

@cached_property
def account(self) -> Optional[Account]:
if self.account_info is None:
return None
return get_account_config().get_account_from_info(self.account_info)

def set_account(self, account: Optional[Account]):
if account is None:
self.account_info = None
if "account" in self.__dict__:
del self.__dict__["account"]
else:
self.account_info = account.get_account_info()
self.__dict__["account"] = account

@property
def ai_model_id(self) -> Optional[str]:
if self.ai_model_info is None:
return None
return self.ai_model_info.split("/", 1)[1]

@property
def ai_provider(self) -> Optional[Provider]:
if self.account is None and self.ai_model_info is None:
return None
if self.account:
return self.account.provider
if self.ai_model_info:
provider_id, model_id = self.ai_model_info.split("/", 1)
return get_provider(id=provider_id)

def set_model_info(self, provider_id: str, model_id: str):
self.ai_model_info = f"{provider_id}/{model_id}"

def __eq__(self, value: Optional[ConversationProfile]) -> bool:
if value is None:
return False
return self.id == value.id

@model_validator(mode="after")
def check_same_provider(self) -> ConversationProfile:
if self.account is not None and self.ai_model_info is not None:
provider_id, model_id = self.ai_model_info.split("/", 1)
if provider_id != self.account.provider.id:
raise ValueError(
"Model provider must be the same as account provider"
)
return self

@model_validator(mode="after")
def check_model_params(self) -> ConversationProfile:
if self.ai_model_info is None:
if self.max_tokens is not None:
raise ValueError("Max tokens must be None without model")
if self.temperature is not None:
raise ValueError("Temperature must be None without model")
if self.top_p is not None:
raise ValueError("Top P must be None without model")
return self


config_file_name = "profiles.yml"


class ConversationProfileManager(BasiliskBaseSettings):
model_config = get_settings_config_dict(config_file_name)

profiles: list[OnErrorOmit[ConversationProfile]] = Field(
default_factory=list
)

default_profile_id: Optional[UUID4] = Field(default=None)

def get_profile(self, **kwargs: dict) -> Optional[ConversationProfile]:
return next(
filter(
lambda p: all(getattr(p, k) == v for k, v in kwargs.items()),
self.profiles,
),
None,
)

@cached_property
def default_profile(self) -> Optional[ConversationProfile]:
if self.default_profile_id is None:
return None
return self.get_profile(id=self.default_profile_id)

def set_default_profile(self, value: Optional[ConversationProfile]):
if value is None:
self.default_profile_id = None
else:
self.default_profile_id = value.id
if "default_profile" in self.__dict__:
del self.__dict__["default_profile"]

@model_validator(mode="after")
def check_default_profile(self) -> ConversationProfileManager:
if self.default_profile_id is None:
return self
if self.default_profile is None:
raise ValueError("Default profile not found")
return self

def __iter__(self) -> Iterable[ConversationProfile]:
return iter(self.profiles)

def add(self, profile: ConversationProfile):
self.profiles.append(profile)

def remove(self, profile: ConversationProfile):
if profile == self.default_profile:
self.default_profile_id = None
if "default_profile" in self.__dict__:
del self.__dict__["default_profile"]
self.profiles.remove(profile)

def __len__(self) -> int:
return len(self.profiles)

def __getitem__(self, index: Union[int, UUID]) -> ConversationProfile:
if isinstance(index, int):
return self.profiles[index]
elif isinstance(index, UUID):
profile = self.get_profile(id=index)
if profile is None:
raise KeyError(f"No profile found with id {index}")
return profile
else:
raise TypeError(f"Invalid index type: {type(index)}")

def __delitem__(self, index: int):
profile = self.profiles[index]
self.remove(profile)

Comment on lines +198 to +201
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

Support deletion by UUID in __delitem__ method for consistency.

The __delitem__ method currently only accepts an integer index. Aligning it with __getitem__ and __setitem__ by accepting a UUID would enhance consistency.

Modify the method to handle both int and UUID indices:

-def __delitem__(self, index: int):
+def __delitem__(self, index: Union[int, UUID]):
    if isinstance(index, int):
        profile = self.profiles[index]
+   elif isinstance(index, UUID):
+       profile = self.get_profile(id=index)
+       if profile is None:
+           raise KeyError(f"No profile found with id {index}")
    else:
        raise TypeError(f"Invalid index type: {type(index)}")
    self.remove(profile)

Committable suggestion was skipped due to low confidence.

def __setitem__(self, index: Union[int, UUID], value: ConversationProfile):
if isinstance(index, int):
self.profiles[index] = value
elif isinstance(index, UUID):
profile = self.get_profile(id=index)
if not profile:
self.add(value)
else:
idx = self.profiles.index(profile)
self.profiles[idx] = value
else:
raise TypeError(f"Invalid index type: {type(index)}")

def save(self):
save_config_file(
self.model_dump(
mode="json", exclude_defaults=True, exclude_none=True
),
config_file_name,
)


@cache
def get_conversation_profile_config() -> ConversationProfileManager:
log.debug("Loading conversation profile config")
return ConversationProfileManager()
2 changes: 1 addition & 1 deletion basilisk/gui/account_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ def init_ui(self):
)
btn = wx.Button(panel, wx.ID_CLOSE)
btn.Bind(wx.EVT_BUTTON, self.on_close)

self.SetEscapeId(btn.GetId())
sizer.Add(btn, 0, wx.ALL, 5)

def init_data(self):
Expand Down
Loading