Spaces:
Build error
Build error
| import os | |
| import sys | |
| import logging | |
| from pathlib import Path | |
| from dotenv import find_dotenv, load_dotenv | |
| # Locked profile: set to a profile name (e.g., "astronomer") to lock the app | |
| # to that profile and disable all profile switching. Leave as None for normal behavior. | |
| LOCKED_PROFILE: str | None = None | |
| DEFAULT_PROFILES_DIRECTORY = Path(__file__).parent / "profiles" | |
| logger = logging.getLogger(__name__) | |
| def _env_flag(name: str, default: bool = False) -> bool: | |
| """Parse a boolean environment flag. | |
| Accepted truthy values: 1, true, yes, on | |
| Accepted falsy values: 0, false, no, off | |
| """ | |
| raw = os.getenv(name) | |
| if raw is None: | |
| return default | |
| value = raw.strip().lower() | |
| if value in {"1", "true", "yes", "on"}: | |
| return True | |
| if value in {"0", "false", "no", "off"}: | |
| return False | |
| logger.warning("Invalid boolean value for %s=%r, using default=%s", name, raw, default) | |
| return default | |
| def _collect_profile_names(profiles_root: Path) -> set[str]: | |
| """Return profile folder names from a profiles root directory.""" | |
| if not profiles_root.exists() or not profiles_root.is_dir(): | |
| return set() | |
| return {p.name for p in profiles_root.iterdir() if p.is_dir()} | |
| def _collect_tool_module_names(tools_root: Path) -> set[str]: | |
| """Return tool module names from a tools directory.""" | |
| if not tools_root.exists() or not tools_root.is_dir(): | |
| return set() | |
| ignored = {"__init__", "core_tools"} | |
| return { | |
| p.stem | |
| for p in tools_root.glob("*.py") | |
| if p.is_file() and p.stem not in ignored | |
| } | |
| def _raise_on_name_collisions( | |
| *, | |
| label: str, | |
| external_root: Path, | |
| internal_root: Path, | |
| external_names: set[str], | |
| internal_names: set[str], | |
| ) -> None: | |
| """Raise with a clear message when external/internal names collide.""" | |
| collisions = sorted(external_names & internal_names) | |
| if not collisions: | |
| return | |
| raise RuntimeError( | |
| f"Config.__init__(): Ambiguous {label} names found in both external and built-in libraries: {collisions}. " | |
| f"External {label} root: {external_root}. Built-in {label} root: {internal_root}. " | |
| f"Please rename the conflicting external {label}(s) to continue." | |
| ) | |
| # Validate LOCKED_PROFILE at startup | |
| if LOCKED_PROFILE is not None: | |
| _profiles_dir = DEFAULT_PROFILES_DIRECTORY | |
| _profile_path = _profiles_dir / LOCKED_PROFILE | |
| _instructions_file = _profile_path / "instructions.txt" | |
| if not _profile_path.is_dir(): | |
| print(f"Error: LOCKED_PROFILE '{LOCKED_PROFILE}' does not exist in {_profiles_dir}", file=sys.stderr) | |
| sys.exit(1) | |
| if not _instructions_file.is_file(): | |
| print(f"Error: LOCKED_PROFILE '{LOCKED_PROFILE}' has no instructions.txt", file=sys.stderr) | |
| sys.exit(1) | |
| _skip_dotenv = _env_flag("REACHY_MINI_SKIP_DOTENV", default=False) | |
| if _skip_dotenv: | |
| logger.info("Skipping .env loading because REACHY_MINI_SKIP_DOTENV is set") | |
| else: | |
| # Locate .env file (search upward from current working directory) | |
| dotenv_path = find_dotenv(usecwd=True) | |
| if dotenv_path: | |
| # Load .env and override environment variables | |
| load_dotenv(dotenv_path=dotenv_path, override=True) | |
| logger.info(f"Configuration loaded from {dotenv_path}") | |
| else: | |
| logger.warning("No .env file found, using environment variables") | |
| class Config: | |
| """Configuration class for the conversation app.""" | |
| # Ollama | |
| OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") | |
| MODEL_NAME = os.getenv("MODEL_NAME", "llama3.2") | |
| # STT (faster-whisper model size: tiny, base, small, medium, large-v3) | |
| STT_MODEL = os.getenv("STT_MODEL", "base") | |
| # TTS (edge-tts voice name) | |
| TTS_VOICE = os.getenv("TTS_VOICE", "en-US-AriaNeural") | |
| # Vision (optional, used with --local-vision CLI flag) | |
| HF_HOME = os.getenv("HF_HOME", "./cache") | |
| LOCAL_VISION_MODEL = os.getenv("LOCAL_VISION_MODEL", "HuggingFaceTB/SmolVLM2-2.2B-Instruct") | |
| HF_TOKEN = os.getenv("HF_TOKEN") # Optional, falls back to hf auth login if not set | |
| logger.debug(f"Model: {MODEL_NAME}, Ollama: {OLLAMA_BASE_URL}, STT: {STT_MODEL}, TTS: {TTS_VOICE}") | |
| _profiles_directory_env = os.getenv("REACHY_MINI_EXTERNAL_PROFILES_DIRECTORY") | |
| PROFILES_DIRECTORY = ( | |
| Path(_profiles_directory_env) if _profiles_directory_env else Path(__file__).parent / "profiles" | |
| ) | |
| _tools_directory_env = os.getenv("REACHY_MINI_EXTERNAL_TOOLS_DIRECTORY") | |
| TOOLS_DIRECTORY = Path(_tools_directory_env) if _tools_directory_env else None | |
| AUTOLOAD_EXTERNAL_TOOLS = _env_flag("AUTOLOAD_EXTERNAL_TOOLS", default=False) | |
| REACHY_MINI_CUSTOM_PROFILE = LOCKED_PROFILE or os.getenv("REACHY_MINI_CUSTOM_PROFILE") | |
| logger.debug(f"Custom Profile: {REACHY_MINI_CUSTOM_PROFILE}") | |
| def __init__(self) -> None: | |
| """Initialize the configuration.""" | |
| if self.REACHY_MINI_CUSTOM_PROFILE and self.PROFILES_DIRECTORY != DEFAULT_PROFILES_DIRECTORY: | |
| selected_profile_path = self.PROFILES_DIRECTORY / self.REACHY_MINI_CUSTOM_PROFILE | |
| if not selected_profile_path.is_dir(): | |
| available_profiles = sorted(_collect_profile_names(self.PROFILES_DIRECTORY)) | |
| raise RuntimeError( | |
| "Config.__init__(): Selected profile " | |
| f"'{self.REACHY_MINI_CUSTOM_PROFILE}' was not found in external profiles root " | |
| f"{self.PROFILES_DIRECTORY}. " | |
| f"Available external profiles: {available_profiles}. " | |
| "Either set 'REACHY_MINI_CUSTOM_PROFILE' to one of the available external profiles " | |
| "or unset 'REACHY_MINI_EXTERNAL_PROFILES_DIRECTORY' to use built-in profiles." | |
| ) | |
| if self.PROFILES_DIRECTORY != DEFAULT_PROFILES_DIRECTORY: | |
| external_profiles = _collect_profile_names(self.PROFILES_DIRECTORY) | |
| internal_profiles = _collect_profile_names(DEFAULT_PROFILES_DIRECTORY) | |
| _raise_on_name_collisions( | |
| label="profile", | |
| external_root=self.PROFILES_DIRECTORY, | |
| internal_root=DEFAULT_PROFILES_DIRECTORY, | |
| external_names=external_profiles, | |
| internal_names=internal_profiles, | |
| ) | |
| if self.TOOLS_DIRECTORY is not None: | |
| builtin_tools_root = Path(__file__).parent / "tools" | |
| external_tools = _collect_tool_module_names(self.TOOLS_DIRECTORY) | |
| internal_tools = _collect_tool_module_names(builtin_tools_root) | |
| _raise_on_name_collisions( | |
| label="tool", | |
| external_root=self.TOOLS_DIRECTORY, | |
| internal_root=builtin_tools_root, | |
| external_names=external_tools, | |
| internal_names=internal_tools, | |
| ) | |
| if self.PROFILES_DIRECTORY != DEFAULT_PROFILES_DIRECTORY: | |
| logger.warning( | |
| "Environment variable 'REACHY_MINI_EXTERNAL_PROFILES_DIRECTORY' is set. " | |
| "Profiles (instructions.txt, ...) will be loaded from %s.", | |
| self.PROFILES_DIRECTORY, | |
| ) | |
| else: | |
| logger.info( | |
| "'REACHY_MINI_EXTERNAL_PROFILES_DIRECTORY' is not set. " | |
| "Using built-in profiles from %s.", | |
| DEFAULT_PROFILES_DIRECTORY, | |
| ) | |
| if self.TOOLS_DIRECTORY is not None: | |
| logger.warning( | |
| "Environment variable 'REACHY_MINI_EXTERNAL_TOOLS_DIRECTORY' is set. " | |
| "External tools will be loaded from %s.", | |
| self.TOOLS_DIRECTORY, | |
| ) | |
| else: | |
| logger.info( | |
| "'REACHY_MINI_EXTERNAL_TOOLS_DIRECTORY' is not set. " | |
| "Using built-in shared tools only." | |
| ) | |
| config = Config() | |
| def set_custom_profile(profile: str | None) -> None: | |
| """Update the selected custom profile at runtime and expose it via env. | |
| This ensures modules that read `config` and code that inspects the | |
| environment see a consistent value. | |
| """ | |
| if LOCKED_PROFILE is not None: | |
| return | |
| try: | |
| config.REACHY_MINI_CUSTOM_PROFILE = profile | |
| except Exception: | |
| pass | |
| try: | |
| import os as _os | |
| if profile: | |
| _os.environ["REACHY_MINI_CUSTOM_PROFILE"] = profile | |
| else: | |
| # Remove to reflect default | |
| _os.environ.pop("REACHY_MINI_CUSTOM_PROFILE", None) | |
| except Exception: | |
| pass | |