Compare commits

..

5 Commits

Author SHA1 Message Date
Devin AI
c9159ea09a Fix Agent.kickoff() to pass custom system_template and prompt_template to LiteAgent
- Add system_template and prompt_template fields to LiteAgent class
- Update LiteAgent._get_default_system_prompt() to use custom templates when provided
- Pass template parameters from Agent.kickoff() and Agent.kickoff_async() to LiteAgent
- Add comprehensive tests to verify custom templates are passed and used correctly
- Maintain backward compatibility with default behavior when no templates provided

Fixes #3261

Co-Authored-By: João <joao@crewai.com>
2025-08-02 21:39:19 +00:00
Heitor Carvalho
88ed91561f feat: add crewai config command group and tests (#3206)
Some checks failed
Notify Downstream / notify-downstream (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
2025-07-31 10:38:51 -04:00
Lorenze Jay
9a347ad458 chore: update crewai-tools dependency to version 0.59.0 and bump CrewAI version to 0.152.0 (#3244)
Some checks failed
Notify Downstream / notify-downstream (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
- Updated `crewai-tools` dependency from `0.58.0` to `0.59.0` in `pyproject.toml` and `uv.lock`.
- Bumped the version of the CrewAI library from `0.150.0` to `0.152.0` in `__init__.py`.
- Updated dependency versions in CLI templates for crew, flow, and tool projects to reflect the new CrewAI version.
2025-07-30 14:38:24 -07:00
Lucas Gomide
34c3075fdb fix: support to add memories to Mem0 with agent_id (#3217)
* fix: support to add memories to Mem0 with agent_id

* feat: removing memory_type checkings from Mem0Storage

* feat: ensure agent_id is always present while saving memory into Mem0

* fix: use OR operator when querying Mem0 memories with both user_id and agent_id
2025-07-30 11:56:46 -04:00
Vidit Ostwal
498e8dc6e8 Changed the import error to show missing module files (#2423)
Some checks failed
Notify Downstream / notify-downstream (push) Has been cancelled
* Fix issue #2421: Handle missing google.genai dependency gracefully

Co-Authored-By: Joe Moura <joao@crewai.com>

* Fix import sorting in test file

Co-Authored-By: Joe Moura <joao@crewai.com>

* Fix import sorting with ruff

Co-Authored-By: Joe Moura <joao@crewai.com>

* Removed unwatned test case

* Added dynamic catching for all the embedder function

* Dropped the comment

* Added test case

* Fixed Linting Issue

* Flaky test case in 3.13

* Test Case fixed

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Joe Moura <joao@crewai.com>
Co-authored-by: Lucas Gomide <lucaslg200@gmail.com>
2025-07-30 10:01:17 -04:00
26 changed files with 725 additions and 376 deletions

View File

@@ -48,7 +48,7 @@ Documentation = "https://docs.crewai.com"
Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
tools = ["crewai-tools~=0.58.0"]
tools = ["crewai-tools~=0.59.0"]
embeddings = [
"tiktoken~=0.8.0"
]

View File

@@ -54,7 +54,7 @@ def _track_install_async():
_track_install_async()
__version__ = "0.150.0"
__version__ = "0.152.0"
__all__ = [
"Agent",
"Crew",

View File

@@ -817,6 +817,8 @@ class Agent(BaseAgent):
original_agent=self,
guardrail=self.guardrail,
guardrail_max_retries=self.guardrail_max_retries,
system_template=self.system_template,
prompt_template=self.prompt_template,
)
return lite_agent.kickoff(messages)
@@ -853,6 +855,8 @@ class Agent(BaseAgent):
response_format=response_format,
i18n=self.i18n,
original_agent=self,
system_template=self.system_template,
prompt_template=self.prompt_template,
)
return await lite_agent.kickoff_async(messages)

View File

@@ -3,6 +3,7 @@ from typing import Optional
import click
from crewai.cli.config import Settings
from crewai.cli.settings.main import SettingsCommand
from crewai.cli.add_crew_to_flow import add_crew_to_flow
from crewai.cli.create_crew import create_crew
from crewai.cli.create_flow import create_flow
@@ -227,7 +228,7 @@ def update():
@crewai.command()
def login():
"""Sign Up/Login to CrewAI Enterprise."""
Settings().clear()
Settings().clear_user_settings()
AuthenticationCommand().login()
@@ -369,8 +370,8 @@ def org():
pass
@org.command()
def list():
@org.command("list")
def org_list():
"""List available organizations."""
org_command = OrganizationCommand()
org_command.list()
@@ -391,5 +392,34 @@ def current():
org_command.current()
@crewai.group()
def config():
"""CLI Configuration commands."""
pass
@config.command("list")
def config_list():
"""List all CLI configuration parameters."""
config_command = SettingsCommand()
config_command.list()
@config.command("set")
@click.argument("key")
@click.argument("value")
def config_set(key: str, value: str):
"""Set a CLI configuration parameter."""
config_command = SettingsCommand()
config_command.set(key, value)
@config.command("reset")
def config_reset():
"""Reset all CLI configuration parameters to default values."""
config_command = SettingsCommand()
config_command.reset_all_settings()
if __name__ == "__main__":
crewai()

View File

@@ -4,10 +4,47 @@ from typing import Optional
from pydantic import BaseModel, Field
from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
DEFAULT_CONFIG_PATH = Path.home() / ".config" / "crewai" / "settings.json"
# Settings that are related to the user's account
USER_SETTINGS_KEYS = [
"tool_repository_username",
"tool_repository_password",
"org_name",
"org_uuid",
]
# Settings that are related to the CLI
CLI_SETTINGS_KEYS = [
"enterprise_base_url",
]
# Default values for CLI settings
DEFAULT_CLI_SETTINGS = {
"enterprise_base_url": DEFAULT_CREWAI_ENTERPRISE_URL,
}
# Readonly settings - cannot be set by the user
READONLY_SETTINGS_KEYS = [
"org_name",
"org_uuid",
]
# Hidden settings - not displayed by the 'list' command and cannot be set by the user
HIDDEN_SETTINGS_KEYS = [
"config_path",
"tool_repository_username",
"tool_repository_password",
]
class Settings(BaseModel):
enterprise_base_url: Optional[str] = Field(
default=DEFAULT_CREWAI_ENTERPRISE_URL,
description="Base URL of the CrewAI Enterprise instance",
)
tool_repository_username: Optional[str] = Field(
None, description="Username for interacting with the Tool Repository"
)
@@ -20,7 +57,7 @@ class Settings(BaseModel):
org_uuid: Optional[str] = Field(
None, description="UUID of the currently active organization"
)
config_path: Path = Field(default=DEFAULT_CONFIG_PATH, exclude=True)
config_path: Path = Field(default=DEFAULT_CONFIG_PATH, frozen=True, exclude=True)
def __init__(self, config_path: Path = DEFAULT_CONFIG_PATH, **data):
"""Load Settings from config path"""
@@ -37,9 +74,16 @@ class Settings(BaseModel):
merged_data = {**file_data, **data}
super().__init__(config_path=config_path, **merged_data)
def clear(self) -> None:
"""Clear all settings"""
self.config_path.unlink(missing_ok=True)
def clear_user_settings(self) -> None:
"""Clear all user settings"""
self._reset_user_settings()
self.dump()
def reset(self) -> None:
"""Reset all settings to default values"""
self._reset_user_settings()
self._reset_cli_settings()
self.dump()
def dump(self) -> None:
"""Save current settings to settings.json"""
@@ -52,3 +96,13 @@ class Settings(BaseModel):
updated_data = {**existing_data, **self.model_dump(exclude_unset=True)}
with self.config_path.open("w") as f:
json.dump(updated_data, f, indent=4)
def _reset_user_settings(self) -> None:
"""Reset all user settings to default values"""
for key in USER_SETTINGS_KEYS:
setattr(self, key, None)
def _reset_cli_settings(self) -> None:
"""Reset all CLI settings to default values"""
for key in CLI_SETTINGS_KEYS:
setattr(self, key, DEFAULT_CLI_SETTINGS[key])

View File

@@ -1,3 +1,5 @@
DEFAULT_CREWAI_ENTERPRISE_URL = "https://app.crewai.com"
ENV_VARS = {
"openai": [
{
@@ -320,5 +322,4 @@ DEFAULT_LLM_MODEL = "gpt-4o-mini"
JSON_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
LITELLM_PARAMS = ["api_key", "api_base", "api_version"]

View File

@@ -1,4 +1,3 @@
from os import getenv
from typing import List, Optional
from urllib.parse import urljoin
@@ -6,6 +5,7 @@ import requests
from crewai.cli.config import Settings
from crewai.cli.version import get_crewai_version
from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
class PlusAPI:
@@ -29,7 +29,10 @@ class PlusAPI:
settings = Settings()
if settings.org_uuid:
self.headers["X-Crewai-Organization-Id"] = settings.org_uuid
self.base_url = getenv("CREWAI_BASE_URL", "https://app.crewai.com")
self.base_url = (
str(settings.enterprise_base_url) or DEFAULT_CREWAI_ENTERPRISE_URL
)
def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
url = urljoin(self.base_url, endpoint)
@@ -108,7 +111,6 @@ class PlusAPI:
def create_crew(self, payload) -> requests.Response:
return self._make_request("POST", self.CREWS_RESOURCE, json=payload)
def get_organizations(self) -> requests.Response:
return self._make_request("GET", self.ORGANIZATIONS_RESOURCE)

View File

View File

@@ -0,0 +1,67 @@
from rich.console import Console
from rich.table import Table
from crewai.cli.command import BaseCommand
from crewai.cli.config import Settings, READONLY_SETTINGS_KEYS, HIDDEN_SETTINGS_KEYS
from typing import Any
console = Console()
class SettingsCommand(BaseCommand):
"""A class to handle CLI configuration commands."""
def __init__(self, settings_kwargs: dict[str, Any] = {}):
super().__init__()
self.settings = Settings(**settings_kwargs)
def list(self) -> None:
"""List all CLI configuration parameters."""
table = Table(title="CrewAI CLI Configuration")
table.add_column("Setting", style="cyan", no_wrap=True)
table.add_column("Value", style="green")
table.add_column("Description", style="yellow")
# Add all settings to the table
for field_name, field_info in Settings.model_fields.items():
if field_name in HIDDEN_SETTINGS_KEYS:
# Do not display hidden settings
continue
current_value = getattr(self.settings, field_name)
description = field_info.description or "No description available"
display_value = (
str(current_value) if current_value is not None else "Not set"
)
table.add_row(field_name, display_value, description)
console.print(table)
def set(self, key: str, value: str) -> None:
"""Set a CLI configuration parameter."""
readonly_settings = READONLY_SETTINGS_KEYS + HIDDEN_SETTINGS_KEYS
if not hasattr(self.settings, key) or key in readonly_settings:
console.print(
f"Error: Unknown or readonly configuration key '{key}'",
style="bold red",
)
console.print("Available keys:", style="yellow")
for field_name in Settings.model_fields.keys():
if field_name not in readonly_settings:
console.print(f" - {field_name}", style="yellow")
raise SystemExit(1)
setattr(self.settings, key, value)
self.settings.dump()
console.print(f"Successfully set '{key}' to '{value}'", style="bold green")
def reset_all_settings(self) -> None:
"""Reset all CLI configuration parameters to default values."""
self.settings.reset()
console.print(
"Successfully reset all configuration parameters to default values. It is recommended to run [bold yellow]'crewai login'[/bold yellow] to re-authenticate.",
style="bold green",
)

View File

@@ -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]>=0.150.0,<1.0.0"
"crewai[tools]>=0.152.0,<1.0.0"
]
[project.scripts]

View File

@@ -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]>=0.150.0,<1.0.0",
"crewai[tools]>=0.152.0,<1.0.0",
]
[project.scripts]

View File

@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
readme = "README.md"
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]>=0.150.0"
"crewai[tools]>=0.152.0"
]
[tool.crewai]

View File

@@ -174,6 +174,14 @@ class LiteAgent(FlowTrackable, BaseModel):
default=[], description="Callbacks to be used for the agent"
)
# Template Properties
system_template: Optional[str] = Field(
default=None, description="Custom system template for the agent"
)
prompt_template: Optional[str] = Field(
default=None, description="Custom prompt template for the agent"
)
# Guardrail Properties
guardrail: Optional[Union[Callable[[LiteAgentOutput], Tuple[bool, Any]], str]] = (
Field(
@@ -446,6 +454,26 @@ class LiteAgent(FlowTrackable, BaseModel):
def _get_default_system_prompt(self) -> str:
"""Get the default system prompt for the agent."""
if self.system_template and self.prompt_template:
system_components = ["role_playing"]
if self._parsed_tools:
system_components.append("tools")
else:
system_components.append("no_tools")
system_parts = [self.i18n.slice(component) for component in system_components]
system_content = "".join(system_parts)
system_prompt = self.system_template.replace("{{ .System }}", system_content)
system_prompt = (
system_prompt.replace("{goal}", self.goal)
.replace("{role}", self.role)
.replace("{backstory}", self.backstory)
)
return system_prompt
base_prompt = ""
if self._parsed_tools:
# Use the prompt template for agents with tools
@@ -622,4 +650,4 @@ class LiteAgent(FlowTrackable, BaseModel):
def _append_message(self, text: str, role: str = "assistant") -> None:
"""Append a message to the message list with the given role."""
self._messages.append(format_message_for_llm(text, role=role))
self._messages.append(format_message_for_llm(text, role=role))

View File

@@ -1,7 +1,8 @@
import os
from typing import Any, Dict, List
from collections import defaultdict
from mem0 import Memory, MemoryClient
from crewai.utilities.chromadb import sanitize_collection_name
from crewai.memory.storage.interface import Storage
@@ -70,26 +71,32 @@ class Mem0Storage(Storage):
"""
Returns:
dict: A filter dictionary containing AND conditions for querying data.
- Includes user_id if memory_type is 'external'.
- Includes user_id and agent_id if both are present.
- Includes user_id if only user_id is present.
- Includes agent_id if only agent_id is present.
- Includes run_id if memory_type is 'short_term' and mem0_run_id is present.
"""
filter = {
"AND": []
}
filter = defaultdict(list)
# Add user_id condition if the memory type is external
if self.memory_type == "external":
filter["AND"].append({"user_id": self.config.get("user_id", "")})
# Add run_id condition if the memory type is short_term and a run ID is set
if self.memory_type == "short_term" and self.mem0_run_id:
filter["AND"].append({"run_id": self.mem0_run_id})
else:
user_id = self.config.get("user_id", "")
agent_id = self.config.get("agent_id", "")
if user_id and agent_id:
filter["OR"].append({"user_id": user_id})
filter["OR"].append({"agent_id": agent_id})
elif user_id:
filter["AND"].append({"user_id": user_id})
elif agent_id:
filter["AND"].append({"agent_id": agent_id})
return filter
def save(self, value: Any, metadata: Dict[str, Any]) -> None:
user_id = self.config.get("user_id", "")
assistant_message = [{"role" : "assistant","content" : value}]
assistant_message = [{"role" : "assistant","content" : value}]
base_metadata = {
"short_term": "short_term",
@@ -104,31 +111,32 @@ class Mem0Storage(Storage):
"infer": self.infer
}
if self.memory_type == "external":
# MemoryClient-specific overrides
if isinstance(self.memory, MemoryClient):
params["includes"] = self.includes
params["excludes"] = self.excludes
params["output_format"] = "v1.1"
params["version"] = "v2"
if self.memory_type == "short_term" and self.mem0_run_id:
params["run_id"] = self.mem0_run_id
if user_id:
params["user_id"] = user_id
if params:
# MemoryClient-specific overrides
if isinstance(self.memory, MemoryClient):
params["includes"] = self.includes
params["excludes"] = self.excludes
params["output_format"] = "v1.1"
params["version"]="v2"
if agent_id := self.config.get("agent_id", self._get_agent_name()):
params["agent_id"] = agent_id
if self.memory_type == "short_term":
params["run_id"] = self.mem0_run_id
self.memory.add(assistant_message, **params)
self.memory.add(assistant_message, **params)
def search(self,query: str,limit: int = 3,score_threshold: float = 0.35) -> List[Any]:
params = {
"query": query,
"limit": limit,
"query": query,
"limit": limit,
"version": "v2",
"output_format": "v1.1"
}
if user_id := self.config.get("user_id", ""):
params["user_id"] = user_id
@@ -138,7 +146,7 @@ class Mem0Storage(Storage):
"entities": {"type": "entity"},
"external": {"type": "external"},
}
if self.memory_type in memory_type_map:
params["metadata"] = memory_type_map[self.memory_type]
if self.memory_type == "short_term":
@@ -151,11 +159,28 @@ class Mem0Storage(Storage):
params['threshold'] = score_threshold
if isinstance(self.memory, Memory):
del params["metadata"], params["version"], params["run_id"], params['output_format']
del params["metadata"], params["version"], params['output_format']
if params.get("run_id"):
del params["run_id"]
results = self.memory.search(**params)
return [r for r in results["results"]]
def reset(self):
if self.memory:
self.memory.reset()
def _sanitize_role(self, role: str) -> str:
"""
Sanitizes agent roles to ensure valid directory names.
"""
return role.replace("\n", "").replace(" ", "_").replace("/", "_")
def _get_agent_name(self) -> str:
if not self.crew:
return ""
agents = self.crew.agents
agents = [self._sanitize_role(agent.role) for agent in agents]
agents = "_".join(agents)
return sanitize_collection_name(name=agents, max_collection_length=MAX_AGENT_ID_LENGTH_MEM0)

View File

@@ -44,7 +44,7 @@ class RAGStorage(BaseRAGStorage):
):
super().__init__(type, allow_reset, embedder_config, crew)
agents = crew.agents if crew else []
agents = [str(agent.id) for agent in agents]
agents = [self._sanitize_role(agent.role) for agent in agents]
agents = "_".join(agents)
self.agents = agents
self.storage_file_name = self._build_storage_file_name(type, agents)
@@ -74,6 +74,11 @@ class RAGStorage(BaseRAGStorage):
)
logging.info(f"Collection found or created: {self.collection}")
def _sanitize_role(self, role: str) -> str:
"""
Sanitizes agent roles to ensure valid directory names.
"""
return role.replace("\n", "").replace(" ", "_").replace("/", "_")
def _build_storage_file_name(self, type: str, file_name: str) -> str:
"""

View File

@@ -38,7 +38,14 @@ class EmbeddingConfigurator:
f"Unsupported embedding provider: {provider}, supported providers: {list(self.embedding_functions.keys())}"
)
embedding_function = self.embedding_functions[provider]
try:
embedding_function = self.embedding_functions[provider]
except ImportError as e:
missing_package = str(e).split()[-1]
raise ImportError(
f"{missing_package} is not installed. Please install it with: pip install {missing_package}"
)
return (
embedding_function(config)
if provider == "custom"

View File

@@ -4,7 +4,12 @@ import tempfile
import unittest
from pathlib import Path
from crewai.cli.config import Settings
from crewai.cli.config import (
Settings,
USER_SETTINGS_KEYS,
CLI_SETTINGS_KEYS,
DEFAULT_CLI_SETTINGS,
)
class TestSettings(unittest.TestCase):
@@ -52,6 +57,30 @@ class TestSettings(unittest.TestCase):
self.assertEqual(settings.tool_repository_username, "new_user")
self.assertEqual(settings.tool_repository_password, "file_pass")
def test_clear_user_settings(self):
user_settings = {key: f"value_for_{key}" for key in USER_SETTINGS_KEYS}
settings = Settings(config_path=self.config_path, **user_settings)
settings.clear_user_settings()
for key in user_settings.keys():
self.assertEqual(getattr(settings, key), None)
def test_reset_settings(self):
user_settings = {key: f"value_for_{key}" for key in USER_SETTINGS_KEYS}
cli_settings = {key: f"value_for_{key}" for key in CLI_SETTINGS_KEYS}
settings = Settings(
config_path=self.config_path, **user_settings, **cli_settings
)
settings.reset()
for key in user_settings.keys():
self.assertEqual(getattr(settings, key), None)
for key in cli_settings.keys():
self.assertEqual(getattr(settings, key), DEFAULT_CLI_SETTINGS[key])
def test_dump_new_settings(self):
settings = Settings(
config_path=self.config_path, tool_repository_username="user1"

View File

@@ -6,7 +6,7 @@ from click.testing import CliRunner
import requests
from crewai.cli.organization.main import OrganizationCommand
from crewai.cli.cli import list, switch, current
from crewai.cli.cli import org_list, switch, current
@pytest.fixture
@@ -16,44 +16,44 @@ def runner():
@pytest.fixture
def org_command():
with patch.object(OrganizationCommand, '__init__', return_value=None):
with patch.object(OrganizationCommand, "__init__", return_value=None):
command = OrganizationCommand()
yield command
@pytest.fixture
def mock_settings():
with patch('crewai.cli.organization.main.Settings') as mock_settings_class:
with patch("crewai.cli.organization.main.Settings") as mock_settings_class:
mock_settings_instance = MagicMock()
mock_settings_class.return_value = mock_settings_instance
yield mock_settings_instance
@patch('crewai.cli.cli.OrganizationCommand')
@patch("crewai.cli.cli.OrganizationCommand")
def test_org_list_command(mock_org_command_class, runner):
mock_org_instance = MagicMock()
mock_org_command_class.return_value = mock_org_instance
result = runner.invoke(list)
result = runner.invoke(org_list)
assert result.exit_code == 0
mock_org_command_class.assert_called_once()
mock_org_instance.list.assert_called_once()
@patch('crewai.cli.cli.OrganizationCommand')
@patch("crewai.cli.cli.OrganizationCommand")
def test_org_switch_command(mock_org_command_class, runner):
mock_org_instance = MagicMock()
mock_org_command_class.return_value = mock_org_instance
result = runner.invoke(switch, ['test-id'])
result = runner.invoke(switch, ["test-id"])
assert result.exit_code == 0
mock_org_command_class.assert_called_once()
mock_org_instance.switch.assert_called_once_with('test-id')
mock_org_instance.switch.assert_called_once_with("test-id")
@patch('crewai.cli.cli.OrganizationCommand')
@patch("crewai.cli.cli.OrganizationCommand")
def test_org_current_command(mock_org_command_class, runner):
mock_org_instance = MagicMock()
mock_org_command_class.return_value = mock_org_instance
@@ -67,18 +67,18 @@ def test_org_current_command(mock_org_command_class, runner):
class TestOrganizationCommand(unittest.TestCase):
def setUp(self):
with patch.object(OrganizationCommand, '__init__', return_value=None):
with patch.object(OrganizationCommand, "__init__", return_value=None):
self.org_command = OrganizationCommand()
self.org_command.plus_api_client = MagicMock()
@patch('crewai.cli.organization.main.console')
@patch('crewai.cli.organization.main.Table')
@patch("crewai.cli.organization.main.console")
@patch("crewai.cli.organization.main.Table")
def test_list_organizations_success(self, mock_table, mock_console):
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json.return_value = [
{"name": "Org 1", "uuid": "org-123"},
{"name": "Org 2", "uuid": "org-456"}
{"name": "Org 2", "uuid": "org-456"},
]
self.org_command.plus_api_client = MagicMock()
self.org_command.plus_api_client.get_organizations.return_value = mock_response
@@ -89,16 +89,14 @@ class TestOrganizationCommand(unittest.TestCase):
self.org_command.plus_api_client.get_organizations.assert_called_once()
mock_table.assert_called_once_with(title="Your Organizations")
mock_table.return_value.add_column.assert_has_calls([
call("Name", style="cyan"),
call("ID", style="green")
])
mock_table.return_value.add_row.assert_has_calls([
call("Org 1", "org-123"),
call("Org 2", "org-456")
])
mock_table.return_value.add_column.assert_has_calls(
[call("Name", style="cyan"), call("ID", style="green")]
)
mock_table.return_value.add_row.assert_has_calls(
[call("Org 1", "org-123"), call("Org 2", "org-456")]
)
@patch('crewai.cli.organization.main.console')
@patch("crewai.cli.organization.main.console")
def test_list_organizations_empty(self, mock_console):
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
@@ -110,33 +108,32 @@ class TestOrganizationCommand(unittest.TestCase):
self.org_command.plus_api_client.get_organizations.assert_called_once()
mock_console.print.assert_called_once_with(
"You don't belong to any organizations yet.",
style="yellow"
"You don't belong to any organizations yet.", style="yellow"
)
@patch('crewai.cli.organization.main.console')
@patch("crewai.cli.organization.main.console")
def test_list_organizations_api_error(self, mock_console):
self.org_command.plus_api_client = MagicMock()
self.org_command.plus_api_client.get_organizations.side_effect = requests.exceptions.RequestException("API Error")
self.org_command.plus_api_client.get_organizations.side_effect = (
requests.exceptions.RequestException("API Error")
)
with pytest.raises(SystemExit):
self.org_command.list()
self.org_command.plus_api_client.get_organizations.assert_called_once()
mock_console.print.assert_called_once_with(
"Failed to retrieve organization list: API Error",
style="bold red"
"Failed to retrieve organization list: API Error", style="bold red"
)
@patch('crewai.cli.organization.main.console')
@patch('crewai.cli.organization.main.Settings')
@patch("crewai.cli.organization.main.console")
@patch("crewai.cli.organization.main.Settings")
def test_switch_organization_success(self, mock_settings_class, mock_console):
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json.return_value = [
{"name": "Org 1", "uuid": "org-123"},
{"name": "Test Org", "uuid": "test-id"}
{"name": "Test Org", "uuid": "test-id"},
]
self.org_command.plus_api_client = MagicMock()
self.org_command.plus_api_client.get_organizations.return_value = mock_response
@@ -151,17 +148,16 @@ class TestOrganizationCommand(unittest.TestCase):
assert mock_settings_instance.org_name == "Test Org"
assert mock_settings_instance.org_uuid == "test-id"
mock_console.print.assert_called_once_with(
"Successfully switched to Test Org (test-id)",
style="bold green"
"Successfully switched to Test Org (test-id)", style="bold green"
)
@patch('crewai.cli.organization.main.console')
@patch("crewai.cli.organization.main.console")
def test_switch_organization_not_found(self, mock_console):
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json.return_value = [
{"name": "Org 1", "uuid": "org-123"},
{"name": "Org 2", "uuid": "org-456"}
{"name": "Org 2", "uuid": "org-456"},
]
self.org_command.plus_api_client = MagicMock()
self.org_command.plus_api_client.get_organizations.return_value = mock_response
@@ -170,12 +166,11 @@ class TestOrganizationCommand(unittest.TestCase):
self.org_command.plus_api_client.get_organizations.assert_called_once()
mock_console.print.assert_called_once_with(
"Organization with id 'non-existent-id' not found.",
style="bold red"
"Organization with id 'non-existent-id' not found.", style="bold red"
)
@patch('crewai.cli.organization.main.console')
@patch('crewai.cli.organization.main.Settings')
@patch("crewai.cli.organization.main.console")
@patch("crewai.cli.organization.main.Settings")
def test_current_organization_with_org(self, mock_settings_class, mock_console):
mock_settings_instance = MagicMock()
mock_settings_instance.org_name = "Test Org"
@@ -186,12 +181,11 @@ class TestOrganizationCommand(unittest.TestCase):
self.org_command.plus_api_client.get_organizations.assert_not_called()
mock_console.print.assert_called_once_with(
"Currently logged in to organization Test Org (test-id)",
style="bold green"
"Currently logged in to organization Test Org (test-id)", style="bold green"
)
@patch('crewai.cli.organization.main.console')
@patch('crewai.cli.organization.main.Settings')
@patch("crewai.cli.organization.main.console")
@patch("crewai.cli.organization.main.Settings")
def test_current_organization_without_org(self, mock_settings_class, mock_console):
mock_settings_instance = MagicMock()
mock_settings_instance.org_uuid = None
@@ -201,16 +195,14 @@ class TestOrganizationCommand(unittest.TestCase):
assert mock_console.print.call_count == 3
mock_console.print.assert_any_call(
"You're not currently logged in to any organization.",
style="yellow"
"You're not currently logged in to any organization.", style="yellow"
)
@patch('crewai.cli.organization.main.console')
@patch("crewai.cli.organization.main.console")
def test_list_organizations_unauthorized(self, mock_console):
mock_response = MagicMock()
mock_http_error = requests.exceptions.HTTPError(
"401 Client Error: Unauthorized",
response=MagicMock(status_code=401)
"401 Client Error: Unauthorized", response=MagicMock(status_code=401)
)
mock_response.raise_for_status.side_effect = mock_http_error
@@ -221,15 +213,14 @@ class TestOrganizationCommand(unittest.TestCase):
self.org_command.plus_api_client.get_organizations.assert_called_once()
mock_console.print.assert_called_once_with(
"You are not logged in to any organization. Use 'crewai login' to login.",
style="bold red"
style="bold red",
)
@patch('crewai.cli.organization.main.console')
@patch("crewai.cli.organization.main.console")
def test_switch_organization_unauthorized(self, mock_console):
mock_response = MagicMock()
mock_http_error = requests.exceptions.HTTPError(
"401 Client Error: Unauthorized",
response=MagicMock(status_code=401)
"401 Client Error: Unauthorized", response=MagicMock(status_code=401)
)
mock_response.raise_for_status.side_effect = mock_http_error
@@ -240,5 +231,5 @@ class TestOrganizationCommand(unittest.TestCase):
self.org_command.plus_api_client.get_organizations.assert_called_once()
mock_console.print.assert_called_once_with(
"You are not logged in to any organization. Use 'crewai login' to login.",
style="bold red"
style="bold red",
)

View File

@@ -1,8 +1,8 @@
import os
import unittest
from unittest.mock import MagicMock, patch, ANY
from crewai.cli.plus_api import PlusAPI
from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
class TestPlusAPI(unittest.TestCase):
@@ -30,29 +30,41 @@ class TestPlusAPI(unittest.TestCase):
)
self.assertEqual(response, mock_response)
def assert_request_with_org_id(self, mock_make_request, method: str, endpoint: str, **kwargs):
def assert_request_with_org_id(
self, mock_make_request, method: str, endpoint: str, **kwargs
):
mock_make_request.assert_called_once_with(
method, f"https://app.crewai.com{endpoint}", headers={'Authorization': ANY, 'Content-Type': ANY, 'User-Agent': ANY, 'X-Crewai-Version': ANY, 'X-Crewai-Organization-Id': self.org_uuid}, **kwargs
method,
f"{DEFAULT_CREWAI_ENTERPRISE_URL}{endpoint}",
headers={
"Authorization": ANY,
"Content-Type": ANY,
"User-Agent": ANY,
"X-Crewai-Version": ANY,
"X-Crewai-Organization-Id": self.org_uuid,
},
**kwargs,
)
@patch("crewai.cli.plus_api.Settings")
@patch("requests.Session.request")
def test_login_to_tool_repository_with_org_uuid(self, mock_make_request, mock_settings_class):
def test_login_to_tool_repository_with_org_uuid(
self, mock_make_request, mock_settings_class
):
mock_settings = MagicMock()
mock_settings.org_uuid = self.org_uuid
mock_settings.enterprise_base_url = DEFAULT_CREWAI_ENTERPRISE_URL
mock_settings_class.return_value = mock_settings
# re-initialize Client
self.api = PlusAPI(self.api_key)
mock_response = MagicMock()
mock_make_request.return_value = mock_response
response = self.api.login_to_tool_repository()
self.assert_request_with_org_id(
mock_make_request,
'POST',
'/crewai_plus/api/v1/tools/login'
mock_make_request, "POST", "/crewai_plus/api/v1/tools/login"
)
self.assertEqual(response, mock_response)
@@ -66,28 +78,27 @@ class TestPlusAPI(unittest.TestCase):
"GET", "/crewai_plus/api/v1/agents/test_agent_handle"
)
self.assertEqual(response, mock_response)
@patch("crewai.cli.plus_api.Settings")
@patch("requests.Session.request")
def test_get_agent_with_org_uuid(self, mock_make_request, mock_settings_class):
mock_settings = MagicMock()
mock_settings.org_uuid = self.org_uuid
mock_settings.enterprise_base_url = DEFAULT_CREWAI_ENTERPRISE_URL
mock_settings_class.return_value = mock_settings
# re-initialize Client
self.api = PlusAPI(self.api_key)
mock_response = MagicMock()
mock_make_request.return_value = mock_response
response = self.api.get_agent("test_agent_handle")
self.assert_request_with_org_id(
mock_make_request,
"GET",
"/crewai_plus/api/v1/agents/test_agent_handle"
mock_make_request, "GET", "/crewai_plus/api/v1/agents/test_agent_handle"
)
self.assertEqual(response, mock_response)
@patch("crewai.cli.plus_api.PlusAPI._make_request")
def test_get_tool(self, mock_make_request):
mock_response = MagicMock()
@@ -98,12 +109,13 @@ class TestPlusAPI(unittest.TestCase):
"GET", "/crewai_plus/api/v1/tools/test_tool_handle"
)
self.assertEqual(response, mock_response)
@patch("crewai.cli.plus_api.Settings")
@patch("requests.Session.request")
def test_get_tool_with_org_uuid(self, mock_make_request, mock_settings_class):
mock_settings = MagicMock()
mock_settings.org_uuid = self.org_uuid
mock_settings.enterprise_base_url = DEFAULT_CREWAI_ENTERPRISE_URL
mock_settings_class.return_value = mock_settings
# re-initialize Client
self.api = PlusAPI(self.api_key)
@@ -115,9 +127,7 @@ class TestPlusAPI(unittest.TestCase):
response = self.api.get_tool("test_tool_handle")
self.assert_request_with_org_id(
mock_make_request,
"GET",
"/crewai_plus/api/v1/tools/test_tool_handle"
mock_make_request, "GET", "/crewai_plus/api/v1/tools/test_tool_handle"
)
self.assertEqual(response, mock_response)
@@ -147,12 +157,13 @@ class TestPlusAPI(unittest.TestCase):
"POST", "/crewai_plus/api/v1/tools", json=params
)
self.assertEqual(response, mock_response)
@patch("crewai.cli.plus_api.Settings")
@patch("requests.Session.request")
def test_publish_tool_with_org_uuid(self, mock_make_request, mock_settings_class):
mock_settings = MagicMock()
mock_settings.org_uuid = self.org_uuid
mock_settings.enterprise_base_url = DEFAULT_CREWAI_ENTERPRISE_URL
mock_settings_class.return_value = mock_settings
# re-initialize Client
self.api = PlusAPI(self.api_key)
@@ -160,7 +171,7 @@ class TestPlusAPI(unittest.TestCase):
# Set up mock response
mock_response = MagicMock()
mock_make_request.return_value = mock_response
handle = "test_tool_handle"
public = True
version = "1.0.0"
@@ -180,12 +191,9 @@ class TestPlusAPI(unittest.TestCase):
"description": description,
"available_exports": None,
}
self.assert_request_with_org_id(
mock_make_request,
"POST",
"/crewai_plus/api/v1/tools",
json=expected_params
mock_make_request, "POST", "/crewai_plus/api/v1/tools", json=expected_params
)
self.assertEqual(response, mock_response)
@@ -311,8 +319,11 @@ class TestPlusAPI(unittest.TestCase):
"POST", "/crewai_plus/api/v1/crews", json=payload
)
@patch.dict(os.environ, {"CREWAI_BASE_URL": "https://custom-url.com/api"})
def test_custom_base_url(self):
@patch("crewai.cli.plus_api.Settings")
def test_custom_base_url(self, mock_settings_class):
mock_settings = MagicMock()
mock_settings.enterprise_base_url = "https://custom-url.com/api"
mock_settings_class.return_value = mock_settings
custom_api = PlusAPI("test_key")
self.assertEqual(
custom_api.base_url,

View File

@@ -0,0 +1,91 @@
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch, MagicMock, call
from crewai.cli.settings.main import SettingsCommand
from crewai.cli.config import (
Settings,
USER_SETTINGS_KEYS,
CLI_SETTINGS_KEYS,
DEFAULT_CLI_SETTINGS,
HIDDEN_SETTINGS_KEYS,
READONLY_SETTINGS_KEYS,
)
import shutil
class TestSettingsCommand(unittest.TestCase):
def setUp(self):
self.test_dir = Path(tempfile.mkdtemp())
self.config_path = self.test_dir / "settings.json"
self.settings = Settings(config_path=self.config_path)
self.settings_command = SettingsCommand(
settings_kwargs={"config_path": self.config_path}
)
def tearDown(self):
shutil.rmtree(self.test_dir)
@patch("crewai.cli.settings.main.console")
@patch("crewai.cli.settings.main.Table")
def test_list_settings(self, mock_table_class, mock_console):
mock_table_instance = MagicMock()
mock_table_class.return_value = mock_table_instance
self.settings_command.list()
# Tests that the table is created skipping hidden settings
mock_table_instance.add_row.assert_has_calls(
[
call(
field_name,
getattr(self.settings, field_name) or "Not set",
field_info.description,
)
for field_name, field_info in Settings.model_fields.items()
if field_name not in HIDDEN_SETTINGS_KEYS
]
)
# Tests that the table is printed
mock_console.print.assert_called_once_with(mock_table_instance)
def test_set_valid_keys(self):
valid_keys = Settings.model_fields.keys() - (
READONLY_SETTINGS_KEYS + HIDDEN_SETTINGS_KEYS
)
for key in valid_keys:
test_value = f"some_value_for_{key}"
self.settings_command.set(key, test_value)
self.assertEqual(getattr(self.settings_command.settings, key), test_value)
def test_set_invalid_key(self):
with self.assertRaises(SystemExit):
self.settings_command.set("invalid_key", "value")
def test_set_readonly_keys(self):
for key in READONLY_SETTINGS_KEYS:
with self.assertRaises(SystemExit):
self.settings_command.set(key, "some_readonly_key_value")
def test_set_hidden_keys(self):
for key in HIDDEN_SETTINGS_KEYS:
with self.assertRaises(SystemExit):
self.settings_command.set(key, "some_hidden_key_value")
def test_reset_all_settings(self):
for key in USER_SETTINGS_KEYS + CLI_SETTINGS_KEYS:
setattr(self.settings_command.settings, key, f"custom_value_for_{key}")
self.settings_command.settings.dump()
self.settings_command.reset_all_settings()
print(USER_SETTINGS_KEYS)
for key in USER_SETTINGS_KEYS:
self.assertEqual(getattr(self.settings_command.settings, key), None)
for key in CLI_SETTINGS_KEYS:
self.assertEqual(
getattr(self.settings_command.settings, key), DEFAULT_CLI_SETTINGS[key]
)

View File

@@ -160,41 +160,3 @@ def test_save_and_search(short_term_memory):
find = short_term_memory.search("test value", score_threshold=0.01)[0]
assert find["context"] == memory.data, "Data value mismatch."
assert find["metadata"]["agent"] == "test_agent", "Agent value mismatch."
def test_memory_with_long_agent_role():
"""Test that memory works correctly with very long agent roles."""
very_long_role = (
"Senior Equity Research Analyst specializing in corporate fundamentals and industry dynamics "
"with expertise in financial modeling, valuation techniques, and market analysis for "
"technology, healthcare, and consumer discretionary sectors, responsible for generating "
"comprehensive investment recommendations and detailed research reports"
)
agent = Agent(
role=very_long_role,
goal="Search relevant data and provide results",
backstory="You are a researcher at a leading tech think tank.",
tools=[],
verbose=True,
)
task = Task(
description="Perform a search on specific topics.",
expected_output="A list of relevant URLs based on the search query.",
agent=agent,
)
memory = ShortTermMemory(crew=Crew(agents=[agent], tasks=[task]))
test_data = "Test memory data for long role agent"
test_metadata = {"task": "test_task"}
memory.save(
value=test_data,
metadata=test_metadata,
agent=very_long_role,
)
results = memory.search("Test memory", score_threshold=0.01)
assert isinstance(results, list), "Search should return a list even with long agent roles"

View File

@@ -1,187 +0,0 @@
from crewai.agent import Agent
from crewai.crew import Crew
from crewai.memory.storage.rag_storage import RAGStorage
from crewai.task import Task
def test_long_agent_role_memory_storage():
"""Test that agents with very long roles don't exceed Windows path limits."""
very_long_role = (
"Senior Equity Research Analyst specializing in corporate fundamentals and industry dynamics "
"with expertise in financial modeling, valuation techniques, and market analysis for "
"technology, healthcare, and consumer discretionary sectors, responsible for generating "
"comprehensive investment recommendations and detailed research reports"
)
agent = Agent(
role=very_long_role,
goal="Analyze market trends and provide investment insights",
backstory="You are an experienced financial analyst with deep market knowledge.",
verbose=True,
)
task = Task(
description="Analyze the current market conditions.",
expected_output="A comprehensive market analysis report.",
agent=agent,
)
crew = Crew(agents=[agent], tasks=[task])
rag_storage = RAGStorage(type="short_term", crew=crew)
assert len(rag_storage.storage_file_name) <= 260, (
f"Storage path too long: {len(rag_storage.storage_file_name)} characters"
)
assert str(agent.id) in rag_storage.agents, (
"Agent UUID should be used in storage path"
)
def test_multiple_agents_with_long_roles():
"""Test that multiple agents with long roles create valid storage paths."""
long_roles = [
"Senior Investment Advisor specializing in equity portfolio strategy and client tailored recommendations",
"Financial Communications Specialist skilled in distilling complex analysis into concise client-facing investment reports",
"Registered Investment Advisor (RIA) specializing in equity portfolio strategy and client-tailored recommendations"
]
agents = []
for i, role in enumerate(long_roles):
agent = Agent(
role=role,
goal=f"Goal for agent {i+1}",
backstory=f"Backstory for agent {i+1}",
verbose=True,
)
agents.append(agent)
tasks = [
Task(
description=f"Task {i+1}",
expected_output=f"Output {i+1}",
agent=agent,
)
for i, agent in enumerate(agents)
]
crew = Crew(agents=agents, tasks=tasks)
rag_storage = RAGStorage(type="short_term", crew=crew)
assert len(rag_storage.storage_file_name) <= 260, (
f"Storage path too long: {len(rag_storage.storage_file_name)} characters"
)
for agent in agents:
assert str(agent.id) in rag_storage.agents, (
f"Agent UUID {agent.id} should be in storage path"
)
def test_memory_functionality_with_long_roles():
"""Test that memory save/search functionality works with long agent roles."""
long_role = (
"Senior Equity Research Analyst specializing in corporate fundamentals and industry dynamics "
"with expertise in financial modeling, valuation techniques, and market analysis"
)
agent = Agent(
role=long_role,
goal="Analyze market trends",
backstory="You are an experienced analyst.",
verbose=True,
)
task = Task(
description="Analyze market conditions.",
expected_output="Market analysis report.",
agent=agent,
)
crew = Crew(agents=[agent], tasks=[task])
rag_storage = RAGStorage(type="short_term", crew=crew)
test_value = "Test memory content for long role agent"
test_metadata = {"task": "test_task", "agent": long_role}
rag_storage.save(value=test_value, metadata=test_metadata)
results = rag_storage.search(query="Test memory", limit=1, score_threshold=0.1)
assert isinstance(results, list), "Search should return a list"
def test_backward_compatibility_short_roles():
"""Test that short agent roles still work correctly."""
agent = Agent(
role="Researcher",
goal="Research topics",
backstory="You are a researcher.",
verbose=True,
)
task = Task(
description="Research a topic.",
expected_output="Research results.",
agent=agent,
)
crew = Crew(agents=[agent], tasks=[task])
rag_storage = RAGStorage(type="short_term", crew=crew)
assert str(agent.id) in rag_storage.agents, (
"Agent UUID should be used even for short roles"
)
assert len(rag_storage.storage_file_name) <= 260, (
f"Storage path should be valid: {len(rag_storage.storage_file_name)} characters"
)
def test_uuid_based_path_uniqueness():
"""Test that different agents with same role create different storage paths."""
role = "Data Analyst"
agent1 = Agent(
role=role,
goal="Analyze data",
backstory="You are an analyst.",
verbose=True,
)
agent2 = Agent(
role=role,
goal="Analyze data",
backstory="You are an analyst.",
verbose=True,
)
task1 = Task(
description="Analyze dataset 1.",
expected_output="Analysis 1.",
agent=agent1,
)
task2 = Task(
description="Analyze dataset 2.",
expected_output="Analysis 2.",
agent=agent2,
)
crew1 = Crew(agents=[agent1], tasks=[task1])
crew2 = Crew(agents=[agent2], tasks=[task2])
storage1 = RAGStorage(type="short_term", crew=crew1)
storage2 = RAGStorage(type="short_term", crew=crew2)
assert storage1.agents != storage2.agents, (
"Different agents should create different storage paths even with same role"
)
assert str(agent1.id) in storage1.agents
assert str(agent2.id) in storage2.agents
assert str(agent1.id) != str(agent2.id)

View File

@@ -191,17 +191,39 @@ def test_save_method_with_memory_oss(mem0_storage_with_mocked_config):
"""Test save method for different memory types"""
mem0_storage, _, _ = mem0_storage_with_mocked_config
mem0_storage.memory.add = MagicMock()
# Test short_term memory type (already set in fixture)
test_value = "This is a test memory"
test_metadata = {"key": "value"}
mem0_storage.save(test_value, test_metadata)
mem0_storage.memory.add.assert_called_once_with(
[{'role': 'assistant' , 'content': test_value}],
[{"role": "assistant" , "content": test_value}],
infer=True,
metadata={"type": "short_term", "key": "value"},
run_id="my_run_id",
user_id="test_user",
agent_id='Test_Agent'
)
def test_save_method_with_multiple_agents(mem0_storage_with_mocked_config):
mem0_storage, _, _ = mem0_storage_with_mocked_config
mem0_storage.crew.agents = [MagicMock(role="Test Agent"), MagicMock(role="Test Agent 2"), MagicMock(role="Test Agent 3")]
mem0_storage.memory.add = MagicMock()
test_value = "This is a test memory"
test_metadata = {"key": "value"}
mem0_storage.save(test_value, test_metadata)
mem0_storage.memory.add.assert_called_once_with(
[{"role": "assistant" , "content": test_value}],
infer=True,
metadata={"type": "short_term", "key": "value"},
run_id="my_run_id",
user_id="test_user",
agent_id='Test_Agent_Test_Agent_2_Test_Agent_3'
)
@@ -209,13 +231,13 @@ def test_save_method_with_memory_client(mem0_storage_with_memory_client_using_co
"""Test save method for different memory types"""
mem0_storage = mem0_storage_with_memory_client_using_config_from_crew
mem0_storage.memory.add = MagicMock()
# Test short_term memory type (already set in fixture)
test_value = "This is a test memory"
test_metadata = {"key": "value"}
mem0_storage.save(test_value, test_metadata)
mem0_storage.memory.add.assert_called_once_with(
[{'role': 'assistant' , 'content': test_value}],
infer=True,
@@ -224,7 +246,9 @@ def test_save_method_with_memory_client(mem0_storage_with_memory_client_using_co
run_id="my_run_id",
includes="include1",
excludes="exclude1",
output_format='v1.1'
output_format='v1.1',
user_id='test_user',
agent_id='Test_Agent'
)
@@ -237,10 +261,10 @@ def test_search_method_with_memory_oss(mem0_storage_with_mocked_config):
results = mem0_storage.search("test query", limit=5, score_threshold=0.5)
mem0_storage.memory.search.assert_called_once_with(
query="test query",
limit=5,
query="test query",
limit=5,
user_id="test_user",
filters={'AND': [{'run_id': 'my_run_id'}]},
filters={'AND': [{'run_id': 'my_run_id'}]},
threshold=0.5
)
@@ -257,8 +281,8 @@ def test_search_method_with_memory_client(mem0_storage_with_memory_client_using_
results = mem0_storage.search("test query", limit=5, score_threshold=0.5)
mem0_storage.memory.search.assert_called_once_with(
query="test query",
limit=5,
query="test query",
limit=5,
metadata={"type": "short_term"},
user_id="test_user",
version='v2',
@@ -286,4 +310,56 @@ def test_mem0_storage_default_infer_value(mock_mem0_memory_client):
)
mem0_storage = Mem0Storage(type="short_term", crew=crew)
assert mem0_storage.infer is True
assert mem0_storage.infer is True
def test_save_memory_using_agent_entity(mock_mem0_memory_client):
config = {
"agent_id": "agent-123",
}
mock_memory = MagicMock(spec=Memory)
with patch.object(Memory, "__new__", return_value=mock_memory):
mem0_storage = Mem0Storage(type="external", config=config)
mem0_storage.save("test memory", {"key": "value"})
mem0_storage.memory.add.assert_called_once_with(
[{'role': 'assistant' , 'content': 'test memory'}],
infer=True,
metadata={"type": "external", "key": "value"},
agent_id="agent-123",
)
def test_search_method_with_agent_entity():
mem0_storage = Mem0Storage(type="external", config={"agent_id": "agent-123"})
mock_results = {"results": [{"score": 0.9, "content": "Result 1"}, {"score": 0.4, "content": "Result 2"}]}
mem0_storage.memory.search = MagicMock(return_value=mock_results)
results = mem0_storage.search("test query", limit=5, score_threshold=0.5)
mem0_storage.memory.search.assert_called_once_with(
query="test query",
limit=5,
filters={"AND": [{"agent_id": "agent-123"}]},
threshold=0.5,
)
assert len(results) == 2
assert results[0]["content"] == "Result 1"
def test_search_method_with_agent_id_and_user_id():
mem0_storage = Mem0Storage(type="external", config={"agent_id": "agent-123", "user_id": "user-123"})
mock_results = {"results": [{"score": 0.9, "content": "Result 1"}, {"score": 0.4, "content": "Result 2"}]}
mem0_storage.memory.search = MagicMock(return_value=mock_results)
results = mem0_storage.search("test query", limit=5, score_threshold=0.5)
mem0_storage.memory.search.assert_called_once_with(
query="test query",
limit=5,
user_id='user-123',
filters={"OR": [{"user_id": "user-123"}, {"agent_id": "agent-123"}]},
threshold=0.5,
)
assert len(results) == 2
assert results[0]["content"] == "Result 1"

View File

@@ -118,6 +118,8 @@ def test_lite_agent_created_with_correct_parameters(monkeypatch, verbose):
assert created_lite_agent["max_iterations"] == max_iter
assert created_lite_agent["max_execution_time"] == max_execution_time
assert created_lite_agent["verbose"] == verbose
assert created_lite_agent["system_template"] is None
assert created_lite_agent["prompt_template"] is None
assert created_lite_agent["response_format"] is None
# Test with a response_format
@@ -492,4 +494,129 @@ def test_lite_agent_with_invalid_llm():
backstory="Test backstory",
llm="invalid-model"
)
assert "Expected LLM instance of type BaseLLM" in str(exc_info.value)
assert "Expected LLM instance of type BaseLLM" in str(exc_info.value)
@pytest.mark.vcr(filter_headers=["authorization"])
def test_agent_kickoff_with_custom_templates(monkeypatch):
"""Test that Agent.kickoff() passes custom templates to LiteAgent and they are used correctly."""
custom_system_template = """<|start_header_id|>system<|end_header_id|>
{{ .System }}<|eot_id|>"""
custom_prompt_template = """<|start_header_id|>user<|end_header_id|>
{{ .Prompt }}<|eot_id|>"""
agent = Agent(
role="Test Agent",
goal="Test Goal",
backstory="Test Backstory",
system_template=custom_system_template,
prompt_template=custom_prompt_template,
llm=LLM(model="gpt-4o-mini"),
)
created_lite_agent = None
original_lite_agent = LiteAgent
class MockLiteAgent(original_lite_agent):
def __init__(self, **kwargs):
nonlocal created_lite_agent
created_lite_agent = kwargs
super().__init__(**kwargs)
def kickoff(self, messages):
return LiteAgentOutput(
raw="Test response",
agent_role=self.role
)
monkeypatch.setattr("crewai.agent.LiteAgent", MockLiteAgent)
result = agent.kickoff("Test query")
assert created_lite_agent is not None
assert created_lite_agent["system_template"] == custom_system_template
assert created_lite_agent["prompt_template"] == custom_prompt_template
assert isinstance(result, LiteAgentOutput)
assert result.raw == "Test response"
@pytest.mark.vcr(filter_headers=["authorization"])
def test_agent_kickoff_without_custom_templates(monkeypatch):
"""Test that Agent.kickoff() works normally when no custom templates are provided."""
agent = Agent(
role="Test Agent",
goal="Test Goal",
backstory="Test Backstory",
llm=LLM(model="gpt-4o-mini"),
)
created_lite_agent = None
original_lite_agent = LiteAgent
class MockLiteAgent(original_lite_agent):
def __init__(self, **kwargs):
nonlocal created_lite_agent
created_lite_agent = kwargs
super().__init__(**kwargs)
def kickoff(self, messages):
return LiteAgentOutput(
raw="Test response",
agent_role=self.role
)
monkeypatch.setattr("crewai.agent.LiteAgent", MockLiteAgent)
result = agent.kickoff("Test query")
assert created_lite_agent is not None
assert created_lite_agent["system_template"] is None
assert created_lite_agent["prompt_template"] is None
assert isinstance(result, LiteAgentOutput)
assert result.raw == "Test response"
@pytest.mark.vcr(filter_headers=["authorization"])
def test_lite_agent_uses_custom_templates_in_system_prompt():
"""Test that LiteAgent actually uses custom templates when building system prompts."""
custom_system_template = "CUSTOM SYSTEM: {{ .System }}"
custom_prompt_template = "CUSTOM PROMPT: {{ .Prompt }}"
lite_agent = LiteAgent(
role="Research Assistant",
goal="Help users find accurate information",
backstory="You are a helpful research assistant",
system_template=custom_system_template,
prompt_template=custom_prompt_template,
llm=LLM(model="gpt-4o-mini"),
)
system_prompt = lite_agent._get_default_system_prompt()
assert "CUSTOM SYSTEM:" in system_prompt
assert "Research Assistant" in system_prompt
assert "Help users find accurate information" in system_prompt
assert "You are a helpful research assistant" in system_prompt
@pytest.mark.vcr(filter_headers=["authorization"])
def test_lite_agent_falls_back_to_default_without_templates():
"""Test that LiteAgent falls back to default behavior when no custom templates are provided."""
lite_agent = LiteAgent(
role="Research Assistant",
goal="Help users find accurate information",
backstory="You are a helpful research assistant",
llm=LLM(model="gpt-4o-mini"),
)
system_prompt = lite_agent._get_default_system_prompt()
assert "CUSTOM SYSTEM:" not in system_prompt
assert "Research Assistant" in system_prompt
assert "Help users find accurate information" in system_prompt
assert "You are a helpful research assistant" in system_prompt

View File

@@ -0,0 +1,25 @@
from unittest.mock import patch
import pytest
from crewai.rag.embeddings.configurator import EmbeddingConfigurator
def test_configure_embedder_importerror():
configurator = EmbeddingConfigurator()
embedder_config = {
'provider': 'openai',
'config': {
'model': 'text-embedding-ada-002',
}
}
with patch('chromadb.utils.embedding_functions.openai_embedding_function.OpenAIEmbeddingFunction') as mock_openai:
mock_openai.side_effect = ImportError("Module not found.")
with pytest.raises(ImportError) as exc_info:
configurator.configure_embedder(embedder_config)
assert str(exc_info.value) == "Module not found."
mock_openai.assert_called_once()

9
uv.lock generated
View File

@@ -798,7 +798,7 @@ requires-dist = [
{ name = "blinker", specifier = ">=1.9.0" },
{ name = "chromadb", specifier = ">=0.5.23" },
{ name = "click", specifier = ">=8.1.7" },
{ name = "crewai-tools", marker = "extra == 'tools'", specifier = "~=0.58.0" },
{ name = "crewai-tools", marker = "extra == 'tools'", specifier = "~=0.59.0" },
{ name = "docling", marker = "extra == 'docling'", specifier = ">=2.12.0" },
{ name = "instructor", specifier = ">=1.3.3" },
{ name = "json-repair", specifier = "==0.25.2" },
@@ -850,7 +850,7 @@ dev = [
[[package]]
name = "crewai-tools"
version = "0.58.0"
version = "0.59.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "chromadb" },
@@ -860,6 +860,7 @@ dependencies = [
{ name = "embedchain" },
{ name = "lancedb" },
{ name = "openai" },
{ name = "portalocker" },
{ name = "pydantic" },
{ name = "pyright" },
{ name = "pytube" },
@@ -867,9 +868,9 @@ dependencies = [
{ name = "stagehand" },
{ name = "tiktoken" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1f/bf/72c3a0cb5a8be1f635a4e3b07ee2ad81a6d427e63b7748c2727a33ade0d4/crewai_tools-0.58.0.tar.gz", hash = "sha256:ea82d5df8611ae22a8291934c4cd0b7ed5b77eca475f81014f018b7eca4d3350", size = 1026853, upload-time = "2025-07-23T17:45:53.228Z" }
sdist = { url = "https://files.pythonhosted.org/packages/10/cd/af005e7dca5a35ed6000db2d1594acad890997b92172d8af2c1d9a83784e/crewai_tools-0.59.0.tar.gz", hash = "sha256:030e4b65446f4c6eccdcba5380bbdc90896de74589cb1bdb3cabc2c22c83f326", size = 1032093, upload-time = "2025-07-30T21:15:54.182Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/34/bd/1de36fbf8fb717817d3bf72a94da38e27af6cc5b888d7c1203a3f0b0cc2f/crewai_tools-0.58.0-py3-none-any.whl", hash = "sha256:151688bf0fa8c90e27dcdbaa8619f3dee2a14e97f1b420a38187b12d88305175", size = 650113, upload-time = "2025-07-23T17:45:51.056Z" },
{ url = "https://files.pythonhosted.org/packages/7e/b2/6b11d770fda6df99ddc49bdb26d3cb0b5efd026ff395a72ca5eeb704c335/crewai_tools-0.59.0-py3-none-any.whl", hash = "sha256:816297934eb352368ff0869a0eb11dd9febb4c40b6d0eda8e6eba8f3e426f446", size = 657025, upload-time = "2025-07-30T21:15:52.531Z" },
]
[[package]]