jinja2 support
This commit is contained in:
118
README.md
118
README.md
@@ -1,6 +1,6 @@
|
||||
# LLMUtils
|
||||
|
||||
A Python utility library for managing LLM prompts with template variables and JSON schemas.
|
||||
A Python utility library for managing LLM prompts with Jinja2 template support and JSON schemas.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -18,6 +18,7 @@ pip install git+https://git.project-insanity.de/gmarth/LLMUtils.git
|
||||
|
||||
## Features
|
||||
|
||||
- **Jinja2 Template Engine**: Full support for loops, conditionals, filters, and complex data structures
|
||||
- **Smart Prompt Management**: Load and manage prompt templates with variable substitution
|
||||
- **On-demand Loading**: Prompts are loaded lazily at runtime for better performance
|
||||
- **Caching Support**: Optional caching to avoid repeated disk reads
|
||||
@@ -64,6 +65,37 @@ if not result.validate(name='Alice'):
|
||||
filled = result.fill(name='Alice', age=30)
|
||||
```
|
||||
|
||||
### Advanced Jinja2 Features
|
||||
|
||||
```python
|
||||
# Using lists and loops
|
||||
result = PromptManager.get_prompt('task_list')
|
||||
filled = result.fill(
|
||||
tasks=['Write code', 'Review PR', 'Deploy'],
|
||||
priority='high'
|
||||
)
|
||||
|
||||
# Using conditionals
|
||||
result = PromptManager.get_prompt('status_report')
|
||||
filled = result.fill(
|
||||
error='Connection timeout', # Will show error message
|
||||
items=[] # Will show "No items"
|
||||
)
|
||||
|
||||
# Using complex nested data
|
||||
result = PromptManager.get_prompt('user_profile')
|
||||
filled = result.fill(
|
||||
user={
|
||||
'name': 'Alice',
|
||||
'roles': ['admin', 'developer'],
|
||||
'projects': [
|
||||
{'name': 'Project A', 'status': 'active'},
|
||||
{'name': 'Project B', 'status': 'completed'}
|
||||
]
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### JSON Schema Support
|
||||
|
||||
```python
|
||||
@@ -98,14 +130,59 @@ Place your prompt templates in the `prompts/` directory:
|
||||
- `prompts/greeting.md` - Markdown file with template
|
||||
- `prompts/greeting.json` - Optional JSON schema for structured output
|
||||
|
||||
Example prompt template (`greeting.md`):
|
||||
### Simple Template Example (`greeting.md`):
|
||||
```markdown
|
||||
Hello {{name}},
|
||||
|
||||
You are {{age}} years old.
|
||||
```
|
||||
|
||||
Example schema (`greeting.json`):
|
||||
### Jinja2 Template Examples
|
||||
|
||||
#### Lists and Loops (`task_list.md`):
|
||||
```markdown
|
||||
Priority: {{ priority }}
|
||||
|
||||
Tasks to complete:
|
||||
{% for task in tasks %}
|
||||
- {{ task }}
|
||||
{% endfor %}
|
||||
|
||||
Total: {{ tasks | length }} tasks
|
||||
```
|
||||
|
||||
#### Conditionals (`status_report.md`):
|
||||
```markdown
|
||||
{% if error %}
|
||||
⚠️ ERROR: {{ error }}
|
||||
{% else %}
|
||||
✅ All systems operational
|
||||
{% endif %}
|
||||
|
||||
{% if items %}
|
||||
Items ({{ items | length }}):
|
||||
{% for item in items %}
|
||||
{{ loop.index }}. {{ item }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
No items to process.
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
#### Complex Data (`user_profile.md`):
|
||||
```markdown
|
||||
# User: {{ user.name }}
|
||||
|
||||
## Roles
|
||||
{{ user.roles | join(', ') }}
|
||||
|
||||
## Projects
|
||||
{% for project in user.projects %}
|
||||
- {{ project.name }} [{{ project.status | upper }}]
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### JSON Schema Example (`greeting.json`):
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
@@ -119,22 +196,22 @@ Example schema (`greeting.json`):
|
||||
|
||||
## API Reference
|
||||
|
||||
### PromptResult Class
|
||||
### ManagedPrompt Class
|
||||
|
||||
The `PromptResult` dataclass returned by `get_prompt()`:
|
||||
The `ManagedPrompt` dataclass returned by `get_prompt()`:
|
||||
|
||||
- `template: str` - The original template string
|
||||
- `template: str` - The original Jinja2 template string
|
||||
- `name: str` - The prompt name
|
||||
- `variables: Set[str]` - Required template variables
|
||||
- `variables: Set[str]` - Required template variables (auto-extracted from Jinja2)
|
||||
- `schema: Optional[Dict]` - Associated JSON schema
|
||||
- `prompt: str` - Property that returns filled prompt or template
|
||||
- `fill(**kwargs) -> str` - Fill template with variables
|
||||
- `fill(**kwargs) -> str` - Fill template with variables using Jinja2
|
||||
- `validate(**kwargs) -> bool` - Check if all variables provided
|
||||
- `get_missing_variables(**kwargs) -> Set[str]` - Get missing variables
|
||||
|
||||
### PromptManager Methods
|
||||
|
||||
- `get_prompt(prompt_name, **kwargs) -> PromptResult` - Get a prompt template
|
||||
- `get_prompt(prompt_name, **kwargs) -> ManagedPrompt` - Get a prompt template
|
||||
- `get_schema(prompt_name) -> Optional[Dict]` - Get just the schema
|
||||
- `has_schema(prompt_name) -> bool` - Check if prompt has schema
|
||||
- `list_prompts() -> Dict` - List all available prompts
|
||||
@@ -142,6 +219,29 @@ The `PromptResult` dataclass returned by `get_prompt()`:
|
||||
- `configure(path=None, caching=None)` - Configure settings
|
||||
- `reload_prompts()` - Clear the cache
|
||||
|
||||
### Jinja2 Template Features
|
||||
|
||||
The library supports all standard Jinja2 features:
|
||||
|
||||
#### Filters
|
||||
- `{{ items | length }}` - Get length of list
|
||||
- `{{ name | upper }}` - Convert to uppercase
|
||||
- `{{ name | lower }}` - Convert to lowercase
|
||||
- `{{ skills | join(', ') }}` - Join list items
|
||||
- `{{ data | tojson }}` - Convert to JSON
|
||||
- `{{ price | round(2) }}` - Round numbers
|
||||
|
||||
#### Loops
|
||||
- `{% for item in items %}...{% endfor %}` - Iterate over lists
|
||||
- `{{ loop.index }}` - Current iteration (1-indexed)
|
||||
- `{{ loop.index0 }}` - Current iteration (0-indexed)
|
||||
- `{% for key, value in dict.items() %}...{% endfor %}` - Iterate over dictionaries
|
||||
|
||||
#### Conditionals
|
||||
- `{% if condition %}...{% endif %}` - Simple conditional
|
||||
- `{% if condition %}...{% else %}...{% endif %}` - If/else
|
||||
- `{% if condition %}...{% elif other %}...{% else %}...{% endif %}` - Multiple conditions
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
221
example_jinja2.py
Normal file
221
example_jinja2.py
Normal file
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example showcasing Jinja2 template features in prompt management"""
|
||||
|
||||
from pathlib import Path
|
||||
from src.llmutils.prompt_manager import PromptManager
|
||||
|
||||
# Create example prompts directory
|
||||
prompts_dir = Path("prompts")
|
||||
prompts_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Create a code generation prompt with lists
|
||||
code_gen_prompt = prompts_dir / "code_generator.md"
|
||||
code_gen_prompt.write_text("""Generate a Python class with the following specifications:
|
||||
|
||||
Class Name: {{ class_name }}
|
||||
{% if parent_class %}
|
||||
Inherits from: {{ parent_class }}
|
||||
{% endif %}
|
||||
|
||||
## Attributes:
|
||||
{% for attr in attributes %}
|
||||
- {{ attr.name }}: {{ attr.type }}{% if attr.default %} = {{ attr.default }}{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
## Methods:
|
||||
{% for method in methods %}
|
||||
### {{ method.name }}({{ method.params | join(', ') }})
|
||||
{{ method.description }}
|
||||
Returns: {{ method.returns }}
|
||||
{% endfor %}
|
||||
|
||||
## Example Usage:
|
||||
```python
|
||||
{% for example in examples %}
|
||||
{{ example }}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
{% if additional_notes %}
|
||||
## Notes:
|
||||
{% for note in additional_notes %}
|
||||
- {{ note }}
|
||||
{% endfor %}
|
||||
{% endif %}""")
|
||||
|
||||
# Create an API documentation prompt
|
||||
api_doc_prompt = prompts_dir / "api_documentation.md"
|
||||
api_doc_prompt.write_text("""# API Documentation: {{ api_name }}
|
||||
|
||||
Base URL: `{{ base_url }}`
|
||||
Version: {{ version }}
|
||||
|
||||
## Authentication
|
||||
{{ auth_method }}
|
||||
|
||||
## Endpoints
|
||||
|
||||
{% for endpoint in endpoints %}
|
||||
### {{ endpoint.method }} {{ endpoint.path }}
|
||||
|
||||
**Description:** {{ endpoint.description }}
|
||||
|
||||
{% if endpoint.params %}
|
||||
**Parameters:**
|
||||
{% for param in endpoint.params %}
|
||||
- `{{ param.name }}` ({{ param.type }}): {{ param.description }}{% if param.required %} *[Required]*{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if endpoint.request_body %}
|
||||
**Request Body:**
|
||||
```json
|
||||
{{ endpoint.request_body | tojson(indent=2) }}
|
||||
```
|
||||
{% endif %}
|
||||
|
||||
**Response:** `{{ endpoint.response_code }}`
|
||||
```json
|
||||
{{ endpoint.response_example | tojson(indent=2) }}
|
||||
```
|
||||
|
||||
---
|
||||
{% endfor %}
|
||||
|
||||
## Rate Limiting
|
||||
{{ rate_limit }} requests per {{ rate_limit_window }}
|
||||
|
||||
## Error Codes
|
||||
{% for error in error_codes %}
|
||||
- `{{ error.code }}`: {{ error.message }}
|
||||
{% endfor %}""")
|
||||
|
||||
# Configure PromptManager
|
||||
PromptManager.configure(path=prompts_dir)
|
||||
|
||||
print("=" * 70)
|
||||
print("Jinja2 Template Examples with Lists and Complex Data")
|
||||
print("=" * 70)
|
||||
|
||||
# Example 1: Code Generation
|
||||
print("\n1. CODE GENERATION PROMPT")
|
||||
print("-" * 70)
|
||||
|
||||
prompt = PromptManager.get_prompt("code_generator")
|
||||
filled = prompt.fill(
|
||||
class_name="UserProfile",
|
||||
parent_class="BaseModel",
|
||||
attributes=[
|
||||
{"name": "user_id", "type": "int"},
|
||||
{"name": "username", "type": "str"},
|
||||
{"name": "email", "type": "str"},
|
||||
{"name": "created_at", "type": "datetime", "default": "datetime.now()"},
|
||||
{"name": "is_active", "type": "bool", "default": "True"}
|
||||
],
|
||||
methods=[
|
||||
{
|
||||
"name": "validate_email",
|
||||
"params": ["self"],
|
||||
"description": "Validates the email format",
|
||||
"returns": "bool"
|
||||
},
|
||||
{
|
||||
"name": "update_profile",
|
||||
"params": ["self", "**kwargs"],
|
||||
"description": "Updates user profile with provided fields",
|
||||
"returns": "None"
|
||||
},
|
||||
{
|
||||
"name": "to_dict",
|
||||
"params": ["self"],
|
||||
"description": "Converts the profile to a dictionary",
|
||||
"returns": "Dict[str, Any]"
|
||||
}
|
||||
],
|
||||
examples=[
|
||||
"user = UserProfile(user_id=1, username='alice', email='alice@example.com')",
|
||||
"if user.validate_email():",
|
||||
" user.update_profile(is_active=False)",
|
||||
" print(user.to_dict())"
|
||||
],
|
||||
additional_notes=[
|
||||
"Email validation should follow RFC 5322",
|
||||
"All datetime values should be UTC",
|
||||
"Profile updates should be logged"
|
||||
]
|
||||
)
|
||||
print(filled)
|
||||
|
||||
# Example 2: API Documentation
|
||||
print("\n2. API DOCUMENTATION PROMPT")
|
||||
print("-" * 70)
|
||||
|
||||
prompt = PromptManager.get_prompt("api_documentation")
|
||||
filled = prompt.fill(
|
||||
api_name="User Management API",
|
||||
base_url="https://api.example.com/v1",
|
||||
version="1.0.0",
|
||||
auth_method="Bearer token in Authorization header",
|
||||
endpoints=[
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/users",
|
||||
"description": "List all users with optional filtering",
|
||||
"params": [
|
||||
{"name": "page", "type": "integer", "description": "Page number", "required": False},
|
||||
{"name": "limit", "type": "integer", "description": "Items per page", "required": False},
|
||||
{"name": "status", "type": "string", "description": "Filter by status", "required": False}
|
||||
],
|
||||
"response_code": "200 OK",
|
||||
"response_example": {
|
||||
"users": [
|
||||
{"id": 1, "username": "alice", "status": "active"},
|
||||
{"id": 2, "username": "bob", "status": "inactive"}
|
||||
],
|
||||
"total": 2,
|
||||
"page": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/users",
|
||||
"description": "Create a new user",
|
||||
"request_body": {
|
||||
"username": "string",
|
||||
"email": "string",
|
||||
"password": "string"
|
||||
},
|
||||
"response_code": "201 Created",
|
||||
"response_example": {
|
||||
"id": 3,
|
||||
"username": "charlie",
|
||||
"email": "charlie@example.com",
|
||||
"created_at": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/users/{id}",
|
||||
"description": "Delete a user by ID",
|
||||
"params": [
|
||||
{"name": "id", "type": "integer", "description": "User ID", "required": True}
|
||||
],
|
||||
"response_code": "204 No Content",
|
||||
"response_example": {}
|
||||
}
|
||||
],
|
||||
rate_limit=1000,
|
||||
rate_limit_window="hour",
|
||||
error_codes=[
|
||||
{"code": "400", "message": "Bad Request - Invalid parameters"},
|
||||
{"code": "401", "message": "Unauthorized - Invalid or missing token"},
|
||||
{"code": "404", "message": "Not Found - Resource doesn't exist"},
|
||||
{"code": "429", "message": "Too Many Requests - Rate limit exceeded"},
|
||||
{"code": "500", "message": "Internal Server Error"}
|
||||
]
|
||||
)
|
||||
print(filled)
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("Examples completed successfully!")
|
||||
print("=" * 70)
|
||||
44
prompts/api_documentation.md
Normal file
44
prompts/api_documentation.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# API Documentation: {{ api_name }}
|
||||
|
||||
Base URL: `{{ base_url }}`
|
||||
Version: {{ version }}
|
||||
|
||||
## Authentication
|
||||
{{ auth_method }}
|
||||
|
||||
## Endpoints
|
||||
|
||||
{% for endpoint in endpoints %}
|
||||
### {{ endpoint.method }} {{ endpoint.path }}
|
||||
|
||||
**Description:** {{ endpoint.description }}
|
||||
|
||||
{% if endpoint.params %}
|
||||
**Parameters:**
|
||||
{% for param in endpoint.params %}
|
||||
- `{{ param.name }}` ({{ param.type }}): {{ param.description }}{% if param.required %} *[Required]*{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if endpoint.request_body %}
|
||||
**Request Body:**
|
||||
```json
|
||||
{{ endpoint.request_body | tojson(indent=2) }}
|
||||
```
|
||||
{% endif %}
|
||||
|
||||
**Response:** `{{ endpoint.response_code }}`
|
||||
```json
|
||||
{{ endpoint.response_example | tojson(indent=2) }}
|
||||
```
|
||||
|
||||
---
|
||||
{% endfor %}
|
||||
|
||||
## Rate Limiting
|
||||
{{ rate_limit }} requests per {{ rate_limit_window }}
|
||||
|
||||
## Error Codes
|
||||
{% for error in error_codes %}
|
||||
- `{{ error.code }}`: {{ error.message }}
|
||||
{% endfor %}
|
||||
32
prompts/code_generator.md
Normal file
32
prompts/code_generator.md
Normal file
@@ -0,0 +1,32 @@
|
||||
Generate a Python class with the following specifications:
|
||||
|
||||
Class Name: {{ class_name }}
|
||||
{% if parent_class %}
|
||||
Inherits from: {{ parent_class }}
|
||||
{% endif %}
|
||||
|
||||
## Attributes:
|
||||
{% for attr in attributes %}
|
||||
- {{ attr.name }}: {{ attr.type }}{% if attr.default %} = {{ attr.default }}{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
## Methods:
|
||||
{% for method in methods %}
|
||||
### {{ method.name }}({{ method.params | join(', ') }})
|
||||
{{ method.description }}
|
||||
Returns: {{ method.returns }}
|
||||
{% endfor %}
|
||||
|
||||
## Example Usage:
|
||||
```python
|
||||
{% for example in examples %}
|
||||
{{ example }}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
{% if additional_notes %}
|
||||
## Notes:
|
||||
{% for note in additional_notes %}
|
||||
- {{ note }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -8,7 +8,7 @@ version = "0.1.0"
|
||||
description = "Utilities for working with LLMs"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
dependencies = ["jinja2>=3.0.0"]
|
||||
authors = [
|
||||
{name = "Alexander Thiess", email = "thiess.alexander@googlemail.com"}
|
||||
]
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Set, Optional
|
||||
import logging
|
||||
from jinja2 import Template, Environment, meta, TemplateSyntaxError, UndefinedError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,8 +17,9 @@ class ManagedPrompt:
|
||||
name: str
|
||||
variables: Set[str]
|
||||
schema: Optional[Dict[str, Any]] = None
|
||||
_filled_prompt: Optional[str] = None
|
||||
_context: Optional[Dict[str, Any]] = None
|
||||
_filled_prompt: Optional[str] = field(default=None, init=False, repr=False)
|
||||
_context: Optional[Dict[str, Any]] = field(default=None, init=False, repr=False)
|
||||
_jinja_template: Optional[Template] = field(default=None, init=False, repr=False)
|
||||
|
||||
def validate(self, **kwargs) -> bool:
|
||||
"""Validate that all required variables are provided.
|
||||
@@ -39,7 +41,7 @@ class ManagedPrompt:
|
||||
return self.variables - provided_vars
|
||||
|
||||
def fill(self, **kwargs) -> str:
|
||||
"""Fill the template with provided variables.
|
||||
"""Fill the template with provided variables using Jinja2.
|
||||
|
||||
Args:
|
||||
**kwargs: Variables to fill in the template
|
||||
@@ -48,8 +50,15 @@ class ManagedPrompt:
|
||||
The filled prompt string
|
||||
|
||||
Raises:
|
||||
ValueError: If required variables are missing
|
||||
ValueError: If required variables are missing or template syntax error
|
||||
"""
|
||||
# Create Jinja2 template if not already created
|
||||
if self._jinja_template is None:
|
||||
try:
|
||||
self._jinja_template = Template(self.template)
|
||||
except TemplateSyntaxError as e:
|
||||
raise ValueError(f"Invalid template syntax in prompt '{self.name}': {e}")
|
||||
|
||||
# If no variables required and none provided, return template as-is
|
||||
if not self.variables and not kwargs:
|
||||
self._filled_prompt = self.template
|
||||
@@ -63,15 +72,11 @@ class ManagedPrompt:
|
||||
f"Required: {self.variables}, Provided: {set(kwargs.keys())}"
|
||||
)
|
||||
|
||||
# Only process the template if there are actually variables to replace
|
||||
if self.variables:
|
||||
result = self.template
|
||||
for key, value in kwargs.items():
|
||||
if key in self.variables: # Only replace known variables
|
||||
placeholder = f"{{{{{key}}}}}" # {{key}}
|
||||
result = result.replace(placeholder, str(value))
|
||||
else:
|
||||
result = self.template
|
||||
try:
|
||||
# Render the template with Jinja2
|
||||
result = self._jinja_template.render(**kwargs)
|
||||
except UndefinedError as e:
|
||||
raise ValueError(f"Error rendering template '{self.name}': {e}")
|
||||
|
||||
# Cache the filled result
|
||||
self._filled_prompt = result
|
||||
@@ -182,10 +187,19 @@ class PromptManager:
|
||||
return Path.cwd() / 'prompts'
|
||||
|
||||
def _extract_variables(self, template: str) -> Set[str]:
|
||||
"""Extract all {{variable}} placeholders from template"""
|
||||
pattern = r'\{\{(\w+)\}\}'
|
||||
variables = set(re.findall(pattern, template))
|
||||
return variables
|
||||
"""Extract all variables from Jinja2 template"""
|
||||
try:
|
||||
# Create a Jinja2 environment and parse the template
|
||||
env = Environment()
|
||||
ast = env.parse(template)
|
||||
# Get all undeclared variables from the template
|
||||
variables = meta.find_undeclared_variables(ast)
|
||||
return variables
|
||||
except TemplateSyntaxError:
|
||||
# Fallback to simple regex for backwards compatibility
|
||||
pattern = r'\{\{\s*(\w+)\s*\}\}'
|
||||
variables = set(re.findall(pattern, template))
|
||||
return variables
|
||||
|
||||
def _validate_context(self, prompt_name: str, context: Dict[str, Any]) -> None:
|
||||
"""Validate that all required variables are provided"""
|
||||
@@ -208,14 +222,12 @@ class PromptManager:
|
||||
logger.warning(f"Extra variables provided for prompt '{prompt_name}': {extra_vars}")
|
||||
|
||||
def _fill_template(self, template: str, context: Dict[str, Any]) -> str:
|
||||
"""Fill template with context variables"""
|
||||
result = template
|
||||
|
||||
for key, value in context.items():
|
||||
placeholder = f"{{{{{key}}}}}" # {{key}}
|
||||
result = result.replace(placeholder, str(value))
|
||||
|
||||
return result
|
||||
"""Fill template with context variables using Jinja2"""
|
||||
try:
|
||||
jinja_template = Template(template)
|
||||
return jinja_template.render(**context)
|
||||
except (TemplateSyntaxError, UndefinedError) as e:
|
||||
raise ValueError(f"Error rendering template: {e}")
|
||||
|
||||
@classmethod
|
||||
def configure(cls, path: Optional[Path] = None, caching: Optional[bool] = None):
|
||||
|
||||
156
test_jinja2_prompts.py
Normal file
156
test_jinja2_prompts.py
Normal file
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test script for Jinja2-enhanced prompt management with list support"""
|
||||
|
||||
from pathlib import Path
|
||||
from src.llmutils.prompt_manager import PromptManager, ManagedPrompt
|
||||
|
||||
# Create a test prompts directory
|
||||
test_prompts_dir = Path("test_prompts")
|
||||
test_prompts_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Create test prompt with simple variable
|
||||
simple_prompt = test_prompts_dir / "simple.md"
|
||||
simple_prompt.write_text("""Hello {{name}}!
|
||||
Your age is {{age}}.""")
|
||||
|
||||
# Create test prompt with list iteration
|
||||
list_prompt = test_prompts_dir / "list_example.md"
|
||||
list_prompt.write_text("""# Task List
|
||||
|
||||
Here are your tasks:
|
||||
{% for task in tasks %}
|
||||
- {{ task }}
|
||||
{% endfor %}
|
||||
|
||||
Total tasks: {{ tasks | length }}""")
|
||||
|
||||
# Create test prompt with complex data structures
|
||||
complex_prompt = test_prompts_dir / "complex.md"
|
||||
complex_prompt.write_text("""# User Report
|
||||
|
||||
Name: {{ user.name }}
|
||||
Email: {{ user.email }}
|
||||
|
||||
## Assigned Tasks:
|
||||
{% for task in user.tasks %}
|
||||
- [{{ task.status }}] {{ task.title }}
|
||||
Priority: {{ task.priority }}
|
||||
{% endfor %}
|
||||
|
||||
## Skills:
|
||||
{{ skills | join(', ') }}
|
||||
|
||||
## Recent Activity:
|
||||
{% for date, activity in activities.items() %}
|
||||
- {{ date }}: {{ activity }}
|
||||
{% endfor %}""")
|
||||
|
||||
# Create prompt with conditionals
|
||||
conditional_prompt = test_prompts_dir / "conditional.md"
|
||||
conditional_prompt.write_text("""# Status Report
|
||||
|
||||
{% if error %}
|
||||
⚠️ ERROR: {{ error }}
|
||||
{% else %}
|
||||
✅ All systems operational
|
||||
{% endif %}
|
||||
|
||||
{% if items %}
|
||||
Items to process ({{ items | length }}):
|
||||
{% for item in items %}
|
||||
{{ loop.index }}. {{ item | upper }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
No items to process.
|
||||
{% endif %}
|
||||
|
||||
{% if debug %}
|
||||
Debug info: {{ debug_data | tojson }}
|
||||
{% endif %}""")
|
||||
|
||||
# Configure PromptManager
|
||||
PromptManager.configure(path=test_prompts_dir)
|
||||
|
||||
print("=" * 60)
|
||||
print("Testing Jinja2-Enhanced Prompt Management")
|
||||
print("=" * 60)
|
||||
|
||||
# Test 1: Simple variable replacement
|
||||
print("\n1. Simple variable replacement:")
|
||||
print("-" * 40)
|
||||
prompt = PromptManager.get_prompt("simple")
|
||||
filled = prompt.fill(name="Alice", age=30)
|
||||
print(filled)
|
||||
|
||||
# Test 2: List iteration
|
||||
print("\n2. List iteration:")
|
||||
print("-" * 40)
|
||||
prompt = PromptManager.get_prompt("list_example")
|
||||
filled = prompt.fill(tasks=["Write code", "Review PR", "Update documentation", "Deploy to staging"])
|
||||
print(filled)
|
||||
|
||||
# Test 3: Complex data structures
|
||||
print("\n3. Complex data structures:")
|
||||
print("-" * 40)
|
||||
prompt = PromptManager.get_prompt("complex")
|
||||
filled = prompt.fill(
|
||||
user={
|
||||
"name": "Bob Smith",
|
||||
"email": "bob@example.com",
|
||||
"tasks": [
|
||||
{"title": "Fix bug #123", "status": "✓", "priority": "High"},
|
||||
{"title": "Implement feature X", "status": "○", "priority": "Medium"},
|
||||
{"title": "Code review", "status": "→", "priority": "Low"}
|
||||
]
|
||||
},
|
||||
skills=["Python", "JavaScript", "Docker", "Kubernetes"],
|
||||
activities={
|
||||
"2024-01-15": "Deployed v2.3.0",
|
||||
"2024-01-14": "Fixed critical security issue",
|
||||
"2024-01-13": "Merged 5 PRs"
|
||||
}
|
||||
)
|
||||
print(filled)
|
||||
|
||||
# Test 4: Conditionals
|
||||
print("\n4. Conditionals (with items):")
|
||||
print("-" * 40)
|
||||
prompt = PromptManager.get_prompt("conditional")
|
||||
filled = prompt.fill(
|
||||
error=None, # Provide None for optional conditional variables
|
||||
items=["apple", "banana", "cherry"],
|
||||
debug=True,
|
||||
debug_data={"version": "1.0", "env": "dev"}
|
||||
)
|
||||
print(filled)
|
||||
|
||||
print("\n5. Conditionals (with error):")
|
||||
print("-" * 40)
|
||||
prompt = PromptManager.get_prompt("conditional")
|
||||
filled = prompt.fill(
|
||||
error="Connection timeout",
|
||||
items=[],
|
||||
debug=False,
|
||||
debug_data={}
|
||||
)
|
||||
print(filled)
|
||||
|
||||
# Test 6: Pre-filled prompt
|
||||
print("\n6. Pre-filled prompt on retrieval:")
|
||||
print("-" * 40)
|
||||
prompt = PromptManager.get_prompt("simple", name="Charlie", age=25)
|
||||
print(prompt.prompt) # Should already be filled
|
||||
|
||||
# Test 7: Variable extraction
|
||||
print("\n7. Variable extraction from complex template:")
|
||||
print("-" * 40)
|
||||
prompt = PromptManager.get_prompt("complex")
|
||||
print(f"Required variables: {prompt.variables}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All tests completed successfully!")
|
||||
print("=" * 60)
|
||||
|
||||
# Cleanup
|
||||
import shutil
|
||||
shutil.rmtree(test_prompts_dir)
|
||||
46
uv.lock
generated
46
uv.lock
generated
@@ -2,7 +2,53 @@ version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "llmutils"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "jinja2" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "jinja2", specifier = ">=3.0.0" }]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user