mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-08 07:38:29 +00:00
Compare commits
5 Commits
devin/1761
...
devin/1761
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1784636e93 | ||
|
|
494ed7e671 | ||
|
|
a83c57a2f2 | ||
|
|
08e15ab267 | ||
|
|
9728388ea7 |
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"pytube>=15.0.0",
|
||||
"requests>=2.32.5",
|
||||
"docker>=7.1.0",
|
||||
"crewai==1.1.0",
|
||||
"crewai==1.2.0",
|
||||
"lancedb>=0.5.4",
|
||||
"tiktoken>=0.8.0",
|
||||
"beautifulsoup4>=4.13.4",
|
||||
|
||||
@@ -287,4 +287,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.1.0"
|
||||
__version__ = "1.2.0"
|
||||
|
||||
@@ -49,7 +49,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.1.0",
|
||||
"crewai-tools==1.2.0",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
|
||||
@@ -40,7 +40,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.1.0"
|
||||
__version__ = "1.2.0"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
|
||||
@@ -1186,6 +1186,15 @@ class Agent(BaseAgent):
|
||||
Returns:
|
||||
LiteAgentOutput: The result of the agent execution.
|
||||
"""
|
||||
if self.apps:
|
||||
platform_tools = self.get_platform_tools(self.apps)
|
||||
if platform_tools:
|
||||
self.tools.extend(platform_tools)
|
||||
if self.mcps:
|
||||
mcps = self.get_mcp_tools(self.mcps)
|
||||
if mcps:
|
||||
self.tools.extend(mcps)
|
||||
|
||||
lite_agent = LiteAgent(
|
||||
id=self.id,
|
||||
role=self.role,
|
||||
|
||||
@@ -322,7 +322,7 @@ MODELS = {
|
||||
],
|
||||
}
|
||||
|
||||
DEFAULT_LLM_MODEL = "gpt-4o-mini"
|
||||
DEFAULT_LLM_MODEL = "gpt-4.1-mini"
|
||||
|
||||
JSON_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.1.0"
|
||||
"crewai[tools]==1.2.0"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.1.0"
|
||||
"crewai[tools]==1.2.0"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyvis.network import Network # type: ignore[import-untyped]
|
||||
|
||||
@@ -29,7 +29,7 @@ _printer = Printer()
|
||||
class FlowPlot:
|
||||
"""Handles the creation and rendering of flow visualization diagrams."""
|
||||
|
||||
def __init__(self, flow: Flow) -> None:
|
||||
def __init__(self, flow: Flow[Any]) -> None:
|
||||
"""
|
||||
Initialize FlowPlot with a flow object.
|
||||
|
||||
@@ -136,7 +136,7 @@ class FlowPlot:
|
||||
f"Unexpected error during flow visualization: {e!s}"
|
||||
) from e
|
||||
finally:
|
||||
self._cleanup_pyvis_lib()
|
||||
self._cleanup_pyvis_lib(filename)
|
||||
|
||||
def _generate_final_html(self, network_html: str) -> str:
|
||||
"""
|
||||
@@ -186,26 +186,33 @@ class FlowPlot:
|
||||
raise IOError(f"Failed to generate visualization HTML: {e!s}") from e
|
||||
|
||||
@staticmethod
|
||||
def _cleanup_pyvis_lib() -> None:
|
||||
def _cleanup_pyvis_lib(filename: str) -> None:
|
||||
"""
|
||||
Clean up the generated lib folder from pyvis.
|
||||
|
||||
This method safely removes the temporary lib directory created by pyvis
|
||||
during network visualization generation.
|
||||
during network visualization generation. The lib folder is created in the
|
||||
same directory as the output HTML file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : str
|
||||
The output filename (without .html extension) used for the visualization.
|
||||
"""
|
||||
try:
|
||||
lib_folder = safe_path_join("lib", root=os.getcwd())
|
||||
if os.path.exists(lib_folder) and os.path.isdir(lib_folder):
|
||||
import shutil
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(lib_folder)
|
||||
except ValueError as e:
|
||||
_printer.print(f"Error validating lib folder path: {e}", color="red")
|
||||
output_dir = os.path.dirname(os.path.abspath(filename)) or os.getcwd()
|
||||
lib_folder = os.path.join(output_dir, "lib")
|
||||
if os.path.exists(lib_folder) and os.path.isdir(lib_folder):
|
||||
vis_js = os.path.join(lib_folder, "vis-network.min.js")
|
||||
if os.path.exists(vis_js):
|
||||
shutil.rmtree(lib_folder)
|
||||
except Exception as e:
|
||||
_printer.print(f"Error cleaning up lib folder: {e}", color="red")
|
||||
|
||||
|
||||
def plot_flow(flow: Flow, filename: str = "flow_plot") -> None:
|
||||
def plot_flow(flow: Flow[Any], filename: str = "flow_plot") -> None:
|
||||
"""
|
||||
Convenience function to create and save a flow visualization.
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"""HTML template processing and generation for flow visualization diagrams."""
|
||||
|
||||
import base64
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from crewai.flow.path_utils import validate_path_exists
|
||||
|
||||
@@ -7,7 +10,7 @@ from crewai.flow.path_utils import validate_path_exists
|
||||
class HTMLTemplateHandler:
|
||||
"""Handles HTML template processing and generation for flow visualization diagrams."""
|
||||
|
||||
def __init__(self, template_path, logo_path):
|
||||
def __init__(self, template_path: str, logo_path: str) -> None:
|
||||
"""
|
||||
Initialize HTMLTemplateHandler with validated template and logo paths.
|
||||
|
||||
@@ -29,23 +32,23 @@ class HTMLTemplateHandler:
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Invalid template or logo path: {e}") from e
|
||||
|
||||
def read_template(self):
|
||||
def read_template(self) -> str:
|
||||
"""Read and return the HTML template file contents."""
|
||||
with open(self.template_path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def encode_logo(self):
|
||||
def encode_logo(self) -> str:
|
||||
"""Convert the logo SVG file to base64 encoded string."""
|
||||
with open(self.logo_path, "rb") as logo_file:
|
||||
logo_svg_data = logo_file.read()
|
||||
return base64.b64encode(logo_svg_data).decode("utf-8")
|
||||
|
||||
def extract_body_content(self, html):
|
||||
def extract_body_content(self, html: str) -> str:
|
||||
"""Extract and return content between body tags from HTML string."""
|
||||
match = re.search("<body.*?>(.*?)</body>", html, re.DOTALL)
|
||||
return match.group(1) if match else ""
|
||||
|
||||
def generate_legend_items_html(self, legend_items):
|
||||
def generate_legend_items_html(self, legend_items: list[dict[str, Any]]) -> str:
|
||||
"""Generate HTML markup for the legend items."""
|
||||
legend_items_html = ""
|
||||
for item in legend_items:
|
||||
@@ -73,7 +76,9 @@ class HTMLTemplateHandler:
|
||||
"""
|
||||
return legend_items_html
|
||||
|
||||
def generate_final_html(self, network_body, legend_items_html, title="Flow Plot"):
|
||||
def generate_final_html(
|
||||
self, network_body: str, legend_items_html: str, title: str = "Flow Plot"
|
||||
) -> str:
|
||||
"""Combine all components into final HTML document with network visualization."""
|
||||
html_template = self.read_template()
|
||||
logo_svg_base64 = self.encode_logo()
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
def get_legend_items(colors):
|
||||
"""Legend generation for flow visualization diagrams."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from crewai.flow.config import FlowColors
|
||||
|
||||
|
||||
def get_legend_items(colors: FlowColors) -> list[dict[str, Any]]:
|
||||
"""Generate legend items based on flow colors.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
colors : FlowColors
|
||||
Dictionary containing color definitions for flow elements.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[dict[str, Any]]
|
||||
List of legend item dictionaries with labels and styling.
|
||||
"""
|
||||
return [
|
||||
{"label": "Start Method", "color": colors["start"]},
|
||||
{"label": "Method", "color": colors["method"]},
|
||||
@@ -24,7 +43,19 @@ def get_legend_items(colors):
|
||||
]
|
||||
|
||||
|
||||
def generate_legend_items_html(legend_items):
|
||||
def generate_legend_items_html(legend_items: list[dict[str, Any]]) -> str:
|
||||
"""Generate HTML markup for legend items.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
legend_items : list[dict[str, Any]]
|
||||
List of legend item dictionaries containing labels and styling.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
HTML string containing formatted legend items.
|
||||
"""
|
||||
legend_items_html = ""
|
||||
for item in legend_items:
|
||||
if "border" in item:
|
||||
|
||||
@@ -36,28 +36,29 @@ from crewai.flow.utils import (
|
||||
from crewai.utilities.printer import Printer
|
||||
|
||||
|
||||
|
||||
_printer = Printer()
|
||||
|
||||
|
||||
def method_calls_crew(method: Any) -> bool:
|
||||
"""
|
||||
Check if the method contains a call to `.crew()`.
|
||||
Check if the method contains a call to `.crew()`, `.kickoff()`, or `.kickoff_async()`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
method : Any
|
||||
The method to analyze for crew() calls.
|
||||
The method to analyze for crew or agent execution calls.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if the method calls .crew(), False otherwise.
|
||||
True if the method calls .crew(), .kickoff(), or .kickoff_async(), False otherwise.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Uses AST analysis to detect method calls, specifically looking for
|
||||
attribute access of 'crew'.
|
||||
attribute access of 'crew', 'kickoff', or 'kickoff_async'.
|
||||
This includes both traditional Crew execution (.crew()) and Agent/LiteAgent
|
||||
execution (.kickoff() or .kickoff_async()).
|
||||
"""
|
||||
try:
|
||||
source = inspect.getsource(method)
|
||||
@@ -68,14 +69,14 @@ def method_calls_crew(method: Any) -> bool:
|
||||
return False
|
||||
|
||||
class CrewCallVisitor(ast.NodeVisitor):
|
||||
"""AST visitor to detect .crew() method calls."""
|
||||
"""AST visitor to detect .crew(), .kickoff(), or .kickoff_async() method calls."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.found = False
|
||||
|
||||
def visit_Call(self, node):
|
||||
def visit_Call(self, node: ast.Call) -> None:
|
||||
if isinstance(node.func, ast.Attribute):
|
||||
if node.func.attr == "crew":
|
||||
if node.func.attr in ("crew", "kickoff", "kickoff_async"):
|
||||
self.found = True
|
||||
self.generic_visit(node)
|
||||
|
||||
@@ -113,7 +114,7 @@ def add_nodes_to_network(
|
||||
- Regular methods
|
||||
"""
|
||||
|
||||
def human_friendly_label(method_name):
|
||||
def human_friendly_label(method_name: str) -> str:
|
||||
return method_name.replace("_", " ").title()
|
||||
|
||||
node_style: (
|
||||
|
||||
@@ -13,11 +13,21 @@ from crewai.project.annotations import (
|
||||
task,
|
||||
tool,
|
||||
)
|
||||
from crewai.project.crew_base import CrewBase
|
||||
from crewai.project.crew_base import (
|
||||
AgentConfig,
|
||||
AgentsConfigDict,
|
||||
CrewBase,
|
||||
TaskConfig,
|
||||
TasksConfigDict,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentConfig",
|
||||
"AgentsConfigDict",
|
||||
"CrewBase",
|
||||
"TaskConfig",
|
||||
"TasksConfigDict",
|
||||
"after_kickoff",
|
||||
"agent",
|
||||
"before_kickoff",
|
||||
|
||||
@@ -52,11 +52,11 @@ class AgentConfig(TypedDict, total=False):
|
||||
allow_delegation: bool
|
||||
max_iter: int
|
||||
max_tokens: int
|
||||
callbacks: list[str]
|
||||
callbacks: list[str] | list[Any]
|
||||
|
||||
# LLM configuration
|
||||
llm: str
|
||||
function_calling_llm: str
|
||||
# LLM configuration (can be string references or resolved instances)
|
||||
llm: str | Any
|
||||
function_calling_llm: str | Any
|
||||
use_system_prompt: bool
|
||||
|
||||
# Template configuration
|
||||
@@ -66,7 +66,7 @@ class AgentConfig(TypedDict, total=False):
|
||||
|
||||
# Tools and handlers (can be string references or instances)
|
||||
tools: list[str] | list[BaseTool]
|
||||
step_callback: str
|
||||
step_callback: str | Any
|
||||
cache_handler: str | CacheHandler
|
||||
|
||||
# Code execution
|
||||
@@ -111,18 +111,18 @@ class TaskConfig(TypedDict, total=False):
|
||||
description: str
|
||||
expected_output: str
|
||||
|
||||
# Agent and context
|
||||
agent: str
|
||||
context: list[str]
|
||||
# Agent and context (can be string references or resolved instances)
|
||||
agent: str | Any
|
||||
context: list[str] | list[Any]
|
||||
|
||||
# Tools and callbacks (can be string references or instances)
|
||||
tools: list[str] | list[BaseTool]
|
||||
callback: str
|
||||
callbacks: list[str]
|
||||
callback: str | Any
|
||||
callbacks: list[str] | list[Any]
|
||||
|
||||
# Output configuration
|
||||
output_json: str
|
||||
output_pydantic: str
|
||||
# Output configuration (can be string references or resolved class wrappers)
|
||||
output_json: str | Any
|
||||
output_pydantic: str | Any
|
||||
output_file: str
|
||||
create_directory: bool
|
||||
|
||||
@@ -139,6 +139,10 @@ class TaskConfig(TypedDict, total=False):
|
||||
allow_crewai_trigger_context: bool
|
||||
|
||||
|
||||
AgentsConfigDict = dict[str, AgentConfig]
|
||||
TasksConfigDict = dict[str, TaskConfig]
|
||||
|
||||
|
||||
load_dotenv()
|
||||
|
||||
CallableT = TypeVar("CallableT", bound=Callable[..., Any])
|
||||
@@ -378,8 +382,14 @@ def load_configurations(self: CrewInstance) -> None:
|
||||
Args:
|
||||
self: Crew instance with configuration paths.
|
||||
"""
|
||||
self.agents_config = self._load_config(self.original_agents_config_path, "agent")
|
||||
self.tasks_config = self._load_config(self.original_tasks_config_path, "task")
|
||||
self.agents_config = cast(
|
||||
AgentsConfigDict,
|
||||
self._load_config(self.original_agents_config_path, "agent"),
|
||||
)
|
||||
self.tasks_config = cast(
|
||||
TasksConfigDict,
|
||||
self._load_config(self.original_tasks_config_path, "task"),
|
||||
)
|
||||
|
||||
|
||||
def load_yaml(config_path: Path) -> dict[str, Any]:
|
||||
|
||||
@@ -22,6 +22,12 @@ from typing_extensions import Self
|
||||
if TYPE_CHECKING:
|
||||
from crewai import Agent, Crew, Task
|
||||
from crewai.crews.crew_output import CrewOutput
|
||||
from crewai.project.crew_base import (
|
||||
AgentConfig,
|
||||
AgentsConfigDict,
|
||||
TaskConfig,
|
||||
TasksConfigDict,
|
||||
)
|
||||
from crewai.tools import BaseTool
|
||||
|
||||
|
||||
@@ -75,8 +81,8 @@ class CrewInstance(Protocol):
|
||||
base_directory: Path
|
||||
original_agents_config_path: str
|
||||
original_tasks_config_path: str
|
||||
agents_config: dict[str, Any]
|
||||
tasks_config: dict[str, Any]
|
||||
agents_config: AgentsConfigDict
|
||||
tasks_config: TasksConfigDict
|
||||
mcp_server_params: Any
|
||||
mcp_connect_timeout: int
|
||||
|
||||
@@ -90,7 +96,7 @@ class CrewInstance(Protocol):
|
||||
def _map_agent_variables(
|
||||
self,
|
||||
agent_name: str,
|
||||
agent_info: dict[str, Any],
|
||||
agent_info: AgentConfig,
|
||||
llms: dict[str, Callable[..., Any]],
|
||||
tool_functions: dict[str, Callable[..., Any]],
|
||||
cache_handler_functions: dict[str, Callable[..., Any]],
|
||||
@@ -99,7 +105,7 @@ class CrewInstance(Protocol):
|
||||
def _map_task_variables(
|
||||
self,
|
||||
task_name: str,
|
||||
task_info: dict[str, Any],
|
||||
task_info: TaskConfig,
|
||||
agents: dict[str, Callable[..., Any]],
|
||||
tasks: dict[str, Callable[..., Any]],
|
||||
output_json_functions: dict[str, Callable[..., Any]],
|
||||
|
||||
@@ -419,7 +419,7 @@ def handle_context_length(
|
||||
i18n: I18N instance for messages
|
||||
|
||||
Raises:
|
||||
LLMContextLengthExceededError: If context length is exceeded and user opts not to summarize
|
||||
SystemExit: If context length is exceeded and user opts not to summarize
|
||||
"""
|
||||
if respect_context_window:
|
||||
printer.print(
|
||||
@@ -432,7 +432,7 @@ def handle_context_length(
|
||||
content="Context length exceeded. Consider using smaller text or RAG tools from crewai_tools.",
|
||||
color="red",
|
||||
)
|
||||
raise LLMContextLengthExceededError(
|
||||
raise SystemExit(
|
||||
"Context length exceeded and user opted not to summarize. Consider using smaller text or RAG tools from crewai_tools."
|
||||
)
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ def create_llm(
|
||||
try:
|
||||
return LLM(model=llm_value)
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to instantiate LLM with model='{llm_value}': {e}")
|
||||
return None
|
||||
logger.error(f"Error instantiating LLM from string: {e}")
|
||||
raise e
|
||||
|
||||
if llm_value is None:
|
||||
return _llm_via_environment_or_fallback()
|
||||
@@ -62,8 +62,8 @@ def create_llm(
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error instantiating LLM from unknown object type: {e}")
|
||||
return None
|
||||
logger.error(f"Error instantiating LLM from unknown object type: {e}")
|
||||
raise e
|
||||
|
||||
|
||||
UNACCEPTED_ATTRIBUTES: Final[list[str]] = [
|
||||
@@ -176,10 +176,10 @@ def _llm_via_environment_or_fallback() -> LLM | None:
|
||||
try:
|
||||
return LLM(**llm_params)
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
logger.error(
|
||||
f"Error instantiating LLM from environment/fallback: {type(e).__name__}: {e}"
|
||||
)
|
||||
return None
|
||||
raise e
|
||||
|
||||
|
||||
def _normalize_key_name(key_name: str) -> str:
|
||||
|
||||
@@ -6,6 +6,7 @@ from unittest import mock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from crewai.agents.crew_agent_executor import AgentFinish, CrewAgentExecutor
|
||||
from crewai.cli.constants import DEFAULT_LLM_MODEL
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.tool_usage_events import ToolUsageFinishedEvent
|
||||
from crewai.knowledge.knowledge import Knowledge
|
||||
@@ -18,9 +19,6 @@ from crewai.process import Process
|
||||
from crewai.tools.tool_calling import InstructorToolCalling
|
||||
from crewai.tools.tool_usage import ToolUsage
|
||||
from crewai.utilities.errors import AgentRepositoryError
|
||||
from crewai.utilities.exceptions.context_window_exceeding_exception import (
|
||||
LLMContextLengthExceededError,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from crewai import Agent, Crew, Task
|
||||
@@ -138,7 +136,7 @@ def test_agent_with_missing_response_template():
|
||||
|
||||
def test_agent_default_values():
|
||||
agent = Agent(role="test role", goal="test goal", backstory="test backstory")
|
||||
assert agent.llm.model == "gpt-4o-mini"
|
||||
assert agent.llm.model == DEFAULT_LLM_MODEL
|
||||
assert agent.allow_delegation is False
|
||||
|
||||
|
||||
@@ -228,7 +226,7 @@ def test_logging_tool_usage():
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
assert agent.llm.model == "gpt-4o-mini"
|
||||
assert agent.llm.model == DEFAULT_LLM_MODEL
|
||||
assert agent.tools_handler.last_used_tool is None
|
||||
task = Task(
|
||||
description="What is 3 times 4?",
|
||||
|
||||
@@ -591,3 +591,81 @@ def test_lite_agent_with_invalid_llm():
|
||||
llm="invalid-model",
|
||||
)
|
||||
assert "Expected LLM instance of type BaseLLM" in str(exc_info.value)
|
||||
|
||||
|
||||
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"})
|
||||
@patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get")
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
def test_agent_kickoff_with_platform_tools(mock_get):
|
||||
"""Test that Agent.kickoff() properly integrates platform tools with LiteAgent"""
|
||||
mock_response = Mock()
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_response.json.return_value = {
|
||||
"actions": {
|
||||
"github": [
|
||||
{
|
||||
"name": "create_issue",
|
||||
"description": "Create a GitHub issue",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string", "description": "Issue title"},
|
||||
"body": {"type": "string", "description": "Issue body"},
|
||||
},
|
||||
"required": ["title"],
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
llm=LLM(model="gpt-3.5-turbo"),
|
||||
apps=["github"],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = agent.kickoff("Create a GitHub issue")
|
||||
|
||||
assert isinstance(result, LiteAgentOutput)
|
||||
assert result.raw is not None
|
||||
|
||||
|
||||
@patch.dict("os.environ", {"EXA_API_KEY": "test_exa_key"})
|
||||
@patch("crewai.agent.Agent._get_external_mcp_tools")
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
def test_agent_kickoff_with_mcp_tools(mock_get_mcp_tools):
|
||||
"""Test that Agent.kickoff() properly integrates MCP tools with LiteAgent"""
|
||||
# Setup mock MCP tools - create a proper BaseTool instance
|
||||
class MockMCPTool(BaseTool):
|
||||
name: str = "exa_search"
|
||||
description: str = "Search the web using Exa"
|
||||
|
||||
def _run(self, query: str) -> str:
|
||||
return f"Mock search results for: {query}"
|
||||
|
||||
mock_get_mcp_tools.return_value = [MockMCPTool()]
|
||||
|
||||
# Create agent with MCP servers
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
llm=LLM(model="gpt-3.5-turbo"),
|
||||
mcps=["https://mcp.exa.ai/mcp?api_key=test_exa_key&profile=research"],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Execute kickoff
|
||||
result = agent.kickoff("Search for information about AI")
|
||||
|
||||
# Verify the result is a LiteAgentOutput
|
||||
assert isinstance(result, LiteAgentOutput)
|
||||
assert result.raw is not None
|
||||
|
||||
# Verify MCP tools were retrieved
|
||||
mock_get_mcp_tools.assert_called_once_with("https://mcp.exa.ai/mcp?api_key=test_exa_key&profile=research")
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages": [{"role": "system", "content": "You are Test Agent. Test backstory\nYour
|
||||
personal goal is: Test goal\n\nYou ONLY have access to the following tools,
|
||||
and should NEVER make up tools that are not listed here:\n\nTool Name: exa_search\nTool
|
||||
Arguments: {''query'': {''description'': None, ''type'': ''str''}}\nTool Description:
|
||||
Search the web using Exa\n\nIMPORTANT: Use the following format in your response:\n\n```\nThought:
|
||||
you should always think about what to do\nAction: the action to take, only one
|
||||
name of [exa_search], just the name, exactly as it''s written.\nAction Input:
|
||||
the input to the action, just a simple JSON object, enclosed in curly braces,
|
||||
using \" to wrap keys and values.\nObservation: the result of the action\n```\n\nOnce
|
||||
all necessary information is gathered, return the following format:\n\n```\nThought:
|
||||
I now know the final answer\nFinal Answer: the final answer to the original
|
||||
input question\n```"}, {"role": "user", "content": "Search for information about
|
||||
AI"}], "model": "gpt-3.5-turbo", "stream": false}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '1038'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.109.1
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.109.1
|
||||
x-stainless-read-timeout:
|
||||
- '600'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFNNb9swDL3nVxA6J0GTLgnmW7pLDGzrPi+dC0ORaVurLHoSVaQI8t8H
|
||||
OR92twzYxYD4+MjHR3o/AhC6EAkIVUtWTWsm776/320+fbzbPLfy893br8vi6WGePnywNW3uxTgy
|
||||
aPsTFZ9ZU0VNa5A12SOsHErGWHW2Ws5uFzfz1awDGirQRFrV8uR2uphwcFua3MzmixOzJq3QiwR+
|
||||
jAAA9t03arQF7kQCN+NzpEHvZYUiuSQBCEcmRoT0XnuWlsW4BxVZRtvJ/lZTqGpOIAVfUzAFBI/A
|
||||
NQLuZO5ROlUDExlggtOzJAfaluQaGUcFuaXAsE6nmV2rGEkG5HMMUtsGTmCfiV8B3UsmEsjEOs3E
|
||||
IbP3W4/uWR65X9AHwx4cmmhebLxOoXTUXNM1zexwNIdl8DJaa4MxA0BaS9x16Ex9PCGHi42GqtbR
|
||||
1v9BFaW22te5Q+nJRss8Uys69DACeOzWFV5tQLSOmpZzpifs2s1ns2M90V9Ij75ZnkAmlmbAWqzG
|
||||
V+rlBbLUxg8WLpRUNRY9tb8OGQpNA2A0mPpvNddqHyfXtvqf8j2gFLaMRd46LLR6PXGf5jD+QP9K
|
||||
u7jcCRbxSLTCnDW6uIkCSxnM8bSFf/GMTV5qW6Frne7uO25ydBj9BgAA//8DAChlpSTeAwAA
|
||||
headers:
|
||||
CF-RAY:
|
||||
- 993d6b3e6b64ffb8-SJC
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Fri, 24 Oct 2025 23:57:52 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- __cf_bm=cXZeAPPk9o5VuaArJFruIKai9Oj2X9ResvQgx_qCwdg-1761350272-1.0.1.1-42v7QDan6OIFJYT2vOisNB0AeLg3KsbAiCGsrrsPgH1N13l8o_Vy6HvQCVCIRAqPaHCcvybK8xTxrHKqZgLBRH4XM7.l5IYkFLhgl8IIUA0;
|
||||
path=/; expires=Sat, 25-Oct-25 00:27:52 GMT; domain=.api.openai.com; HttpOnly;
|
||||
Secure; SameSite=None
|
||||
- _cfuvid=wGtD6dA8GfZzwvY_uzLiXlAVzOIOJPtIPQYQRS_19oo-1761350272656-0.0.1.1-604800000;
|
||||
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '718'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '791'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- '10000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '50000000'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '9999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '49999774'
|
||||
x-ratelimit-reset-requests:
|
||||
- 6ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_a2e42e9d98bc4c3db1a4de14cf1a94ec
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"messages": [{"role": "system", "content": "You are Test Agent. Test backstory\nYour
|
||||
personal goal is: Test goal\n\nYou ONLY have access to the following tools,
|
||||
and should NEVER make up tools that are not listed here:\n\nTool Name: exa_search\nTool
|
||||
Arguments: {''query'': {''description'': None, ''type'': ''str''}}\nTool Description:
|
||||
Search the web using Exa\n\nIMPORTANT: Use the following format in your response:\n\n```\nThought:
|
||||
you should always think about what to do\nAction: the action to take, only one
|
||||
name of [exa_search], just the name, exactly as it''s written.\nAction Input:
|
||||
the input to the action, just a simple JSON object, enclosed in curly braces,
|
||||
using \" to wrap keys and values.\nObservation: the result of the action\n```\n\nOnce
|
||||
all necessary information is gathered, return the following format:\n\n```\nThought:
|
||||
I now know the final answer\nFinal Answer: the final answer to the original
|
||||
input question\n```"}, {"role": "user", "content": "Search for information about
|
||||
AI"}, {"role": "assistant", "content": "Thought: I should use the exa_search
|
||||
tool to search for information about AI.\nAction: exa_search\nAction Input:
|
||||
{\"query\": \"AI\"}\nObservation: Mock search results for: AI"}], "model": "gpt-3.5-turbo",
|
||||
"stream": false}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '1250'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- __cf_bm=cXZeAPPk9o5VuaArJFruIKai9Oj2X9ResvQgx_qCwdg-1761350272-1.0.1.1-42v7QDan6OIFJYT2vOisNB0AeLg3KsbAiCGsrrsPgH1N13l8o_Vy6HvQCVCIRAqPaHCcvybK8xTxrHKqZgLBRH4XM7.l5IYkFLhgl8IIUA0;
|
||||
_cfuvid=wGtD6dA8GfZzwvY_uzLiXlAVzOIOJPtIPQYQRS_19oo-1761350272656-0.0.1.1-604800000
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.109.1
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.109.1
|
||||
x-stainless-read-timeout:
|
||||
- '600'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFNNaxsxEL3vrxh06cU2/sBJs5diCi0phULr0EMaFlma3VWs1ajSbG0T
|
||||
/N+L1o5306bQi0B6743evJGeMgBhtMhBqFqyarwdv7/7vP9kb+jjt8dV/Ln/otdrh3ezjdx9DUaM
|
||||
koI2j6j4WTVR1HiLbMidYBVQMqaqs+ur2WI5nV8vOqAhjTbJKs/jxWQ55jZsaDydzZdnZU1GYRQ5
|
||||
3GcAAE/dmjw6jXuRw3T0fNJgjLJCkV9IACKQTSdCxmgiS8di1IOKHKPrbK9raquac7gFRzvYpoVr
|
||||
hNI4aUG6uMPww33odqtul6iKWqvdG040DRKiR2VKo86CCXxPBDhQC9ZsERoEJogog6qhpADSHbg2
|
||||
rgK0ESGgTTElzur23dBpwLKNMiXlWmsHgHSOWKaku4wezsjxkoqlygfaxD+kojTOxLoIKCO5lEBk
|
||||
8qJDjxnAQ5d++yJQ4QM1ngumLXbXzZdXp3qiH3iPLhZnkImlHaje3oxeqVdoZGlsHMxPKKlq1L20
|
||||
H7ZstaEBkA26/tvNa7VPnRtX/U/5HlAKPaMufEBt1MuOe1rA9B/+Rbuk3BkWEcMvo7BggyFNQmMp
|
||||
W3t6qSIeImNTlMZVGHww3XNNk8yO2W8AAAD//wMA7uEpt60DAAA=
|
||||
headers:
|
||||
CF-RAY:
|
||||
- 993d6b44dc97ffb8-SJC
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Fri, 24 Oct 2025 23:57:53 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '446'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '655'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- '10000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '50000000'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '9999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '49999732'
|
||||
x-ratelimit-reset-requests:
|
||||
- 6ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_9ce6b4f80d9546eba4ce23b5fac77153
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -0,0 +1,126 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages": [{"role": "system", "content": "You are Test Agent. Test backstory\nYour
|
||||
personal goal is: Test goal\n\nYou ONLY have access to the following tools,
|
||||
and should NEVER make up tools that are not listed here:\n\nTool Name: create_issue\nTool
|
||||
Arguments: {''title'': {''description'': ''Issue title'', ''type'': ''str''},
|
||||
''body'': {''description'': ''Issue body'', ''type'': ''Union[str, NoneType]''}}\nTool
|
||||
Description: Create a GitHub issue\nDetailed Parameter Structure:\nObject with
|
||||
properties:\n - title: Issue title (required)\n - body: Issue body (optional)\n\nIMPORTANT:
|
||||
Use the following format in your response:\n\n```\nThought: you should always
|
||||
think about what to do\nAction: the action to take, only one name of [create_issue],
|
||||
just the name, exactly as it''s written.\nAction Input: the input to the action,
|
||||
just a simple JSON object, enclosed in curly braces, using \" to wrap keys and
|
||||
values.\nObservation: the result of the action\n```\n\nOnce all necessary information
|
||||
is gathered, return the following format:\n\n```\nThought: I now know the final
|
||||
answer\nFinal Answer: the final answer to the original input question\n```"},
|
||||
{"role": "user", "content": "Create a GitHub issue"}], "model": "gpt-3.5-turbo",
|
||||
"stream": false}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '1233'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.109.1
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.109.1
|
||||
x-stainless-read-timeout:
|
||||
- '600'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFNNbxMxEL3vrxj5nET5aGjIBUGoIMAFCRASqiLHns0O9Xose7ZtqPLf
|
||||
0XrTbApF4rKHefOe37yZfSgAFFm1BGUqLaYObrj6+un+45er9ZvF/PW3tZirz+OXvy6+0/jDav1W
|
||||
DVoGb3+ikUfWyHAdHAqx72ATUQu2qpPLF5PZfDy9vMhAzRZdS9sFGc5G86E0ccvD8WQ6PzIrJoNJ
|
||||
LeFHAQDwkL+tR2/xXi1hPHis1JiS3qFanpoAVGTXVpROiZJoL2rQg4a9oM+213BHzoFHtFBzREgB
|
||||
DZVkgHzJsdbtMCAM3Sig4R3J+2YLlFKDI1hx4yzsuYHgUCeEEPmWLHZiFkWTS5AaU4FOIBWCkDgE
|
||||
7S1s2e6By1zNclnnLis6usH+2Vfn7iOWTdJter5x7gzQ3rNkwzm36yNyOCXleBcib9MfVFWSp1Rt
|
||||
IurEvk0lCQeV0UMBcJ030jwJWYXIdZCN8A3m56bzeaen+iPo0dnsCAqLdmesxWLwjN7mGNzZTpXR
|
||||
pkLbU/sD0I0lPgOKs6n/dvOcdjc5+d3/yPeAMRgE7SZEtGSeTty3RWz/kX+1nVLOhlXCeEsGN0IY
|
||||
201YLHXjuutVaZ8E601JfocxRMon3G6yOBS/AQAA//8DABKn8+vBAwAA
|
||||
headers:
|
||||
CF-RAY:
|
||||
- 993d6b4be9862379-SJC
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Fri, 24 Oct 2025 23:57:54 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- __cf_bm=WY9bgemMDI_hUYISAPlQ2a.DBGeZfM6AjVEa3SKNg1c-1761350274-1.0.1.1-K3Qm2cl6IlDAgmocoKZ8IMUTmue6Q81hH9stECprUq_SM8LF8rR9d1sHktvRCN3.jEM.twEuFFYDNpBnN8NBRJFZcea1yvpm8Uo0G_UhyDs;
|
||||
path=/; expires=Sat, 25-Oct-25 00:27:54 GMT; domain=.api.openai.com; HttpOnly;
|
||||
Secure; SameSite=None
|
||||
- _cfuvid=JklLS4i3hBGELpS9cz1KMpTbj72hCwP41LyXDSxWIv8-1761350274521-0.0.1.1-604800000;
|
||||
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '487'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '526'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- '10000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '50000000'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '9999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '49999727'
|
||||
x-ratelimit-reset-requests:
|
||||
- 6ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_1708dc0928c64882aaa5bc2c168c140f
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
0
lib/crewai/tests/project/__init__.py
Normal file
0
lib/crewai/tests/project/__init__.py
Normal file
193
lib/crewai/tests/project/test_crew_base_configs.py
Normal file
193
lib/crewai/tests/project/test_crew_base_configs.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""Tests for CrewBase configuration type annotations."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.project import AgentConfig, AgentsConfigDict, CrewBase, TaskConfig, TasksConfigDict, agent, task
|
||||
|
||||
|
||||
def test_agents_config_loads_as_dict(tmp_path: Path) -> None:
|
||||
"""Test that agents_config loads as a properly typed dictionary."""
|
||||
agents_yaml = tmp_path / "agents.yaml"
|
||||
agents_yaml.write_text(
|
||||
"""
|
||||
researcher:
|
||||
role: "Research Analyst"
|
||||
goal: "Find accurate information"
|
||||
backstory: "Expert researcher with years of experience"
|
||||
"""
|
||||
)
|
||||
|
||||
tasks_yaml = tmp_path / "tasks.yaml"
|
||||
tasks_yaml.write_text(
|
||||
"""
|
||||
research_task:
|
||||
description: "Research the topic"
|
||||
expected_output: "A comprehensive report"
|
||||
"""
|
||||
)
|
||||
|
||||
@CrewBase
|
||||
class TestCrew:
|
||||
agents_config = str(agents_yaml)
|
||||
tasks_config = str(tasks_yaml)
|
||||
|
||||
@agent
|
||||
def researcher(self):
|
||||
from crewai import Agent
|
||||
return Agent(config=self.agents_config["researcher"])
|
||||
|
||||
@task
|
||||
def research_task(self):
|
||||
from crewai import Task
|
||||
return Task(config=self.tasks_config["research_task"])
|
||||
|
||||
crew_instance = TestCrew()
|
||||
|
||||
assert isinstance(crew_instance.agents_config, dict)
|
||||
assert "researcher" in crew_instance.agents_config
|
||||
assert crew_instance.agents_config["researcher"]["role"] == "Research Analyst"
|
||||
assert crew_instance.agents_config["researcher"]["goal"] == "Find accurate information"
|
||||
assert crew_instance.agents_config["researcher"]["backstory"] == "Expert researcher with years of experience"
|
||||
|
||||
|
||||
def test_tasks_config_loads_as_dict(tmp_path: Path) -> None:
|
||||
"""Test that tasks_config loads as a properly typed dictionary."""
|
||||
agents_yaml = tmp_path / "agents.yaml"
|
||||
agents_yaml.write_text(
|
||||
"""
|
||||
writer:
|
||||
role: "Content Writer"
|
||||
goal: "Write engaging content"
|
||||
backstory: "Experienced content writer"
|
||||
"""
|
||||
)
|
||||
|
||||
tasks_yaml = tmp_path / "tasks.yaml"
|
||||
tasks_yaml.write_text(
|
||||
"""
|
||||
writing_task:
|
||||
description: "Write an article"
|
||||
expected_output: "A well-written article"
|
||||
agent: "writer"
|
||||
"""
|
||||
)
|
||||
|
||||
@CrewBase
|
||||
class TestCrew:
|
||||
agents_config = str(agents_yaml)
|
||||
tasks_config = str(tasks_yaml)
|
||||
|
||||
@agent
|
||||
def writer(self):
|
||||
from crewai import Agent
|
||||
return Agent(config=self.agents_config["writer"])
|
||||
|
||||
@task
|
||||
def writing_task(self):
|
||||
from crewai import Task
|
||||
return Task(config=self.tasks_config["writing_task"])
|
||||
|
||||
crew_instance = TestCrew()
|
||||
|
||||
assert isinstance(crew_instance.tasks_config, dict)
|
||||
assert "writing_task" in crew_instance.tasks_config
|
||||
assert crew_instance.tasks_config["writing_task"]["description"] == "Write an article"
|
||||
assert crew_instance.tasks_config["writing_task"]["expected_output"] == "A well-written article"
|
||||
|
||||
from crewai import Agent
|
||||
assert isinstance(crew_instance.tasks_config["writing_task"]["agent"], Agent)
|
||||
assert crew_instance.tasks_config["writing_task"]["agent"].role == "Content Writer"
|
||||
|
||||
|
||||
def test_empty_config_files_load_as_empty_dicts(tmp_path: Path) -> None:
|
||||
"""Test that empty config files load as empty dictionaries."""
|
||||
agents_yaml = tmp_path / "agents.yaml"
|
||||
agents_yaml.write_text("")
|
||||
|
||||
tasks_yaml = tmp_path / "tasks.yaml"
|
||||
tasks_yaml.write_text("")
|
||||
|
||||
@CrewBase
|
||||
class TestCrew:
|
||||
agents_config = str(agents_yaml)
|
||||
tasks_config = str(tasks_yaml)
|
||||
|
||||
crew_instance = TestCrew()
|
||||
|
||||
assert isinstance(crew_instance.agents_config, dict)
|
||||
assert isinstance(crew_instance.tasks_config, dict)
|
||||
assert len(crew_instance.agents_config) == 0
|
||||
assert len(crew_instance.tasks_config) == 0
|
||||
|
||||
|
||||
def test_missing_config_files_load_as_empty_dicts(tmp_path: Path) -> None:
|
||||
"""Test that missing config files load as empty dictionaries with warning."""
|
||||
nonexistent_agents = tmp_path / "nonexistent_agents.yaml"
|
||||
nonexistent_tasks = tmp_path / "nonexistent_tasks.yaml"
|
||||
|
||||
@CrewBase
|
||||
class TestCrew:
|
||||
agents_config = str(nonexistent_agents)
|
||||
tasks_config = str(nonexistent_tasks)
|
||||
|
||||
crew_instance = TestCrew()
|
||||
|
||||
assert isinstance(crew_instance.agents_config, dict)
|
||||
assert isinstance(crew_instance.tasks_config, dict)
|
||||
assert len(crew_instance.agents_config) == 0
|
||||
assert len(crew_instance.tasks_config) == 0
|
||||
|
||||
|
||||
def test_config_types_are_exported() -> None:
|
||||
"""Test that AgentConfig, TaskConfig, and type aliases are properly exported."""
|
||||
from crewai.project import AgentConfig, AgentsConfigDict, TaskConfig, TasksConfigDict
|
||||
|
||||
assert AgentConfig is not None
|
||||
assert TaskConfig is not None
|
||||
assert AgentsConfigDict is not None
|
||||
assert TasksConfigDict is not None
|
||||
|
||||
|
||||
def test_agents_config_type_annotation_exists(tmp_path: Path) -> None:
|
||||
"""Test that agents_config has proper type annotation at runtime."""
|
||||
agents_yaml = tmp_path / "agents.yaml"
|
||||
agents_yaml.write_text(
|
||||
"""
|
||||
analyst:
|
||||
role: "Data Analyst"
|
||||
goal: "Analyze data"
|
||||
"""
|
||||
)
|
||||
|
||||
tasks_yaml = tmp_path / "tasks.yaml"
|
||||
tasks_yaml.write_text(
|
||||
"""
|
||||
analysis:
|
||||
description: "Analyze the data"
|
||||
expected_output: "Analysis report"
|
||||
"""
|
||||
)
|
||||
|
||||
@CrewBase
|
||||
class TestCrew:
|
||||
agents_config = str(agents_yaml)
|
||||
tasks_config = str(tasks_yaml)
|
||||
|
||||
@agent
|
||||
def analyst(self):
|
||||
from crewai import Agent
|
||||
return Agent(config=self.agents_config["analyst"])
|
||||
|
||||
@task
|
||||
def analysis(self):
|
||||
from crewai import Task
|
||||
return Task(config=self.tasks_config["analysis"])
|
||||
|
||||
crew_instance = TestCrew()
|
||||
|
||||
assert hasattr(crew_instance, "agents_config")
|
||||
assert hasattr(crew_instance, "tasks_config")
|
||||
assert isinstance(crew_instance.agents_config, dict)
|
||||
assert isinstance(crew_instance.tasks_config, dict)
|
||||
@@ -850,6 +850,31 @@ def test_flow_plotting():
|
||||
assert isinstance(received_events[0].timestamp, datetime)
|
||||
|
||||
|
||||
def test_method_calls_crew_detection():
|
||||
"""Test that method_calls_crew() detects .crew(), .kickoff(), and .kickoff_async() calls."""
|
||||
from crewai.flow.visualization_utils import method_calls_crew
|
||||
from crewai import Agent
|
||||
|
||||
# Test with a real Flow that uses agent.kickoff()
|
||||
class FlowWithAgentKickoff(Flow):
|
||||
@start()
|
||||
def run_agent(self):
|
||||
agent = Agent(role="test", goal="test", backstory="test")
|
||||
return agent.kickoff("query")
|
||||
|
||||
flow = FlowWithAgentKickoff()
|
||||
assert method_calls_crew(flow.run_agent) is True
|
||||
|
||||
# Test with a Flow that has no crew/agent calls
|
||||
class FlowWithoutCrewCalls(Flow):
|
||||
@start()
|
||||
def simple_method(self):
|
||||
return "Just a regular method"
|
||||
|
||||
flow2 = FlowWithoutCrewCalls()
|
||||
assert method_calls_crew(flow2.simple_method) is False
|
||||
|
||||
|
||||
def test_multiple_routers_from_same_trigger():
|
||||
"""Test that multiple routers triggered by the same method all activate their listeners."""
|
||||
execution_order = []
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
"""Test agent utility functions."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from crewai.agent import Agent
|
||||
from crewai.utilities.agent_utils import handle_context_length
|
||||
from crewai.utilities.exceptions.context_window_exceeding_exception import (
|
||||
LLMContextLengthExceededError,
|
||||
)
|
||||
from crewai.utilities.i18n import I18N
|
||||
from crewai.utilities.printer import Printer
|
||||
|
||||
|
||||
def test_handle_context_length_raises_exception_when_respect_context_window_false():
|
||||
"""Test that handle_context_length raises LLMContextLengthExceededError when respect_context_window is False."""
|
||||
# Create mocks for dependencies
|
||||
printer = Printer()
|
||||
i18n = I18N()
|
||||
|
||||
# Create an agent just for its LLM
|
||||
agent = Agent(
|
||||
role="test role",
|
||||
goal="test goal",
|
||||
backstory="test backstory",
|
||||
respect_context_window=False,
|
||||
)
|
||||
|
||||
llm = agent.llm
|
||||
|
||||
# Create test messages
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "This is a test message that would exceed context length",
|
||||
}
|
||||
]
|
||||
|
||||
# Set up test parameters
|
||||
respect_context_window = False
|
||||
callbacks = []
|
||||
|
||||
with pytest.raises(LLMContextLengthExceededError) as excinfo:
|
||||
handle_context_length(
|
||||
respect_context_window=respect_context_window,
|
||||
printer=printer,
|
||||
messages=messages,
|
||||
llm=llm,
|
||||
callbacks=callbacks,
|
||||
i18n=i18n,
|
||||
)
|
||||
|
||||
assert "Context length exceeded" in str(excinfo.value)
|
||||
assert "user opted not to summarize" in str(excinfo.value)
|
||||
|
||||
|
||||
def test_handle_context_length_summarizes_when_respect_context_window_true():
|
||||
"""Test that handle_context_length calls summarize_messages when respect_context_window is True."""
|
||||
# Create mocks for dependencies
|
||||
printer = Printer()
|
||||
i18n = I18N()
|
||||
|
||||
# Create an agent just for its LLM
|
||||
agent = Agent(
|
||||
role="test role",
|
||||
goal="test goal",
|
||||
backstory="test backstory",
|
||||
respect_context_window=True,
|
||||
)
|
||||
|
||||
llm = agent.llm
|
||||
|
||||
# Create test messages
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "This is a test message that would exceed context length",
|
||||
}
|
||||
]
|
||||
|
||||
# Set up test parameters
|
||||
respect_context_window = True
|
||||
callbacks = []
|
||||
|
||||
with patch("crewai.utilities.agent_utils.summarize_messages") as mock_summarize:
|
||||
handle_context_length(
|
||||
respect_context_window=respect_context_window,
|
||||
printer=printer,
|
||||
messages=messages,
|
||||
llm=llm,
|
||||
callbacks=callbacks,
|
||||
i18n=i18n,
|
||||
)
|
||||
|
||||
mock_summarize.assert_called_once_with(
|
||||
messages=messages, llm=llm, callbacks=callbacks, i18n=i18n
|
||||
)
|
||||
|
||||
|
||||
def test_handle_context_length_does_not_raise_system_exit():
|
||||
"""Test that handle_context_length does NOT raise SystemExit (regression test for issue #3774)."""
|
||||
# Create mocks for dependencies
|
||||
printer = Printer()
|
||||
i18n = I18N()
|
||||
|
||||
# Create an agent just for its LLM
|
||||
agent = Agent(
|
||||
role="test role",
|
||||
goal="test goal",
|
||||
backstory="test backstory",
|
||||
respect_context_window=False,
|
||||
)
|
||||
|
||||
llm = agent.llm
|
||||
|
||||
# Create test messages
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "This is a test message that would exceed context length",
|
||||
}
|
||||
]
|
||||
|
||||
# Set up test parameters
|
||||
respect_context_window = False
|
||||
callbacks = []
|
||||
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
handle_context_length(
|
||||
respect_context_window=respect_context_window,
|
||||
printer=printer,
|
||||
messages=messages,
|
||||
llm=llm,
|
||||
callbacks=callbacks,
|
||||
i18n=i18n,
|
||||
)
|
||||
|
||||
assert not isinstance(excinfo.value, SystemExit), (
|
||||
"handle_context_length should not raise SystemExit. "
|
||||
"It should raise LLMContextLengthExceededError instead."
|
||||
)
|
||||
|
||||
assert isinstance(excinfo.value, LLMContextLengthExceededError), (
|
||||
f"Expected LLMContextLengthExceededError but got {type(excinfo.value).__name__}"
|
||||
)
|
||||
@@ -1,77 +1,79 @@
|
||||
import os
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from crewai.cli.constants import DEFAULT_LLM_MODEL
|
||||
from crewai.llm import LLM
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
from crewai.utilities.llm_utils import create_llm
|
||||
import pytest
|
||||
|
||||
|
||||
try:
|
||||
from litellm.exceptions import BadRequestError
|
||||
except ImportError:
|
||||
BadRequestError = Exception
|
||||
|
||||
|
||||
def test_create_llm_with_llm_instance():
|
||||
existing_llm = LLM(model="gpt-4o")
|
||||
llm = create_llm(llm_value=existing_llm)
|
||||
assert llm is existing_llm
|
||||
|
||||
|
||||
def test_create_llm_with_valid_model_string():
|
||||
llm = create_llm(llm_value="gpt-4o")
|
||||
assert isinstance(llm, BaseLLM)
|
||||
assert llm.model == "gpt-4o"
|
||||
|
||||
|
||||
def test_create_llm_with_invalid_model_string():
|
||||
# For invalid model strings, create_llm succeeds but call() fails with API error
|
||||
llm = create_llm(llm_value="invalid-model")
|
||||
assert llm is not None
|
||||
assert isinstance(llm, BaseLLM)
|
||||
|
||||
# The error should occur when making the actual API call
|
||||
# We expect some kind of API error (NotFoundError, etc.)
|
||||
with pytest.raises(Exception): # noqa: B017
|
||||
llm.call(messages=[{"role": "user", "content": "Hello, world!"}])
|
||||
|
||||
|
||||
def test_create_llm_with_unknown_object_missing_attributes():
|
||||
class UnknownObject:
|
||||
pass
|
||||
|
||||
unknown_obj = UnknownObject()
|
||||
llm = create_llm(llm_value=unknown_obj)
|
||||
|
||||
# Should succeed because str(unknown_obj) provides a model name
|
||||
assert llm is not None
|
||||
assert isinstance(llm, BaseLLM)
|
||||
|
||||
|
||||
def test_create_llm_with_none_uses_default_model():
|
||||
def test_create_llm_with_llm_instance() -> None:
|
||||
with patch.dict(os.environ, {"OPENAI_API_KEY": "fake-key"}, clear=True):
|
||||
with patch("crewai.utilities.llm_utils.DEFAULT_LLM_MODEL", "gpt-4o-mini"):
|
||||
existing_llm = LLM(model="gpt-4o")
|
||||
llm = create_llm(llm_value=existing_llm)
|
||||
assert llm is existing_llm
|
||||
|
||||
|
||||
def test_create_llm_with_valid_model_string() -> None:
|
||||
with patch.dict(os.environ, {"OPENAI_API_KEY": "fake-key"}, clear=True):
|
||||
llm = create_llm(llm_value="gpt-4o")
|
||||
assert isinstance(llm, BaseLLM)
|
||||
assert llm.model == "gpt-4o"
|
||||
|
||||
|
||||
def test_create_llm_with_invalid_model_string() -> None:
|
||||
with patch.dict(os.environ, {"OPENAI_API_KEY": "fake-key"}, clear=True):
|
||||
# For invalid model strings, create_llm succeeds but call() fails with API error
|
||||
llm = create_llm(llm_value="invalid-model")
|
||||
assert llm is not None
|
||||
assert isinstance(llm, BaseLLM)
|
||||
|
||||
# The error should occur when making the actual API call
|
||||
# We expect some kind of API error (NotFoundError, etc.)
|
||||
with pytest.raises(Exception): # noqa: B017
|
||||
llm.call(messages=[{"role": "user", "content": "Hello, world!"}])
|
||||
|
||||
|
||||
def test_create_llm_with_unknown_object_missing_attributes() -> None:
|
||||
with patch.dict(os.environ, {"OPENAI_API_KEY": "fake-key"}, clear=True):
|
||||
class UnknownObject:
|
||||
pass
|
||||
|
||||
unknown_obj = UnknownObject()
|
||||
llm = create_llm(llm_value=unknown_obj)
|
||||
|
||||
# Should succeed because str(unknown_obj) provides a model name
|
||||
assert llm is not None
|
||||
assert isinstance(llm, BaseLLM)
|
||||
|
||||
|
||||
def test_create_llm_with_none_uses_default_model() -> None:
|
||||
with patch.dict(os.environ, {"OPENAI_API_KEY": "fake-key"}, clear=True):
|
||||
with patch("crewai.utilities.llm_utils.DEFAULT_LLM_MODEL", DEFAULT_LLM_MODEL):
|
||||
llm = create_llm(llm_value=None)
|
||||
assert isinstance(llm, BaseLLM)
|
||||
assert llm.model == "gpt-4o-mini"
|
||||
assert llm.model == DEFAULT_LLM_MODEL
|
||||
|
||||
|
||||
def test_create_llm_with_unknown_object():
|
||||
class UnknownObject:
|
||||
model_name = "gpt-4o"
|
||||
temperature = 0.7
|
||||
max_tokens = 1500
|
||||
def test_create_llm_with_unknown_object() -> None:
|
||||
with patch.dict(os.environ, {"OPENAI_API_KEY": "fake-key"}, clear=True):
|
||||
class UnknownObject:
|
||||
model_name = "gpt-4o"
|
||||
temperature = 0.7
|
||||
max_tokens = 1500
|
||||
|
||||
unknown_obj = UnknownObject()
|
||||
llm = create_llm(llm_value=unknown_obj)
|
||||
assert isinstance(llm, BaseLLM)
|
||||
assert llm.model == "gpt-4o"
|
||||
assert llm.temperature == 0.7
|
||||
assert llm.max_tokens == 1500
|
||||
unknown_obj = UnknownObject()
|
||||
llm = create_llm(llm_value=unknown_obj)
|
||||
assert isinstance(llm, BaseLLM)
|
||||
assert llm.model == "gpt-4o"
|
||||
assert llm.temperature == 0.7
|
||||
if hasattr(llm, 'max_tokens'):
|
||||
assert llm.max_tokens == 1500
|
||||
|
||||
|
||||
def test_create_llm_from_env_with_unaccepted_attributes():
|
||||
def test_create_llm_from_env_with_unaccepted_attributes() -> None:
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
@@ -90,25 +92,47 @@ def test_create_llm_from_env_with_unaccepted_attributes():
|
||||
assert not hasattr(llm, "AWS_REGION_NAME")
|
||||
|
||||
|
||||
def test_create_llm_with_partial_attributes():
|
||||
class PartialAttributes:
|
||||
model_name = "gpt-4o"
|
||||
# temperature is missing
|
||||
def test_create_llm_with_partial_attributes() -> None:
|
||||
with patch.dict(os.environ, {"OPENAI_API_KEY": "fake-key"}, clear=True):
|
||||
class PartialAttributes:
|
||||
model_name = "gpt-4o"
|
||||
# temperature is missing
|
||||
|
||||
obj = PartialAttributes()
|
||||
llm = create_llm(llm_value=obj)
|
||||
assert isinstance(llm, BaseLLM)
|
||||
assert llm.model == "gpt-4o"
|
||||
assert llm.temperature is None # Should handle missing attributes gracefully
|
||||
obj = PartialAttributes()
|
||||
llm = create_llm(llm_value=obj)
|
||||
assert isinstance(llm, BaseLLM)
|
||||
assert llm.model == "gpt-4o"
|
||||
assert llm.temperature is None # Should handle missing attributes gracefully
|
||||
|
||||
|
||||
def test_create_llm_with_invalid_type():
|
||||
# For integers, create_llm succeeds because str(42) becomes "42"
|
||||
llm = create_llm(llm_value=42)
|
||||
assert llm is not None
|
||||
assert isinstance(llm, BaseLLM)
|
||||
assert llm.model == "42"
|
||||
def test_create_llm_with_invalid_type() -> None:
|
||||
with patch.dict(os.environ, {"OPENAI_API_KEY": "fake-key"}, clear=True):
|
||||
# For integers, create_llm succeeds because str(42) becomes "42"
|
||||
llm = create_llm(llm_value=42)
|
||||
assert llm is not None
|
||||
assert isinstance(llm, BaseLLM)
|
||||
assert llm.model == "42"
|
||||
|
||||
# The error should occur when making the actual API call
|
||||
with pytest.raises(Exception): # noqa: B017
|
||||
llm.call(messages=[{"role": "user", "content": "Hello, world!"}])
|
||||
# The error should occur when making the actual API call
|
||||
with pytest.raises(Exception): # noqa: B017
|
||||
llm.call(messages=[{"role": "user", "content": "Hello, world!"}])
|
||||
|
||||
|
||||
def test_create_llm_openai_missing_api_key() -> None:
|
||||
"""Test that create_llm raises error when OpenAI API key is missing"""
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
with pytest.raises((ValueError, ImportError)) as exc_info:
|
||||
create_llm(llm_value="gpt-4o")
|
||||
|
||||
error_message = str(exc_info.value).lower()
|
||||
assert "openai_api_key" in error_message or "api_key" in error_message
|
||||
|
||||
|
||||
def test_create_llm_anthropic_missing_dependency() -> None:
|
||||
"""Test that create_llm raises error when Anthropic dependency is missing"""
|
||||
with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "fake-key"}, clear=True):
|
||||
with patch("crewai.llm.LLM.__new__", side_effect=ImportError('Anthropic native provider not available, to install: uv add "crewai[anthropic]"')):
|
||||
with pytest.raises(ImportError) as exc_info:
|
||||
create_llm(llm_value="anthropic/claude-3-sonnet")
|
||||
|
||||
assert "Anthropic native provider not available, to install: uv add \"crewai[anthropic]\"" in str(exc_info.value)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.1.0"
|
||||
__version__ = "1.2.0"
|
||||
|
||||
Reference in New Issue
Block a user