mirror of
https://github.com/crewAIInc/crewAI.git
synced 2025-12-18 05:18:31 +00:00
Compare commits
5 Commits
devin/1762
...
devin/1748
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6463715567 | ||
|
|
940bf2aa5d | ||
|
|
1bf2e760ab | ||
|
|
80b48208d5 | ||
|
|
acd5aadfd1 |
@@ -68,7 +68,7 @@ def to_serializable(
|
|||||||
_current_depth=_current_depth + 1,
|
_current_depth=_current_depth + 1,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return repr(obj)
|
return str(obj)
|
||||||
|
|
||||||
|
|
||||||
def _to_serializable_key(key: Any) -> str:
|
def _to_serializable_key(key: Any) -> str:
|
||||||
|
|||||||
@@ -1,10 +1,38 @@
|
|||||||
import re
|
import re
|
||||||
from typing import Any, Dict, List, Optional, Union
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
SUPPORTED_PRIMITIVE_TYPES = (str, int, float, bool)
|
||||||
|
SUPPORTED_CONTAINER_TYPES = (dict, list)
|
||||||
|
SUPPORTED_TYPES = SUPPORTED_PRIMITIVE_TYPES + SUPPORTED_CONTAINER_TYPES
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_input_type(val: Any) -> None:
|
||||||
|
"""Validates input types recursively (str, int, float, bool, dict, list).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
val: The value to validate
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the value contains unsupported types
|
||||||
|
"""
|
||||||
|
if val is None:
|
||||||
|
return
|
||||||
|
if isinstance(val, SUPPORTED_PRIMITIVE_TYPES):
|
||||||
|
return
|
||||||
|
if isinstance(val, SUPPORTED_CONTAINER_TYPES):
|
||||||
|
for item in val.values() if isinstance(val, dict) else val:
|
||||||
|
_validate_input_type(item)
|
||||||
|
return
|
||||||
|
raise ValueError(
|
||||||
|
f"Unsupported type {type(val).__name__} in inputs. "
|
||||||
|
"Only str, int, float, bool, dict, and list are allowed."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def interpolate_only(
|
def interpolate_only(
|
||||||
input_string: Optional[str],
|
input_string: Optional[str],
|
||||||
inputs: Dict[str, Union[str, int, float, Dict[str, Any], List[Any]]],
|
inputs: Dict[str, Any],
|
||||||
|
raise_on_missing: bool = True,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Interpolate placeholders (e.g., {key}) in a string while leaving JSON untouched.
|
"""Interpolate placeholders (e.g., {key}) in a string while leaving JSON untouched.
|
||||||
Only interpolates placeholders that follow the pattern {variable_name} where
|
Only interpolates placeholders that follow the pattern {variable_name} where
|
||||||
@@ -25,27 +53,27 @@ def interpolate_only(
|
|||||||
ValueError: If a value contains unsupported types or a template variable is missing
|
ValueError: If a value contains unsupported types or a template variable is missing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Validation function for recursive type checking
|
from crewai.utilities.serialization import to_serializable
|
||||||
def validate_type(value: Any) -> None:
|
|
||||||
if value is None:
|
processed_inputs = {}
|
||||||
return
|
|
||||||
if isinstance(value, (str, int, float, bool)):
|
|
||||||
return
|
|
||||||
if isinstance(value, (dict, list)):
|
|
||||||
for item in value.values() if isinstance(value, dict) else value:
|
|
||||||
validate_type(item)
|
|
||||||
return
|
|
||||||
raise ValueError(
|
|
||||||
f"Unsupported type {type(value).__name__} in inputs. "
|
|
||||||
"Only str, int, float, bool, dict, and list are allowed."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate all input values
|
|
||||||
for key, value in inputs.items():
|
for key, value in inputs.items():
|
||||||
try:
|
if value is None or isinstance(value, SUPPORTED_TYPES):
|
||||||
validate_type(value)
|
try:
|
||||||
except ValueError as e:
|
_validate_input_type(value)
|
||||||
raise ValueError(f"Invalid value for key '{key}': {str(e)}") from e
|
processed_inputs[key] = value
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError(f"Invalid value for key '{key}': {str(e)}") from e
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
processed_inputs[key] = to_serializable(value)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid value for key '{key}': Unable to serialize {type(value).__name__}. "
|
||||||
|
f"Serialization error: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
inputs = processed_inputs
|
||||||
|
|
||||||
if input_string is None or not input_string:
|
if input_string is None or not input_string:
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
203
tests/cassettes/test_crew_kickoff_with_pandas_dataframe.yaml
Normal file
203
tests/cassettes/test_crew_kickoff_with_pandas_dataframe.yaml
Normal file
File diff suppressed because one or more lines are too long
@@ -4566,3 +4566,96 @@ def test_reset_agent_knowledge_with_only_agent_knowledge(researcher,writer):
|
|||||||
mock_reset_agent_knowledge.assert_called_once_with([mock_ks_research,mock_ks_writer])
|
mock_reset_agent_knowledge.assert_called_once_with([mock_ks_research,mock_ks_writer])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
|
def test_crew_kickoff_with_pandas_dataframe():
|
||||||
|
"""Test that crew.kickoff works with pandas DataFrame inputs."""
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
df = pd.DataFrame({
|
||||||
|
"name": ["Alice", "Bob", "Charlie"],
|
||||||
|
"age": [25, 30, 35],
|
||||||
|
"city": ["New York", "London", "Tokyo"]
|
||||||
|
})
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
role="Data Analyst",
|
||||||
|
goal="Analyze the provided data",
|
||||||
|
backstory="You are an expert data analyst",
|
||||||
|
)
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description="Analyze this dataset: {data}",
|
||||||
|
expected_output="A brief summary of the data",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
crew = Crew(agents=[agent], tasks=[task])
|
||||||
|
|
||||||
|
result = crew.kickoff(inputs={"data": df})
|
||||||
|
assert result is not None
|
||||||
|
assert "Alice" in str(result) or "Bob" in str(result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_crew_inputs_interpolate_with_dataframe():
|
||||||
|
"""Test that input interpolation works with pandas DataFrames."""
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
df = pd.DataFrame({"col1": [1, 2], "col2": [3, 4]})
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
role="Analyst",
|
||||||
|
goal="Process {data_type} data",
|
||||||
|
backstory="Expert in {data_type} analysis",
|
||||||
|
)
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description="Process this data: {dataset}",
|
||||||
|
expected_output="Analysis of {dataset}",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
crew = Crew(agents=[agent], tasks=[task])
|
||||||
|
inputs = {"data_type": "tabular", "dataset": df}
|
||||||
|
|
||||||
|
crew._interpolate_inputs(inputs=inputs)
|
||||||
|
|
||||||
|
assert "tabular" in crew.agents[0].goal
|
||||||
|
assert "tabular" in crew.agents[0].backstory
|
||||||
|
assert str(df) in crew.tasks[0].description
|
||||||
|
assert str(df) in crew.tasks[0].expected_output
|
||||||
|
|
||||||
|
|
||||||
|
def test_crew_inputs_interpolate_mixed_types_with_dataframe():
|
||||||
|
"""Test input interpolation with mixed types including DataFrames."""
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
df = pd.DataFrame({"values": [10, 20, 30]})
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
role="{role_name}",
|
||||||
|
goal="Analyze {count} records",
|
||||||
|
backstory="Expert with {dataset}",
|
||||||
|
)
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description="Process {dataset} with {count} records",
|
||||||
|
expected_output="{count} insights from {dataset}",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
crew = Crew(agents=[agent], tasks=[task])
|
||||||
|
inputs = {
|
||||||
|
"role_name": "Data Scientist",
|
||||||
|
"count": 3,
|
||||||
|
"dataset": df
|
||||||
|
}
|
||||||
|
|
||||||
|
crew._interpolate_inputs(inputs=inputs)
|
||||||
|
|
||||||
|
assert crew.agents[0].role == "Data Scientist"
|
||||||
|
assert "3" in crew.agents[0].goal
|
||||||
|
assert str(df) in crew.agents[0].backstory
|
||||||
|
assert str(df) in crew.tasks[0].description
|
||||||
|
assert "3" in crew.tasks[0].expected_output
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1279,54 +1279,40 @@ def test_interpolate_complex_combination():
|
|||||||
|
|
||||||
|
|
||||||
def test_interpolate_invalid_type_validation():
|
def test_interpolate_invalid_type_validation():
|
||||||
# Test with invalid top-level type
|
# Test with type that fails serialization
|
||||||
with pytest.raises(ValueError) as excinfo:
|
class UnserializableObject:
|
||||||
interpolate_only("{data}", {"data": set()}) # type: ignore we are purposely testing this failure
|
def __str__(self):
|
||||||
|
raise Exception("Cannot serialize")
|
||||||
assert "Unsupported type set" in str(excinfo.value)
|
def __repr__(self):
|
||||||
|
raise Exception("Cannot serialize")
|
||||||
# Test with invalid nested type
|
|
||||||
invalid_nested = {
|
with pytest.raises(ValueError, match="Unable to serialize UnserializableObject"):
|
||||||
"profile": {
|
interpolate_only("{data}", {"data": UnserializableObject()})
|
||||||
"name": "John",
|
|
||||||
"age": 30,
|
result = interpolate_only("{data}", {"data": {1, 2, 3}})
|
||||||
"tags": {"a", "b", "c"}, # Set is invalid
|
assert "1" in result and "2" in result and "3" in result
|
||||||
}
|
|
||||||
}
|
|
||||||
with pytest.raises(ValueError) as excinfo:
|
|
||||||
interpolate_only("{data}", {"data": invalid_nested})
|
|
||||||
assert "Unsupported type set" in str(excinfo.value)
|
|
||||||
|
|
||||||
|
|
||||||
def test_interpolate_custom_object_validation():
|
def test_interpolate_custom_object_validation():
|
||||||
class CustomObject:
|
class SerializableCustomObject:
|
||||||
def __init__(self, value):
|
def __init__(self, value):
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.value)
|
return str(self.value)
|
||||||
|
|
||||||
# Test with custom object at top level
|
class UnserializableCustomObject:
|
||||||
with pytest.raises(ValueError) as excinfo:
|
def __init__(self, value):
|
||||||
interpolate_only("{obj}", {"obj": CustomObject(5)}) # type: ignore we are purposely testing this failure
|
self.value = value
|
||||||
assert "Unsupported type CustomObject" in str(excinfo.value)
|
def __str__(self):
|
||||||
|
raise Exception("Cannot serialize")
|
||||||
# Test with nested custom object in dictionary
|
def __repr__(self):
|
||||||
with pytest.raises(ValueError) as excinfo:
|
raise Exception("Cannot serialize")
|
||||||
interpolate_only("{data}", {"data": {"valid": 1, "invalid": CustomObject(5)}})
|
|
||||||
assert "Unsupported type CustomObject" in str(excinfo.value)
|
result = interpolate_only("{obj}", {"obj": SerializableCustomObject(5)})
|
||||||
|
assert "5" in result
|
||||||
# Test with nested custom object in list
|
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(ValueError, match="Unable to serialize UnserializableCustomObject"):
|
||||||
interpolate_only("{data}", {"data": [1, "valid", CustomObject(5)]})
|
interpolate_only("{obj}", {"obj": UnserializableCustomObject(5)})
|
||||||
assert "Unsupported type CustomObject" in str(excinfo.value)
|
|
||||||
|
|
||||||
# Test with deeply nested custom object
|
|
||||||
with pytest.raises(ValueError) as excinfo:
|
|
||||||
interpolate_only(
|
|
||||||
"{data}", {"data": {"level1": {"level2": [{"level3": CustomObject(5)}]}}}
|
|
||||||
)
|
|
||||||
assert "Unsupported type CustomObject" in str(excinfo.value)
|
|
||||||
|
|
||||||
|
|
||||||
def test_interpolate_valid_complex_types():
|
def test_interpolate_valid_complex_types():
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from typing import Any, Dict, List, Union
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
from crewai.utilities.string_utils import interpolate_only
|
from crewai.utilities.string_utils import interpolate_only
|
||||||
|
|
||||||
@@ -90,16 +91,19 @@ class TestInterpolateOnly:
|
|||||||
assert "name" in str(excinfo.value)
|
assert "name" in str(excinfo.value)
|
||||||
|
|
||||||
def test_invalid_input_types(self):
|
def test_invalid_input_types(self):
|
||||||
"""Test that an error is raised with invalid input types."""
|
"""Test that an error is raised when serialization fails."""
|
||||||
|
class UnserializableObject:
|
||||||
|
def __str__(self):
|
||||||
|
raise Exception("Cannot convert to string")
|
||||||
|
def __repr__(self):
|
||||||
|
raise Exception("Cannot convert to string")
|
||||||
|
|
||||||
template = "Hello, {name}!"
|
template = "Hello, {name}!"
|
||||||
# Using Any for this test since we're intentionally testing an invalid type
|
inputs: Dict[str, Any] = {"name": UnserializableObject()}
|
||||||
inputs: Dict[str, Any] = {"name": object()} # Object is not a valid input type
|
|
||||||
|
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(ValueError, match="Unable to serialize UnserializableObject"):
|
||||||
interpolate_only(template, inputs)
|
interpolate_only(template, inputs)
|
||||||
|
|
||||||
assert "unsupported type" in str(excinfo.value).lower()
|
|
||||||
|
|
||||||
def test_empty_input_string(self):
|
def test_empty_input_string(self):
|
||||||
"""Test handling of empty or None input string."""
|
"""Test handling of empty or None input string."""
|
||||||
inputs: Dict[str, Union[str, int, float, Dict[str, Any], List[Any]]] = {
|
inputs: Dict[str, Union[str, int, float, Dict[str, Any], List[Any]]] = {
|
||||||
@@ -181,7 +185,86 @@ class TestInterpolateOnly:
|
|||||||
template = "Hello, {name}!"
|
template = "Hello, {name}!"
|
||||||
inputs: Dict[str, Any] = {}
|
inputs: Dict[str, Any] = {}
|
||||||
|
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(ValueError):
|
||||||
interpolate_only(template, inputs)
|
interpolate_only(template, inputs)
|
||||||
|
|
||||||
assert "inputs dictionary cannot be empty" in str(excinfo.value).lower()
|
|
||||||
|
def test_interpolate_only_with_dataframe(self):
|
||||||
|
"""Test that interpolate_only handles pandas DataFrames correctly."""
|
||||||
|
df = pd.DataFrame({"name": ["Alice", "Bob"], "age": [25, 30]})
|
||||||
|
|
||||||
|
result = interpolate_only("Data: {data}", {"data": df})
|
||||||
|
|
||||||
|
assert "Alice" in result
|
||||||
|
assert "Bob" in result
|
||||||
|
assert "25" in result
|
||||||
|
assert "30" in result
|
||||||
|
|
||||||
|
def test_interpolate_only_mixed_types_with_dataframe(self):
|
||||||
|
"""Test interpolate_only with mixed input types including DataFrame."""
|
||||||
|
df = pd.DataFrame({"col": [1, 2, 3]})
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
"text": "hello",
|
||||||
|
"number": 42,
|
||||||
|
"flag": True,
|
||||||
|
"data": df,
|
||||||
|
"items": [1, 2, 3]
|
||||||
|
}
|
||||||
|
|
||||||
|
template = "Text: {text}, Number: {number}, Flag: {flag}, Data: {data}, Items: {items}"
|
||||||
|
result = interpolate_only(template, inputs)
|
||||||
|
|
||||||
|
assert "hello" in result
|
||||||
|
assert "42" in result
|
||||||
|
assert "True" in result
|
||||||
|
assert "col" in result
|
||||||
|
assert "[1, 2, 3]" in result
|
||||||
|
|
||||||
|
def test_interpolate_only_unsupported_type_error(self):
|
||||||
|
"""Test that interpolate_only handles unsupported types gracefully."""
|
||||||
|
class CustomObject:
|
||||||
|
def __str__(self):
|
||||||
|
raise Exception("Cannot serialize")
|
||||||
|
def __repr__(self):
|
||||||
|
raise Exception("Cannot serialize")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Unable to serialize CustomObject"):
|
||||||
|
interpolate_only("Value: {obj}", {"obj": CustomObject()})
|
||||||
|
|
||||||
|
def test_interpolate_only_complex_dataframe(self):
|
||||||
|
"""Test interpolate_only with more complex DataFrame structures."""
|
||||||
|
df = pd.DataFrame({
|
||||||
|
"product": ["Widget A", "Widget B", "Widget C"],
|
||||||
|
"sales": [100, 150, 200],
|
||||||
|
"region": ["North", "South", "East"]
|
||||||
|
})
|
||||||
|
|
||||||
|
result = interpolate_only("Sales report: {report}", {"report": df})
|
||||||
|
|
||||||
|
assert "Widget A" in result
|
||||||
|
assert "100" in result
|
||||||
|
assert "North" in result
|
||||||
|
assert "sales" in result
|
||||||
|
assert "product" in result
|
||||||
|
|
||||||
|
def test_interpolate_only_backward_compatibility(self):
|
||||||
|
"""Test that existing supported types still work correctly."""
|
||||||
|
inputs = {
|
||||||
|
"text": "hello",
|
||||||
|
"number": 42,
|
||||||
|
"float_val": 3.14,
|
||||||
|
"flag": True,
|
||||||
|
"nested": {"key": "value"},
|
||||||
|
"items": [1, 2, 3]
|
||||||
|
}
|
||||||
|
|
||||||
|
template = "Text: {text}, Number: {number}, Float: {float_val}, Flag: {flag}, Nested: {nested}, Items: {items}"
|
||||||
|
result = interpolate_only(template, inputs)
|
||||||
|
|
||||||
|
assert "hello" in result
|
||||||
|
assert "42" in result
|
||||||
|
assert "3.14" in result
|
||||||
|
assert "True" in result
|
||||||
|
assert "key" in result
|
||||||
|
assert "[1, 2, 3]" in result
|
||||||
|
|||||||
Reference in New Issue
Block a user