diff --git a/openevolve/config.py b/openevolve/config.py index 86f65d765..23f82822f 100644 --- a/openevolve/config.py +++ b/openevolve/config.py @@ -427,9 +427,18 @@ class Config: @classmethod def from_yaml(cls, path: Union[str, Path]) -> "Config": """Load configuration from a YAML file""" - with open(path, "r") as f: + config_path = Path(path).resolve() + with open(config_path, "r") as f: config_dict = yaml.safe_load(f) - return cls.from_dict(config_dict) + config = cls.from_dict(config_dict) + + # Resolve template_dir relative to config file location + if config.prompt.template_dir: + template_path = Path(config.prompt.template_dir) + if not template_path.is_absolute(): + config.prompt.template_dir = str((config_path.parent / template_path).resolve()) + + return config @classmethod def from_dict(cls, config_dict: Dict[str, Any]) -> "Config": diff --git a/openevolve/prompt/templates.py b/openevolve/prompt/templates.py index 465f92d78..deac051c2 100644 --- a/openevolve/prompt/templates.py +++ b/openevolve/prompt/templates.py @@ -4,9 +4,12 @@ import os import json +import logging from pathlib import Path from typing import Dict, List, Optional, Union, Any +logger = logging.getLogger(__name__) + # Base system message template for evolution BASE_SYSTEM_TEMPLATE = """You are an expert software developer tasked with iteratively improving a codebase. Your job is to analyze the current program and suggest improvements based on feedback from previous attempts. @@ -185,8 +188,13 @@ def __init__(self, custom_template_dir: Optional[str] = None): self._load_from_directory(self.default_dir) # 2. Override with custom templates (if provided) - if self.custom_dir and self.custom_dir.exists(): - self._load_from_directory(self.custom_dir) + if self.custom_dir: + if self.custom_dir.exists(): + self._load_from_directory(self.custom_dir) + else: + logger.warning( + f"Custom template directory does not exist, using default prompt." + ) def _load_from_directory(self, directory: Path) -> None: """Load all templates and fragments from a directory""" diff --git a/tests/test_template_dir_resolution.py b/tests/test_template_dir_resolution.py new file mode 100644 index 000000000..3c13799be --- /dev/null +++ b/tests/test_template_dir_resolution.py @@ -0,0 +1,119 @@ +""" +Tests for template_dir path resolution +""" + +import unittest +import tempfile +import yaml +from pathlib import Path + +from openevolve.config import Config + + +class TestTemplateDirResolution(unittest.TestCase): + """Test that template_dir paths are resolved relative to config file location""" + + def test_relative_template_dir_resolved_to_config_location(self): + """Relative paths in template_dir should resolve relative to config file""" + # Create a temporary directory structure + with tempfile.TemporaryDirectory() as tmpdir: + config_dir = Path(tmpdir) / "config_subdir" + config_dir.mkdir() + config_file = config_dir / "test_config.yaml" + + # Write config with relative template_dir + config_data = { + "prompt": {"template_dir": "templates"}, + "llm": {"models": [{"name": "gpt-4", "weight": 1.0}]}, + } + with open(config_file, "w") as f: + yaml.dump(config_data, f) + + # Load config + config = Config.from_yaml(config_file) + + # Template_dir should be resolved relative to config file location + expected_path = str((config_dir / "templates").resolve()) + self.assertEqual(config.prompt.template_dir, expected_path) + + def test_absolute_template_dir_unchanged(self): + """Absolute paths in template_dir should remain unchanged""" + with tempfile.TemporaryDirectory() as tmpdir: + config_file = Path(tmpdir) / "test_config.yaml" + absolute_template_path = "/absolute/path/to/templates" + + # Write config with absolute template_dir + config_data = { + "prompt": {"template_dir": absolute_template_path}, + "llm": {"models": [{"name": "gpt-4", "weight": 1.0}]}, + } + with open(config_file, "w") as f: + yaml.dump(config_data, f) + + # Load config + config = Config.from_yaml(config_file) + + # Absolute path should remain unchanged + self.assertEqual(config.prompt.template_dir, absolute_template_path) + + def test_null_template_dir_unchanged(self): + """Null template_dir should remain None""" + with tempfile.TemporaryDirectory() as tmpdir: + config_file = Path(tmpdir) / "test_config.yaml" + + # Write config with null template_dir + config_data = { + "prompt": {"template_dir": None}, + "llm": {"models": [{"name": "gpt-4", "weight": 1.0}]}, + } + with open(config_file, "w") as f: + yaml.dump(config_data, f) + + # Load config + config = Config.from_yaml(config_file) + + # None should remain None + self.assertIsNone(config.prompt.template_dir) + + def test_nested_relative_template_dir(self): + """Nested relative paths should resolve correctly""" + with tempfile.TemporaryDirectory() as tmpdir: + config_dir = Path(tmpdir) / "configs" + config_dir.mkdir() + config_file = config_dir / "test_config.yaml" + + # Write config with nested relative path + config_data = { + "prompt": {"template_dir": "../templates/custom"}, + "llm": {"models": [{"name": "gpt-4", "weight": 1.0}]}, + } + with open(config_file, "w") as f: + yaml.dump(config_data, f) + + # Load config + config = Config.from_yaml(config_file) + + # Should resolve to /templates/custom + expected_path = str((config_dir / "../templates/custom").resolve()) + self.assertEqual(config.prompt.template_dir, expected_path) + + def test_real_example_config(self): + """Test with real example config file""" + # This test uses the actual llm_prompt_optimization example + config_path = "examples/llm_prompt_optimization/config.yaml" + if not Path(config_path).exists(): + self.skipTest(f"Example config not found: {config_path}") + + config = Config.from_yaml(config_path) + + # Should resolve to examples/llm_prompt_optimization/templates + expected_dir = Path("examples/llm_prompt_optimization/templates").resolve() + actual_dir = Path(config.prompt.template_dir) + + self.assertEqual(actual_dir, expected_dir) + # Verify the resolved path is absolute + self.assertTrue(actual_dir.is_absolute()) + + +if __name__ == "__main__": + unittest.main()