from collections import OrderedDict from pathlib import Path from typing import Dict, List, Optional from phi.cli.console import print_heading, print_info from phi.cli.settings import phi_cli_settings from phi.api.schemas.user import UserSchema from phi.api.schemas.workspace import WorkspaceSchema, WorkspaceDelete from phi.utils.log import logger from phi.utils.json_io import read_json_file, write_json_file from phi.workspace.config import WorkspaceConfig class PhiCliConfig: """The PhiCliConfig class manages user data for the phi cli""" def __init__( self, user: Optional[UserSchema] = None, active_ws_dir: Optional[str] = None, ws_config_map: Optional[Dict[str, WorkspaceConfig]] = None, ) -> None: # Current user, populated after authenticating with the api # To add a user, use the user setter self._user: Optional[UserSchema] = user # Active ws dir - used as the default for `phi` commands # To add an active workspace, use the active_ws_dir setter self._active_ws_dir: Optional[str] = active_ws_dir # Mapping from ws_root_path to ws_config self.ws_config_map: Dict[str, WorkspaceConfig] = ws_config_map or OrderedDict() ###################################################### ## User functions ###################################################### @property def user(self) -> Optional[UserSchema]: return self._user @user.setter def user(self, user: Optional[UserSchema]) -> None: """Sets the user""" if user is not None: logger.debug(f"Setting user to: {user.email}") clear_user_cache = ( self._user is not None # previous user is not None and self._user.email != "anon" # previous user is not anon and (user.email != self._user.email or user.id_user != self._user.id_user) # new user is different ) self._user = user if clear_user_cache: self.clear_user_cache() self.save_config() def clear_user_cache(self) -> None: """Clears the user cache""" logger.debug("Clearing user cache") self.ws_config_map.clear() self._active_ws_dir = None phi_cli_settings.ai_conversations_path.unlink(missing_ok=True) logger.info("Workspaces cleared, please setup again using `phi ws setup`") ###################################################### ## Workspace functions ###################################################### @property def active_ws_dir(self) -> Optional[str]: return self._active_ws_dir def set_active_ws_dir(self, ws_root_path: Optional[Path]) -> None: if ws_root_path is not None: logger.debug(f"Setting active workspace to: {str(ws_root_path)}") self._active_ws_dir = str(ws_root_path) self.save_config() @property def available_ws(self) -> List[WorkspaceConfig]: return list(self.ws_config_map.values()) def _add_or_update_ws_config( self, ws_root_path: Path, ws_schema: Optional[WorkspaceSchema] = None ) -> Optional[WorkspaceConfig]: """The main function to create, update or refresh a WorkspaceConfig. This function does not call self.save_config(). Remember to save_config() after calling this function. """ # Validate ws_root_path if ws_root_path is None or not isinstance(ws_root_path, Path): raise ValueError(f"Invalid ws_root: {ws_root_path}") ws_root_str = str(ws_root_path) ###################################################### # Create new ws_config if one does not exist ###################################################### if ws_root_str not in self.ws_config_map: logger.debug(f"Creating workspace at: {ws_root_str}") new_workspace_config = WorkspaceConfig( ws_root_path=ws_root_path, ws_schema=ws_schema, ) self.ws_config_map[ws_root_str] = new_workspace_config logger.debug(f"Workspace created at: {ws_root_str}") # Return the new_workspace_config return new_workspace_config ###################################################### # Update ws_config ###################################################### logger.debug(f"Updating workspace at: {ws_root_str}") # By this point there should be a WorkspaceConfig object for this ws_name existing_ws_config: Optional[WorkspaceConfig] = self.ws_config_map.get(ws_root_str, None) if existing_ws_config is None: logger.error(f"Could not find workspace at: {ws_root_str}, please run `phi ws setup`") return None # Update the ws_schema if it's not None and different from the existing one if ws_schema is not None and existing_ws_config.ws_schema != ws_schema: existing_ws_config.ws_schema = ws_schema logger.debug(f"Workspace updated: {ws_root_str}") # Return the updated_ws_config return existing_ws_config ###################################################### # END ###################################################### def add_new_ws_to_config(self, ws_root_path: Path) -> Optional[WorkspaceConfig]: """Adds a newly created workspace to the PhiCliConfig""" ws_config = self._add_or_update_ws_config(ws_root_path=ws_root_path) self.save_config() return ws_config def update_ws_config( self, ws_root_path: Path, ws_schema: Optional[WorkspaceSchema] = None, set_as_active: bool = False, ) -> Optional[WorkspaceConfig]: """Updates WorkspaceConfig and returns True if successful""" ws_config = self._add_or_update_ws_config( ws_root_path=ws_root_path, ws_schema=ws_schema, ) if set_as_active: self._active_ws_dir = str(ws_root_path) self.save_config() return ws_config def delete_ws(self, ws_root_path: Path) -> None: """Handles Deleting a workspace from the PhiCliConfig and api""" ws_root_str = str(ws_root_path) print_heading(f"Deleting record for workspace at: {ws_root_str}") print_info("-*- Note: this does not delete any files on disk, please delete them manually") ws_config: Optional[WorkspaceConfig] = self.ws_config_map.pop(ws_root_str, None) if ws_config is None: logger.warning(f"No record of workspace at {ws_root_str}") return # Check if we're deleting the active workspace, if yes, unset the active ws if self._active_ws_dir is not None and self._active_ws_dir == ws_root_str: print_info(f"Removing {ws_root_str} as the active workspace") self._active_ws_dir = None if self.user is not None and ws_config.ws_schema is not None: print_info("Deleting workspace from the server") from phi.api.workspace import delete_workspace_for_user delete_workspace_for_user( user=self.user, workspace=WorkspaceDelete( id_workspace=ws_config.ws_schema.id_workspace, ws_name=ws_config.ws_schema.ws_name ), ) self.save_config() ###################################################### ## Get Workspace Data ###################################################### def get_ws_config_by_dir_name(self, ws_dir_name: str) -> Optional[WorkspaceConfig]: ws_root_str: Optional[str] = None for k, v in self.ws_config_map.items(): if v.ws_root_path.stem == ws_dir_name: ws_root_str = k break if ws_root_str is None or ws_root_str not in self.ws_config_map: return None return self.ws_config_map[ws_root_str] def get_ws_config_by_path(self, ws_root_path: Path) -> Optional[WorkspaceConfig]: return self.ws_config_map[str(ws_root_path)] if str(ws_root_path) in self.ws_config_map else None def get_active_ws_config(self) -> Optional[WorkspaceConfig]: if self.active_ws_dir is not None and self.active_ws_dir in self.ws_config_map: return self.ws_config_map[self.active_ws_dir] return None ###################################################### ## Save PhiCliConfig ###################################################### def save_config(self): config_data = { "user": self.user.model_dump() if self.user else None, "active_ws_dir": self.active_ws_dir, "ws_config_map": {k: v.to_dict() for k, v in self.ws_config_map.items()}, } write_json_file(file_path=phi_cli_settings.config_file_path, data=config_data) @classmethod def from_saved_config(cls): try: config_data = read_json_file(file_path=phi_cli_settings.config_file_path) if config_data is None or not isinstance(config_data, dict): logger.debug("No config found") return None user_dict = config_data.get("user") user_schema = UserSchema.model_validate(user_dict) if user_dict else None active_ws_dir = config_data.get("active_ws_dir") # Create a new config new_config = cls(user_schema, active_ws_dir) # Add all the workspaces for k, v in config_data.get("ws_config_map", {}).items(): _ws_config = WorkspaceConfig.from_dict(v) if _ws_config is not None: new_config.ws_config_map[k] = _ws_config return new_config except Exception as e: logger.warning(e) logger.warning("Please setup the workspace using `phi ws setup`") ###################################################### ## Print PhiCliConfig ###################################################### def print_to_cli(self, show_all: bool = False): if self.user: print_heading(f"User: {self.user.email}\n") if self.active_ws_dir: print_heading(f"Active workspace directory: {self.active_ws_dir}\n") else: print_info("No active workspace found.") print_info( "Please create a workspace using `phi ws create` " "or setup existing workspace using `phi ws setup`" ) if show_all and len(self.ws_config_map) > 0: print_heading("Available workspaces:\n") c = 1 for k, v in self.ws_config_map.items(): print_info(f" {c}. {k}") if v.ws_schema and v.ws_schema.ws_name: print_info(f" Name: {v.ws_schema.ws_name}") c += 1