mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-02-04 04:58:21 +00:00
Compare commits
11 Commits
devin/1744
...
bugfix-pyt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2224bbe18 | ||
|
|
37979a0ca1 | ||
|
|
d96543d314 | ||
|
|
c9f47e6a37 | ||
|
|
5780c3147a | ||
|
|
52e10d6c84 | ||
|
|
f18a112cd7 | ||
|
|
40dcdb43d6 | ||
|
|
1167fbdd8c | ||
|
|
d200d00bb5 | ||
|
|
bf55dde358 |
8
.github/workflows/tests.yml
vendored
8
.github/workflows/tests.yml
vendored
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]:
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user