diff --git a/lib/env_loader.py b/lib/env_loader.py index 48d6a12db..1af26f6cf 100644 --- a/lib/env_loader.py +++ b/lib/env_loader.py @@ -6,19 +6,64 @@ from __future__ import annotations import os +import re from pathlib import Path from typing import Optional -from dotenv import load_dotenv +# Try to import python-dotenv, but fall back gracefully +try: + from dotenv import load_dotenv as _load_dotenv_lib + HAS_DOTENV = True +except ImportError: + HAS_DOTENV = False -def load_env(project_root: Optional[Path] = None) -> None: - """Load .env file from project root.""" +def load_env(project_root: Optional[Path] = None, use_dotenv: bool = False) -> None: + """Load .env file from project root. + + Args: + project_root: Project root directory (defaults to parent of this file) + use_dotenv: If True, use python-dotenv if available; otherwise use manual parsing + """ if project_root is None: project_root = Path(__file__).resolve().parent.parent env_path = project_root / ".env" - if env_path.exists(): - load_dotenv(env_path) + + if not env_path.exists(): + return + + if use_dotenv and HAS_DOTENV: + _load_dotenv_lib(env_path) + else: + _load_env_manual(env_path) + + +def _load_env_manual(env_path: Path) -> None: + """Manual .env parsing (no dependencies) - matches original behavior.""" + with open(env_path, encoding="utf-8", errors="ignore") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") + key = key.strip() + value = value.strip() + + # Quoted value: take the content inside the quotes verbatim + if value[:1] in ("'", '"'): + quote = value[0] + end = value.find(quote, 1) + value = value[1:end] if end != -1 else value[1:] + else: + # Strip an inline comment ('#' at line start or after + # whitespace) so "VAR= # note" yields "" not "# note" + match = re.search(r"(^|\s)#", value) + if match: + value = value[: match.start()] + value = value.strip() + + if key and key not in os.environ: + os.environ[key] = value def get_env(key: str, default: Optional[str] = None) -> Optional[str]: diff --git a/tools/base_tool.py b/tools/base_tool.py index 15d24e6cd..c7f7f01e9 100644 --- a/tools/base_tool.py +++ b/tools/base_tool.py @@ -21,43 +21,11 @@ from pathlib import Path from typing import Any, Callable, Optional +from lib.env_loader import load_env -def _load_dotenv() -> None: - """Load .env into os.environ once at import time. - This ensures API keys are available before any tool is instantiated, - even when tools are imported directly without going through the registry. - Only sets variables that are not already in the environment. - """ - env_path = Path(__file__).resolve().parent.parent / ".env" - if not env_path.is_file(): - return - import re - with open(env_path, encoding="utf-8", errors="ignore") as f: - for line in f: - line = line.strip() - if not line or line.startswith("#") or "=" not in line: - continue - key, _, value = line.partition("=") - key = key.strip() - value = value.strip() - # Quoted value: take the content inside the quotes verbatim. - if value[:1] in ("'", '"'): - quote = value[0] - end = value.find(quote, 1) - value = value[1:end] if end != -1 else value[1:] - else: - # Strip an inline comment ('#' at line start or after - # whitespace) so "VAR= # note" yields "" not "# note". - match = re.search(r"(^|\s)#", value) - if match: - value = value[: match.start()] - value = value.strip() - if key and key not in os.environ: - os.environ[key] = value - - -_load_dotenv() +# Load .env file on import - use manual parsing for backward compatibility +load_env() class ToolTier(str, Enum): diff --git a/tools/tool_registry.py b/tools/tool_registry.py index 7dcf0d32c..cd44c2318 100644 --- a/tools/tool_registry.py +++ b/tools/tool_registry.py @@ -12,6 +12,7 @@ from types import ModuleType from typing import Any, Optional +from lib.env_loader import load_env from tools.base_tool import BaseTool, ToolStatus, ToolTier, ToolStability @@ -83,41 +84,9 @@ def register_module(self, module: ModuleType) -> list[str]: registered.append(tool.name) return registered - @staticmethod - def _load_dotenv() -> None: - """Load .env file into os.environ if present, so tools can find API keys.""" - from pathlib import Path - import os - env_path = Path(__file__).resolve().parent.parent / ".env" - if not env_path.is_file(): - return - import re - with open(env_path, encoding="utf-8", errors="ignore") as f: - for line in f: - line = line.strip() - if not line or line.startswith("#") or "=" not in line: - continue - key, _, value = line.partition("=") - key = key.strip() - value = value.strip() - # Quoted value: take the content inside the quotes verbatim. - if value[:1] in ("'", '"'): - quote = value[0] - end = value.find(quote, 1) - value = value[1:end] if end != -1 else value[1:] - else: - # Strip an inline comment ('#' at line start or after - # whitespace) so "KEY= # note" yields "" not "# note". - match = re.search(r"(^|\s)#", value) - if match: - value = value[: match.start()] - value = value.strip() - if key and key not in os.environ: - os.environ[key] = value - def discover(self, package_name: str = "tools") -> list[str]: """Import a package tree and register any concrete tools it defines.""" - self._load_dotenv() + load_env() package = importlib.import_module(package_name) discovered: list[str] = [] package_paths = getattr(package, "__path__", None)