jinja2 support

This commit is contained in:
2025-09-16 16:05:01 +02:00
parent 2000370544
commit 5ce72ffb25
8 changed files with 647 additions and 36 deletions

118
README.md
View File

@@ -1,6 +1,6 @@
# LLMUtils # 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 ## Installation
@@ -18,6 +18,7 @@ pip install git+https://git.project-insanity.de/gmarth/LLMUtils.git
## Features ## 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 - **Smart Prompt Management**: Load and manage prompt templates with variable substitution
- **On-demand Loading**: Prompts are loaded lazily at runtime for better performance - **On-demand Loading**: Prompts are loaded lazily at runtime for better performance
- **Caching Support**: Optional caching to avoid repeated disk reads - **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) 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 ### JSON Schema Support
```python ```python
@@ -98,14 +130,59 @@ Place your prompt templates in the `prompts/` directory:
- `prompts/greeting.md` - Markdown file with template - `prompts/greeting.md` - Markdown file with template
- `prompts/greeting.json` - Optional JSON schema for structured output - `prompts/greeting.json` - Optional JSON schema for structured output
Example prompt template (`greeting.md`): ### Simple Template Example (`greeting.md`):
```markdown ```markdown
Hello {{name}}, Hello {{name}},
You are {{age}} years old. 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 ```json
{ {
"type": "object", "type": "object",
@@ -119,22 +196,22 @@ Example schema (`greeting.json`):
## API Reference ## 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 - `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 - `schema: Optional[Dict]` - Associated JSON schema
- `prompt: str` - Property that returns filled prompt or template - `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 - `validate(**kwargs) -> bool` - Check if all variables provided
- `get_missing_variables(**kwargs) -> Set[str]` - Get missing variables - `get_missing_variables(**kwargs) -> Set[str]` - Get missing variables
### PromptManager Methods ### 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 - `get_schema(prompt_name) -> Optional[Dict]` - Get just the schema
- `has_schema(prompt_name) -> bool` - Check if prompt has schema - `has_schema(prompt_name) -> bool` - Check if prompt has schema
- `list_prompts() -> Dict` - List all available prompts - `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 - `configure(path=None, caching=None)` - Configure settings
- `reload_prompts()` - Clear the cache - `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 ## License
MIT MIT

221
example_jinja2.py Normal file
View 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)

View 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
View 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 %}

View File

@@ -8,7 +8,7 @@ version = "0.1.0"
description = "Utilities for working with LLMs" description = "Utilities for working with LLMs"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [] dependencies = ["jinja2>=3.0.0"]
authors = [ authors = [
{name = "Alexander Thiess", email = "thiess.alexander@googlemail.com"} {name = "Alexander Thiess", email = "thiess.alexander@googlemail.com"}
] ]

View File

@@ -1,10 +1,11 @@
import os import os
import re import re
import json import json
from dataclasses import dataclass from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Dict, Any, Set, Optional from typing import Dict, Any, Set, Optional
import logging import logging
from jinja2 import Template, Environment, meta, TemplateSyntaxError, UndefinedError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -16,8 +17,9 @@ class ManagedPrompt:
name: str name: str
variables: Set[str] variables: Set[str]
schema: Optional[Dict[str, Any]] = None schema: Optional[Dict[str, Any]] = None
_filled_prompt: Optional[str] = None _filled_prompt: Optional[str] = field(default=None, init=False, repr=False)
_context: Optional[Dict[str, Any]] = None _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: def validate(self, **kwargs) -> bool:
"""Validate that all required variables are provided. """Validate that all required variables are provided.
@@ -39,7 +41,7 @@ class ManagedPrompt:
return self.variables - provided_vars return self.variables - provided_vars
def fill(self, **kwargs) -> str: def fill(self, **kwargs) -> str:
"""Fill the template with provided variables. """Fill the template with provided variables using Jinja2.
Args: Args:
**kwargs: Variables to fill in the template **kwargs: Variables to fill in the template
@@ -48,8 +50,15 @@ class ManagedPrompt:
The filled prompt string The filled prompt string
Raises: 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 no variables required and none provided, return template as-is
if not self.variables and not kwargs: if not self.variables and not kwargs:
self._filled_prompt = self.template self._filled_prompt = self.template
@@ -63,15 +72,11 @@ class ManagedPrompt:
f"Required: {self.variables}, Provided: {set(kwargs.keys())}" f"Required: {self.variables}, Provided: {set(kwargs.keys())}"
) )
# Only process the template if there are actually variables to replace try:
if self.variables: # Render the template with Jinja2
result = self.template result = self._jinja_template.render(**kwargs)
for key, value in kwargs.items(): except UndefinedError as e:
if key in self.variables: # Only replace known variables raise ValueError(f"Error rendering template '{self.name}': {e}")
placeholder = f"{{{{{key}}}}}" # {{key}}
result = result.replace(placeholder, str(value))
else:
result = self.template
# Cache the filled result # Cache the filled result
self._filled_prompt = result self._filled_prompt = result
@@ -182,8 +187,17 @@ class PromptManager:
return Path.cwd() / 'prompts' return Path.cwd() / 'prompts'
def _extract_variables(self, template: str) -> Set[str]: def _extract_variables(self, template: str) -> Set[str]:
"""Extract all {{variable}} placeholders from template""" """Extract all variables from Jinja2 template"""
pattern = r'\{\{(\w+)\}\}' 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)) variables = set(re.findall(pattern, template))
return variables return variables
@@ -208,14 +222,12 @@ class PromptManager:
logger.warning(f"Extra variables provided for prompt '{prompt_name}': {extra_vars}") logger.warning(f"Extra variables provided for prompt '{prompt_name}': {extra_vars}")
def _fill_template(self, template: str, context: Dict[str, Any]) -> str: def _fill_template(self, template: str, context: Dict[str, Any]) -> str:
"""Fill template with context variables""" """Fill template with context variables using Jinja2"""
result = template try:
jinja_template = Template(template)
for key, value in context.items(): return jinja_template.render(**context)
placeholder = f"{{{{{key}}}}}" # {{key}} except (TemplateSyntaxError, UndefinedError) as e:
result = result.replace(placeholder, str(value)) raise ValueError(f"Error rendering template: {e}")
return result
@classmethod @classmethod
def configure(cls, path: Optional[Path] = None, caching: Optional[bool] = None): def configure(cls, path: Optional[Path] = None, caching: Optional[bool] = None):

156
test_jinja2_prompts.py Normal file
View 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
View File

@@ -2,7 +2,53 @@ version = 1
revision = 3 revision = 3
requires-python = ">=3.13" 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]] [[package]]
name = "llmutils" name = "llmutils"
version = "0.1.0" version = "0.1.0"
source = { editable = "." } 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" },
]