Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions prompts/CodeReviewer/versions/v010.md
Original file line number Diff line number Diff line change
@@ -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}}
```
7 changes: 7 additions & 0 deletions prompts/ComplexCodeReviewer/versions/v009.md
Original file line number Diff line number Diff line change
@@ -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'.
1 change: 1 addition & 0 deletions prompts/SimpleChat/versions/v010.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 16 additions & 0 deletions prompts/TemplateDemo/versions/v009.md
Original file line number Diff line number Diff line change
@@ -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 %}
Comment on lines +3 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider handling invalid difficulty values.

The conditional defaults to "full technical details" for any value other than 'beginner' or 'intermediate'. If an invalid or unexpected difficulty value is passed (e.g., typo, case mismatch, None), it silently falls through to the advanced content rather than providing a neutral default or raising an error.

🛡️ Suggested improvement for robustness

Consider whether a default message or validation would be more appropriate for unexpected values:

 {% if difficulty == 'beginner' %}
 Keep it simple and accessible for beginners.
 {% elif difficulty == 'intermediate' %}
 Include some advanced concepts but explain them clearly.
+{% elif difficulty == 'advanced' %}
+Don't hold back on technical details and advanced concepts.
 {% else %}
-Don't hold back on technical details and advanced concepts.
+Use a balanced approach suitable for general audiences.
 {% endif %}
🤖 Prompt for AI Agents
In @prompts/TemplateDemo/versions/v009.md around lines 3 - 9, The template's
conditional on the variable difficulty currently treats any unexpected value as
“advanced”; update the logic to explicitly handle valid options and a safe
default: change the branches to check for 'beginner', 'intermediate', and an
explicit 'advanced' (or whatever intended top level) and then add an else branch
that either emits a neutral default message (e.g., "Please provide a valid
difficulty: beginner, intermediate, or advanced.") or raises/returns a
validation error; ensure you update any docs/tests that assume the previous
implicit fallback and reference the difficulty variable in the template to
locate and modify the conditional.


{% if elements|length > 0 %}
Be sure to include the following elements:
{% for element in elements %}
- {{element}}
{% endfor %}
{% endif %}
11 changes: 11 additions & 0 deletions prompts/simple_chat/versions/v009.md
Original file line number Diff line number Diff line change
@@ -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?
100 changes: 87 additions & 13 deletions src/promptix/core/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)


Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
123 changes: 122 additions & 1 deletion tests/functional/test_builder_pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,125 @@ def test_builder_validation():
# Verify basic config structure
assert isinstance(config, dict)
assert "messages" in config
assert "model" in config
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)
Loading