Compare commits

..

11 Commits

Author SHA1 Message Date
Lucas Gomide
a2224bbe18 Merge branch 'main' into bugfix-python-3-10 2025-04-10 14:11:16 -03:00
Vini Brasil
37979a0ca1 Raise exception when flow fails (#2579) 2025-04-10 13:08:32 -04:00
Lorenze Jay
d96543d314 Merge branch 'main' into bugfix-python-3-10 2025-04-10 09:47:12 -07:00
devin-ai-integration[bot]
c9f47e6a37 Add result_as_answer parameter to @tool decorator (Fixes #2561) (#2562)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Joe Moura <joao@crewai.com>
2025-04-10 09:01:26 -04:00
x1x2
5780c3147a fix: correct parameter name in crew template test function (#2567)
This commit resolves an issue in the crew template generator where the test() 
function incorrectly uses 'openai_model_name' as a parameter name when calling 
Crew.test(), while the actual implementation expects 'eval_llm'.

The mismatch causes a TypeError when users run the generated test command:
"Crew.test() got an unexpected keyword argument 'openai_model_name'"

This change ensures that templates generated with 'crewai create crew' will 
produce code that aligns with the framework's API.
2025-04-10 08:51:10 -04:00
Lucas Gomide
52e10d6c84 Merge branch 'main' into bugfix-python-3-10 2025-04-10 09:27:37 -03:00
Lorenze Jay
f18a112cd7 Merge branch 'main' into bugfix-python-3-10 2025-04-09 08:35:27 -07:00
Lucas Gomide
40dcdb43d6 Merge branch 'main' into bugfix-python-3-10 2025-04-09 11:58:16 -03:00
Lucas Gomide
1167fbdd8c chore: rename external_memory file test 2025-04-09 11:19:07 -03:00
Lucas Gomide
d200d00bb5 refactor: remove explicit Self import from typing
Python 3.10+ natively supports Self type annotation without explicit imports
2025-04-09 11:13:01 -03:00
Lucas Gomide
bf55dde358 ci(workflows): add Python version matrix (3.10-3.12) for tests 2025-04-09 11:13:01 -03:00
11 changed files with 55 additions and 253 deletions

View File

@@ -12,6 +12,9 @@ jobs:
tests:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -21,9 +24,8 @@ jobs:
with:
enable-cache: true
- name: Set up Python
run: uv python install 3.12.8
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install the project
run: uv sync --dev --all-extras

View File

@@ -60,7 +60,7 @@ def test():
"current_year": str(datetime.now().year)
}
try:
{{crew_name}}().crew().test(n_iterations=int(sys.argv[1]), openai_model_name=sys.argv[2], inputs=inputs)
{{crew_name}}().crew().test(n_iterations=int(sys.argv[1]), eval_llm=sys.argv[2], inputs=inputs)
except Exception as e:
raise Exception(f"An error occurred while testing the crew: {e}")

View File

@@ -1043,6 +1043,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
import traceback
traceback.print_exc()
raise
def _log_flow_event(
self, message: str, color: str = "yellow", level: str = "info"

View File

@@ -819,8 +819,8 @@ class LLM(BaseLLM):
)
def _format_messages_for_provider(
self, messages: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
self, messages: List[Dict[str, str]]
) -> List[Dict[str, str]]:
"""Format messages according to provider requirements.
Args:
@@ -830,7 +830,6 @@ class LLM(BaseLLM):
Returns:
List of formatted messages according to provider requirements.
For Anthropic models, ensures first message has 'user' role.
For multimodal content, ensures proper formatting of image URLs.
Raises:
TypeError: If messages is None or contains invalid message format.
@@ -845,71 +844,40 @@ class LLM(BaseLLM):
"Invalid message format. Each message must be a dict with 'role' and 'content' keys"
)
formatted_messages = []
for msg in messages:
if isinstance(msg["content"], list):
formatted_content = []
for item in msg["content"]:
if isinstance(item, dict):
if item.get("type") == "text":
formatted_content.append({
"type": "text",
"text": item["text"]
})
elif item.get("type") == "image_url":
formatted_content.append({
"type": "image_url",
"image_url": item["image_url"]
})
else:
formatted_content.append(item)
else:
formatted_content.append({
"type": "text",
"text": str(item)
})
formatted_messages.append({
"role": msg["role"],
"content": formatted_content
})
else:
formatted_messages.append(msg)
# Handle O1 models specially
if "o1" in self.model.lower():
result = []
for msg in formatted_messages:
formatted_messages = []
for msg in messages:
# Convert system messages to assistant messages
if msg["role"] == "system":
result.append(
formatted_messages.append(
{"role": "assistant", "content": msg["content"]}
)
else:
result.append(msg)
return result
formatted_messages.append(msg)
return formatted_messages
# Handle Mistral models - they require the last message to have a role of 'user' or 'tool'
if "mistral" in self.model.lower():
# Check if the last message has a role of 'assistant'
if formatted_messages and formatted_messages[-1]["role"] == "assistant":
if messages and messages[-1]["role"] == "assistant":
# Add a dummy user message to ensure the last message has a role of 'user'
formatted_messages = (
formatted_messages.copy()
messages = (
messages.copy()
) # Create a copy to avoid modifying the original
formatted_messages.append({"role": "user", "content": "Please continue."})
return formatted_messages
messages.append({"role": "user", "content": "Please continue."})
return messages
# Handle Anthropic models
if not self.is_anthropic:
return formatted_messages
return messages
# Anthropic requires messages to start with 'user' role
if not formatted_messages or formatted_messages[0]["role"] == "system":
if not messages or messages[0]["role"] == "system":
# If first message is system or empty, add a placeholder user message
return [{"role": "user", "content": "."}, *formatted_messages]
return [{"role": "user", "content": "."}, *messages]
return formatted_messages
return messages
def _get_custom_llm_provider(self) -> Optional[str]:
"""

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Any, Dict, Optional, Self
from typing import TYPE_CHECKING, Any, Dict, Optional
from crewai.memory.external.external_memory_item import ExternalMemoryItem
from crewai.memory.memory import Memory
@@ -52,7 +52,7 @@ class ExternalMemory(Memory):
def reset(self) -> None:
self.storage.reset()
def set_crew(self, crew: Any) -> Self:
def set_crew(self, crew: Any) -> "ExternalMemory":
super().set_crew(crew)
if not self.storage:

View File

@@ -1,4 +1,4 @@
from typing import Any, Dict, List, Optional, Self
from typing import Any, Dict, List, Optional
from pydantic import BaseModel
@@ -38,6 +38,6 @@ class Memory(BaseModel):
query=query, limit=limit, score_threshold=score_threshold
)
def set_crew(self, crew: Any) -> Self:
def set_crew(self, crew: Any) -> "Memory":
self.crew = crew
return self

View File

@@ -244,9 +244,13 @@ def to_langchain(
return [t.to_structured_tool() if isinstance(t, BaseTool) else t for t in tools]
def tool(*args):
def tool(*args, result_as_answer=False):
"""
Decorator to create a tool from a function.
Args:
*args: Positional arguments, either the function to decorate or the tool name.
result_as_answer: Flag to indicate if the tool result should be used as the final agent answer.
"""
def _make_with_name(tool_name: str) -> Callable:
@@ -272,6 +276,7 @@ def tool(*args):
description=f.__doc__,
func=f,
args_schema=args_schema,
result_as_answer=result_as_answer,
)
return _make_tool

File diff suppressed because one or more lines are too long

View File

@@ -3820,88 +3820,6 @@ def test_multimodal_agent_live_image_analysis():
assert "error" not in result.raw.lower() # No error messages in response
def test_format_messages_for_provider_with_multimodal_content():
"""
Test that the _format_messages_for_provider method correctly formats multimodal content.
This specifically tests that image URLs are formatted as structured multimodal content
rather than plain text.
"""
llm = LLM(model="gpt-4o")
messages = [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Analyze this image:"
},
{
"type": "image_url",
"image_url": "https://example.com/test-image.jpg"
}
]
}
]
formatted_messages = llm._format_messages_for_provider(messages)
assert len(formatted_messages) == 1
assert formatted_messages[0]["role"] == "user"
assert isinstance(formatted_messages[0]["content"], list)
content = formatted_messages[0]["content"]
assert len(content) == 2
assert content[0]["type"] == "text"
assert content[0]["text"] == "Analyze this image:"
assert content[1]["type"] == "image_url"
assert "image_url" in content[1]
messages_with_string_url = [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Analyze this image:"
},
{
"type": "image_url",
"image_url": "https://example.com/test-image.jpg"
}
]
}
]
formatted_messages_string_url = llm._format_messages_for_provider(messages_with_string_url)
assert formatted_messages_string_url[0]["content"][1]["type"] == "image_url"
assert "image_url" in formatted_messages_string_url[0]["content"][1]
messages_with_dict_url = [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Analyze this image:"
},
{
"type": "image_url",
"image_url": {
"url": "https://example.com/test-image.jpg"
}
}
]
}
]
formatted_messages_dict_url = llm._format_messages_for_provider(messages_with_dict_url)
assert formatted_messages_dict_url[0]["content"][1]["type"] == "image_url"
assert "image_url" in formatted_messages_dict_url[0]["content"][1]
@pytest.mark.vcr(filter_headers=["authorization"])
def test_crew_with_failing_task_guardrails():
"""Test that crew properly handles failing guardrails and retries with validation feedback."""

View File

@@ -100,3 +100,25 @@ def test_default_cache_function_is_true():
my_tool = MyCustomTool()
# Assert all the right attributes were defined
assert my_tool.cache_function()
def test_result_as_answer_in_tool_decorator():
@tool("Tool with result as answer", result_as_answer=True)
def my_tool_with_result_as_answer(question: str) -> str:
"""This tool will return its result as the final answer."""
return question
assert my_tool_with_result_as_answer.result_as_answer is True
converted_tool = my_tool_with_result_as_answer.to_structured_tool()
assert converted_tool.result_as_answer is True
@tool("Tool with default result_as_answer")
def my_tool_with_default(question: str) -> str:
"""This tool uses the default result_as_answer value."""
return question
assert my_tool_with_default.result_as_answer is False
converted_tool = my_tool_with_default.to_structured_tool()
assert converted_tool.result_as_answer is False