[FEATURE] Improve agent/task templating with Jinja2

Fixes #2650

- Add support for container types (List, Dict, Set)
- Add support for standard objects (datetime)
- Add support for custom objects
- Add support for conditional and loop statements
- Add support for filtering options
- Maintain backward compatibility with existing templates
- Add comprehensive tests
- Add documentation with examples

Co-Authored-By: Joe Moura <joao@crewai.com>
This commit is contained in:
Devin AI
2025-04-20 14:36:46 +00:00
parent 311a078ca6
commit 15dd15fcab
7 changed files with 601 additions and 48 deletions

View File

@@ -0,0 +1,81 @@
import pytest
import datetime
from pydantic import BaseModel
from typing import List, Dict, Any
from crewai.utilities.jinja_templating import to_jinja_template, render_template
class Person(BaseModel):
name: str
age: int
def __str__(self):
return f"{self.name} ({self.age})"
class TestJinjaTemplating:
def test_to_jinja_template(self):
assert to_jinja_template("Hello {name}!") == "Hello {{name}}!"
assert to_jinja_template("Hello {{name}}!") == "Hello {{name}}!"
assert to_jinja_template("Hello {name} and {{title}}!") == "Hello {{name}} and {{title}}!"
assert to_jinja_template("") == ""
assert to_jinja_template("Hello world!") == "Hello world!"
def test_render_template_simple_types(self):
inputs = {"name": "John", "age": 30, "active": True, "height": 1.85}
assert render_template("Hello {name}!", inputs) == "Hello John!"
assert render_template("Age: {age}", inputs) == "Age: 30"
assert render_template("Active: {active}", inputs) == "Active: True"
assert render_template("Height: {height}", inputs) == "Height: 1.85"
assert render_template("{name} is {age} years old", inputs) == "John is 30 years old"
def test_render_template_container_types(self):
inputs = {
"items": ["apple", "banana", "orange"],
"person": {"name": "John", "age": 30}
}
assert render_template("First item: {{items[0]}}", inputs) == "First item: apple"
assert render_template("Person name: {{person.name}}", inputs) == "Person name: John"
assert render_template(
"Items: {% for item in items %}{{item}}{% if not loop.last %}, {% endif %}{% endfor %}",
inputs
) == "Items: apple, banana, orange"
assert render_template(
"{% if items|length > 2 %}Many items{% else %}Few items{% endif %}",
inputs
) == "Many items"
def test_render_template_datetime(self):
today = datetime.datetime.now()
inputs = {"today": today}
assert render_template("Today: {{today|date}}", inputs) == f"Today: {today.strftime('%Y-%m-%d')}"
assert render_template("Today: {{today|date('%d/%m/%Y')}}", inputs) == f"Today: {today.strftime('%d/%m/%Y')}"
def test_render_template_custom_objects(self):
person = Person(name="John", age=30)
inputs = {"person": person}
assert render_template("Person: {person}", inputs) == "Person: John (30)"
assert render_template("Person name: {{person.name}}", inputs) == "Person name: John"
def test_render_template_error_handling(self):
inputs = {"name": "John"}
with pytest.raises(KeyError) as excinfo:
render_template("Hello {age}!", inputs)
assert "Template variable 'age' not found" in str(excinfo.value)
with pytest.raises(ValueError) as excinfo:
render_template("Hello {name}!", {})
assert "Inputs dictionary cannot be empty" in str(excinfo.value)

View File

@@ -1,6 +1,7 @@
from typing import Any, Dict, List, Union
import datetime
import pytest
from pydantic import BaseModel
from crewai.utilities.string_utils import interpolate_only
@@ -185,3 +186,96 @@ class TestInterpolateOnly:
interpolate_only(template, inputs)
assert "inputs dictionary cannot be empty" in str(excinfo.value).lower()
def test_container_types_list_access(self):
"""Test accessing list items with Jinja2 syntax."""
template = "First item: {{items[0]}}, Second item: {{items[1]}}"
inputs = {
"items": ["apple", "banana", "orange"]
}
result = interpolate_only(template, inputs)
assert result == "First item: apple, Second item: banana"
def test_container_types_dict_access(self):
"""Test accessing dictionary items with Jinja2 syntax."""
template = "Name: {{person.name}}, Age: {{person.age}}"
inputs = {
"person": {"name": "John", "age": 30}
}
result = interpolate_only(template, inputs)
assert result == "Name: John, Age: 30"
def test_conditional_statements(self):
"""Test conditional statements with Jinja2 syntax."""
template = "{% if priority == 'high' %}URGENT: {% endif %}Task: {task}"
inputs_high = {
"task": "Fix bug",
"priority": "high"
}
result_high = interpolate_only(template, inputs_high)
assert result_high == "URGENT: Task: Fix bug"
inputs_low = {
"task": "Fix bug",
"priority": "low"
}
result_low = interpolate_only(template, inputs_low)
assert result_low == "Task: Fix bug"
def test_loop_statements(self):
"""Test loop statements with Jinja2 syntax."""
template = "Items: {% for item in items %}{{item}}{% if not loop.last %}, {% endif %}{% endfor %}"
inputs = {
"items": ["apple", "banana", "orange"]
}
result = interpolate_only(template, inputs)
assert result == "Items: apple, banana, orange"
def test_datetime_formatting(self):
"""Test datetime formatting with Jinja2 filters."""
today = datetime.datetime(2024, 4, 20)
inputs = {"today": today}
template = "Date: {{today|date}}"
result = interpolate_only(template, inputs)
assert result == "Date: 2024-04-20"
template = "Date: {{today|date('%d/%m/%Y')}}"
result = interpolate_only(template, inputs)
assert result == "Date: 20/04/2024"
def test_custom_objects(self):
"""Test custom objects with Jinja2 syntax."""
class Person(BaseModel):
name: str
age: int
def __str__(self):
return f"{self.name} ({self.age})"
person = Person(name="John", age=30)
inputs = {"person": person}
template = "Person: {person}"
result = interpolate_only(template, inputs)
assert result == "Person: John (30)"
template = "Name: {{person.name}}, Age: {{person.age}}"
result = interpolate_only(template, inputs)
assert result == "Name: John, Age: 30"
def test_mixed_syntax(self):
"""Test mixed CrewAI and Jinja2 syntax."""
template = "Hello {name}! Items: {% for item in items %}{{item}}{% if not loop.last %}, {% endif %}{% endfor %}"
inputs = {
"name": "John",
"items": ["apple", "banana", "orange"]
}
result = interpolate_only(template, inputs)
assert result == "Hello John! Items: apple, banana, orange"