diff --git a/prompts/CodeReviewer/versions/v010.md b/prompts/CodeReviewer/versions/v010.md new file mode 100644 index 0000000..700beb9 --- /dev/null +++ b/prompts/CodeReviewer/versions/v010.md @@ -0,0 +1,5 @@ +You are a code review assistant specialized in {{programming_language}}. Please review the following code snippet and provide feedback on {{review_focus}}: + +```{{programming_language}} +{{code_snippet}} +``` diff --git a/prompts/ComplexCodeReviewer/versions/v009.md b/prompts/ComplexCodeReviewer/versions/v009.md new file mode 100644 index 0000000..50f329b --- /dev/null +++ b/prompts/ComplexCodeReviewer/versions/v009.md @@ -0,0 +1,7 @@ +You are a code review assistant with active tools: {{active_tools}}. Specialized in {{programming_language}}. Review the code with {{severity}} scrutiny focusing on {{review_focus}}: + +```{{programming_language}} +{{code_snippet}} +``` + +Provide feedback in: 'Summary', 'Critical Issues', 'Improvements', 'Positives'. diff --git a/prompts/SimpleChat/versions/v010.md b/prompts/SimpleChat/versions/v010.md new file mode 100644 index 0000000..f394f88 --- /dev/null +++ b/prompts/SimpleChat/versions/v010.md @@ -0,0 +1 @@ +You are a helpful AI assistant named {{assistant_name}}. Your goal is to provide clear and concise answers to {{user_name}}'s questions. diff --git a/prompts/TemplateDemo/versions/v009.md b/prompts/TemplateDemo/versions/v009.md new file mode 100644 index 0000000..c328f53 --- /dev/null +++ b/prompts/TemplateDemo/versions/v009.md @@ -0,0 +1,16 @@ +You are creating a {{content_type}} about {{theme}}. + +{% if difficulty == 'beginner' %} +Keep it simple and accessible for beginners. +{% elif difficulty == 'intermediate' %} +Include some advanced concepts but explain them clearly. +{% else %} +Don't hold back on technical details and advanced concepts. +{% endif %} + +{% if elements|length > 0 %} +Be sure to include the following elements: +{% for element in elements %} +- {{element}} +{% endfor %} +{% endif %} diff --git a/prompts/simple_chat/versions/v009.md b/prompts/simple_chat/versions/v009.md new file mode 100644 index 0000000..f25fba3 --- /dev/null +++ b/prompts/simple_chat/versions/v009.md @@ -0,0 +1,11 @@ +You are a {{personality}} assistant specialized in {{domain}}. + +Your role is to provide helpful, accurate, and engaging responses to user questions and requests. Always maintain a professional and friendly tone while adapting to the user's needs. + +Key guidelines: +- Be concise but thorough in your explanations +- Ask clarifying questions when needed +- Provide examples when helpful +- Stay focused on the {{domain}} domain when specified + +How can I help you today? \ No newline at end of file diff --git a/src/promptix/core/builder.py b/src/promptix/core/builder.py index 904e57a..9e796d5 100644 --- a/src/promptix/core/builder.py +++ b/src/promptix/core/builder.py @@ -5,27 +5,27 @@ focused components and dependency injection for better testability and modularity. """ -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Tuple, Union from .container import get_container from .components import ( PromptLoader, VariableValidator, TemplateRenderer, VersionManager, - ModelConfigBuilder + ModelConfigBuilder, + LayerComposer, ) from .adapters._base import ModelAdapter from .exceptions import ( PromptNotFoundError, VersionNotFoundError, UnsupportedClientError, - ToolNotFoundError, - ToolProcessingError, ValidationError, StorageError, RequiredVariableError, VariableValidationError, - TemplateRenderError + TemplateRenderError, + LayerRequiredError, ) @@ -58,7 +58,14 @@ def __init__(self, prompt_template: str, container=None): self._model_config_builder = self._container.get_typed("model_config_builder", ModelConfigBuilder) self._logger = self._container.get("logger") self._adapters = self._container.get("adapters") - + + # Layer composition support + self._use_layers = False + self._explicit_layers: Dict[str, Tuple[str, Optional[str]]] = {} + self._skip_layers: List[str] = [] + self._layer_versions: Dict[str, str] = {} + self._layer_composer = self._container.get_typed("layer_composer", LayerComposer) + # Initialize prompt data self._initialize_prompt_data() @@ -200,7 +207,58 @@ def with_extra(self, extra_params: Dict[str, Any]): """ self._model_params.update(extra_params) return self - + + def with_layers(self, enabled: bool = True) -> "PromptixBuilder": + """Enable layer composition for this build. + + When enabled, layers are auto-selected based on variables that match + layer configuration in config.yaml. + + Args: + enabled: Whether to enable layer composition. Defaults to True. + + Returns: + Self for method chaining. + """ + self._use_layers = enabled + return self + + def with_layer( + self, + layer_name: str, + value: str, + version: Optional[str] = None + ) -> "PromptixBuilder": + """Explicitly set a layer value. + + Calling this method automatically enables layer composition. + + Args: + layer_name: Name of the layer (e.g., 'product_line', 'tier') + value: Layer variant to use (e.g., 'software', 'premium') + version: Optional version of this layer + + Returns: + Self for method chaining. + """ + self._use_layers = True + self._explicit_layers[layer_name] = (value, version) + if version: + self._layer_versions[layer_name] = version + return self + + def skip_layer(self, *layer_names: str) -> "PromptixBuilder": + """Skip specified layers during composition. + + Args: + *layer_names: Names of layers to skip + + Returns: + Self for method chaining. + """ + self._skip_layers.extend(layer_names) + return self + def with_memory(self, memory: List[Dict[str, str]]): """Set the conversation memory. @@ -302,7 +360,7 @@ def with_version(self, version: str): return self - def with_tool(self, tool_name: str, *args, **kwargs) -> "PromptixBuilder": + def with_tool(self, tool_name: str, *args) -> "PromptixBuilder": """Activate a tool by name. Args: @@ -523,11 +581,27 @@ def build(self, system_only: bool = False) -> Union[Dict[str, Any], str]: self._logger.warning(f"Required field '{field}' is missing from prompt parameters") try: - # Generate the system message using the template renderer - from .base import Promptix # Import here to avoid circular dependency - promptix_instance = Promptix(self._container) - system_message = promptix_instance.render_prompt(self.prompt_template, self.custom_version, **self._data) - except (ValueError, ImportError, RuntimeError, RequiredVariableError, VariableValidationError) as e: + # Check if layer composition is enabled + if self._use_layers: + # Merge explicit layers into variables + for layer_name, (value, _) in self._explicit_layers.items(): + self._data[layer_name] = value + + system_message = self._layer_composer.compose( + prompt_name=self.prompt_template, + variables=self._data, + base_version=self.custom_version, + layer_versions=self._layer_versions, + skip_layers=self._skip_layers + ) + else: + # Generate the system message using the template renderer + from .base import Promptix # Import here to avoid circular dependency + promptix_instance = Promptix(self._container) + system_message = promptix_instance.render_prompt( + self.prompt_template, self.custom_version, **self._data + ) + except (ValueError, ImportError, RuntimeError, RequiredVariableError, VariableValidationError, LayerRequiredError) as e: if self._logger: self._logger.warning(f"Error generating system message: {e!s}") # Provide a fallback basic message when template rendering fails diff --git a/tests/functional/test_builder_pattern.py b/tests/functional/test_builder_pattern.py index 646b8ae..7182e28 100644 --- a/tests/functional/test_builder_pattern.py +++ b/tests/functional/test_builder_pattern.py @@ -112,4 +112,125 @@ def test_builder_validation(): # Verify basic config structure assert isinstance(config, dict) assert "messages" in config - assert "model" in config \ No newline at end of file + assert "model" in config + + +class TestBuilderLayerComposition: + """Test builder pattern with layer composition.""" + + @pytest.fixture + def layers_container(self, test_prompts_layers_dir, monkeypatch): + """Create container configured for layer testing.""" + from promptix.core.container import Container, set_container, reset_container + from promptix.core.components import LayerComposer + from promptix.core import config as config_module + + # Patch config to return test layers directory as prompts path + monkeypatch.setattr( + config_module.config, + "get_prompts_workspace_path", + lambda: test_prompts_layers_dir + ) + + container = Container() + container.override( + "layer_composer", + LayerComposer( + prompts_dir=test_prompts_layers_dir, + logger=container.get("logger") + ) + ) + set_container(container) + yield container + reset_container() + + def test_with_layers_basic(self, layers_container): + """Test basic layer composition via builder.""" + config = ( + Promptix.builder("CustomerSupport") + .with_layers() + .with_var({"company_name": "TechCorp", "product_line": "software"}) + .build() + ) + + assert isinstance(config, dict) + assert "messages" in config + system_msg = config["messages"][0]["content"] + assert "technical support specialist" in system_msg.lower() + + def test_with_layer_explicit(self, layers_container): + """Test explicit layer selection.""" + config = ( + Promptix.builder("CustomerSupport") + .with_layer("product_line", "hardware") + .with_layer("tier", "premium") + .with_company_name("HardwareCo") + .build() + ) + + assert isinstance(config, dict) + system_msg = config["messages"][0]["content"] + assert "hardware" in system_msg.lower() + assert "Dedicated account manager" in system_msg + + def test_with_layer_version(self, layers_container): + """Test layer version selection.""" + config = ( + Promptix.builder("CustomerSupport") + .with_layer("tier", "premium", version="v1") + .with_company_name("Test Corp") + .build() + ) + + assert isinstance(config, dict) + system_msg = config["messages"][0]["content"] + assert "(v1)" in system_msg + + def test_skip_layer(self, layers_container): + """Test skipping layers.""" + # With region + config_with = ( + Promptix.builder("CustomerSupport") + .with_layers() + .with_var({"company_name": "EU Corp", "region": "eu"}) + .build() + ) + + # Without region (skipped) + config_without = ( + Promptix.builder("CustomerSupport") + .with_layers() + .with_var({"company_name": "EU Corp", "region": "eu"}) + .skip_layer("region") + .build() + ) + + msg_with = config_with["messages"][0]["content"] + msg_without = config_without["messages"][0]["content"] + + assert "Vous etes un assistant" in msg_with + assert "Vous etes un assistant" not in msg_without + + def test_system_only_with_layers(self, layers_container): + """Test system_only mode with layers.""" + system_msg = ( + Promptix.builder("CustomerSupport") + .with_layer("product_line", "software") + .with_company_name("TestCo") + .build(system_only=True) + ) + + assert isinstance(system_msg, str) + assert "technical support specialist" in system_msg.lower() + + def test_layers_disabled_by_default(self, layers_container): + """Verify layers are not applied without with_layers().""" + config = ( + Promptix.builder("CustomerSupport") + .with_var({"company_name": "Test", "product_line": "software"}) + .build() + ) + + # Without with_layers(), should use standard rendering + # which doesn't process layer blocks + assert isinstance(config, dict)