diff --git a/docs/concepts/cli.mdx b/docs/concepts/cli.mdx index 198cfcd8b..e8f3d3088 100644 --- a/docs/concepts/cli.mdx +++ b/docs/concepts/cli.mdx @@ -110,6 +110,8 @@ crewai reset-memories [OPTIONS] - `-s, --short`: Reset SHORT TERM memory - `-e, --entities`: Reset ENTITIES memory - `-k, --kickoff-outputs`: Reset LATEST KICKOFF TASK OUTPUTS +- `-kn, --knowledge`: Reset KNOWLEDGE storage +- `-akn, --agent-knowledge`: Reset AGENT KNOWLEDGE storage - `-a, --all`: Reset ALL memories Example: diff --git a/docs/concepts/knowledge.mdx b/docs/concepts/knowledge.mdx index b4c7627d3..a5ecd2542 100644 --- a/docs/concepts/knowledge.mdx +++ b/docs/concepts/knowledge.mdx @@ -497,6 +497,13 @@ crew = Crew( result = crew.kickoff( inputs={"question": "What is the storage capacity of the XPS 13?"} ) + +# Resetting the agent specific knowledge via crew object +crew.reset_memories(command_type = 'agent_knowledge') + +# Resetting the agent specific knowledge via CLI +crewai reset-memories --agent-knowledge +crewai reset-memories -akn ``` diff --git a/docs/concepts/memory.mdx b/docs/concepts/memory.mdx index c375d4898..43f5918e4 100644 --- a/docs/concepts/memory.mdx +++ b/docs/concepts/memory.mdx @@ -679,6 +679,7 @@ crewai reset-memories [OPTIONS] | `-e`, `--entities` | Reset ENTITIES memory. | Flag (boolean) | False | | `-k`, `--kickoff-outputs` | Reset LATEST KICKOFF TASK OUTPUTS. | Flag (boolean) | False | | `-kn`, `--knowledge` | Reset KNOWLEDEGE storage | Flag (boolean) | False | +| `-akn`, `--agent-knowledge` | Reset AGENT KNOWLEDGE storage | Flag (boolean) | False | | `-a`, `--all` | Reset ALL memories. | Flag (boolean) | False | Note: To use the cli command you need to have your crew in a file called crew.py in the same directory. @@ -716,9 +717,11 @@ my_crew.reset_memories(command_type = 'all') # Resets all the memory | `entities` | Reset ENTITIES memory. | | `kickoff_outputs` | Reset LATEST KICKOFF TASK OUTPUTS. | | `knowledge` | Reset KNOWLEDGE memory. | +| `agent_knowledge` | Reset AGENT KNOWLEDGE memory. | | `all` | Reset ALL memories. | + ## Benefits of Using CrewAI's Memory System - 🦾 **Adaptive Learning:** Crews become more efficient over time, adapting to new information and refining their approach to tasks. diff --git a/pyproject.toml b/pyproject.toml index 64deb4df1..2c71f236c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "crewai" -version = "0.119.0" +version = "0.120.1" description = "Cutting-edge framework for orchestrating role-playing, autonomous AI agents. By fostering collaborative intelligence, CrewAI empowers agents to work together seamlessly, tackling complex tasks." readme = "README.md" requires-python = ">=3.10,<3.13" @@ -45,7 +45,7 @@ Documentation = "https://docs.crewai.com" Repository = "https://github.com/crewAIInc/crewAI" [project.optional-dependencies] -tools = ["crewai-tools~=0.44.0"] +tools = ["crewai-tools~=0.45.0"] embeddings = [ "tiktoken~=0.7.0" ] diff --git a/src/crewai/__init__.py b/src/crewai/__init__.py index 42ce55390..2144c36c8 100644 --- a/src/crewai/__init__.py +++ b/src/crewai/__init__.py @@ -17,7 +17,7 @@ warnings.filterwarnings( category=UserWarning, module="pydantic.main", ) -__version__ = "0.119.0" +__version__ = "0.120.1" __all__ = [ "Agent", "Crew", diff --git a/src/crewai/cli/cli.py b/src/crewai/cli/cli.py index b2d59adbe..c0eff594c 100644 --- a/src/crewai/cli/cli.py +++ b/src/crewai/cli/cli.py @@ -1,6 +1,5 @@ -import os from importlib.metadata import version as get_version -from typing import Optional, Tuple +from typing import Optional import click @@ -138,12 +137,8 @@ def log_tasks_outputs() -> None: @click.option("-s", "--short", is_flag=True, help="Reset SHORT TERM memory") @click.option("-e", "--entities", is_flag=True, help="Reset ENTITIES memory") @click.option("-kn", "--knowledge", is_flag=True, help="Reset KNOWLEDGE storage") -@click.option( - "-k", - "--kickoff-outputs", - is_flag=True, - help="Reset LATEST KICKOFF TASK OUTPUTS", -) +@click.option("-akn", "--agent-knowledge", is_flag=True, help="Reset AGENT KNOWLEDGE storage") +@click.option("-k","--kickoff-outputs",is_flag=True,help="Reset LATEST KICKOFF TASK OUTPUTS") @click.option("-a", "--all", is_flag=True, help="Reset ALL memories") def reset_memories( long: bool, @@ -151,18 +146,20 @@ def reset_memories( entities: bool, knowledge: bool, kickoff_outputs: bool, + agent_knowledge: bool, all: bool, ) -> None: """ - Reset the crew memories (long, short, entity, latest_crew_kickoff_ouputs). This will delete all the data saved. + Reset the crew memories (long, short, entity, latest_crew_kickoff_ouputs, knowledge, agent_knowledge). This will delete all the data saved. """ try: - if not all and not (long or short or entities or knowledge or kickoff_outputs): + memory_types = [long, short, entities, knowledge, agent_knowledge, kickoff_outputs, all] + if not any(memory_types): click.echo( "Please specify at least one memory type to reset using the appropriate flags." ) return - reset_memories_command(long, short, entities, knowledge, kickoff_outputs, all) + reset_memories_command(long, short, entities, knowledge, agent_knowledge, kickoff_outputs, all) except Exception as e: click.echo(f"An error occurred while resetting memories: {e}", err=True) diff --git a/src/crewai/cli/reset_memories_command.py b/src/crewai/cli/reset_memories_command.py index eaf54ffb7..d8910f735 100644 --- a/src/crewai/cli/reset_memories_command.py +++ b/src/crewai/cli/reset_memories_command.py @@ -10,6 +10,7 @@ def reset_memories_command( short, entity, knowledge, + agent_knowledge, kickoff_outputs, all, ) -> None: @@ -23,10 +24,11 @@ def reset_memories_command( kickoff_outputs (bool): Whether to reset the latest kickoff task outputs. all (bool): Whether to reset all memories. knowledge (bool): Whether to reset the knowledge. + agent_knowledge (bool): Whether to reset the agents knowledge. """ try: - if not any([long, short, entity, kickoff_outputs, knowledge, all]): + if not any([long, short, entity, kickoff_outputs, knowledge, agent_knowledge, all]): click.echo( "No memory type specified. Please specify at least one type to reset." ) @@ -67,6 +69,11 @@ def reset_memories_command( click.echo( f"[Crew ({crew.name if crew.name else crew.id})] Knowledge has been reset." ) + if agent_knowledge: + crew.reset_memories(command_type="agent_knowledge") + click.echo( + f"[Crew ({crew.name if crew.name else crew.id})] Agents knowledge has been reset." + ) except subprocess.CalledProcessError as e: click.echo(f"An error occurred while resetting the memories: {e}", err=True) diff --git a/src/crewai/cli/templates/crew/pyproject.toml b/src/crewai/cli/templates/crew/pyproject.toml index d657f1c46..3f5166bfc 100644 --- a/src/crewai/cli/templates/crew/pyproject.toml +++ b/src/crewai/cli/templates/crew/pyproject.toml @@ -5,7 +5,7 @@ description = "{{name}} using crewAI" authors = [{ name = "Your Name", email = "you@example.com" }] requires-python = ">=3.10,<3.13" dependencies = [ - "crewai[tools]>=0.119.0,<1.0.0" + "crewai[tools]>=0.120.1,<1.0.0" ] [project.scripts] diff --git a/src/crewai/cli/templates/flow/pyproject.toml b/src/crewai/cli/templates/flow/pyproject.toml index 0a74a378c..5a86b4e6f 100644 --- a/src/crewai/cli/templates/flow/pyproject.toml +++ b/src/crewai/cli/templates/flow/pyproject.toml @@ -5,7 +5,7 @@ description = "{{name}} using crewAI" authors = [{ name = "Your Name", email = "you@example.com" }] requires-python = ">=3.10,<3.13" dependencies = [ - "crewai[tools]>=0.119.0,<1.0.0", + "crewai[tools]>=0.120.1,<1.0.0", ] [project.scripts] diff --git a/src/crewai/cli/templates/tool/pyproject.toml b/src/crewai/cli/templates/tool/pyproject.toml index 72463bc48..192b9b314 100644 --- a/src/crewai/cli/templates/tool/pyproject.toml +++ b/src/crewai/cli/templates/tool/pyproject.toml @@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}" readme = "README.md" requires-python = ">=3.10,<3.13" dependencies = [ - "crewai[tools]>=0.119.0" + "crewai[tools]>=0.120.1" ] [tool.crewai] diff --git a/src/crewai/crew.py b/src/crewai/crew.py index a0158f646..0164552c5 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -1356,7 +1356,7 @@ class Crew(FlowTrackable, BaseModel): Args: command_type: Type of memory to reset. - Valid options: 'long', 'short', 'entity', 'knowledge', + Valid options: 'long', 'short', 'entity', 'knowledge', 'agent_knowledge' 'kickoff_outputs', or 'all' Raises: @@ -1369,6 +1369,7 @@ class Crew(FlowTrackable, BaseModel): "short", "entity", "knowledge", + "agent_knowledge", "kickoff_outputs", "all", "external", @@ -1393,19 +1394,14 @@ class Crew(FlowTrackable, BaseModel): def _reset_all_memories(self) -> None: """Reset all available memory systems.""" - memory_systems = [ - ("short term", getattr(self, "_short_term_memory", None)), - ("entity", getattr(self, "_entity_memory", None)), - ("external", getattr(self, "_external_memory", None)), - ("long term", getattr(self, "_long_term_memory", None)), - ("task output", getattr(self, "_task_output_handler", None)), - ("knowledge", getattr(self, "knowledge", None)), - ] + memory_systems = self._get_memory_systems() - for name, system in memory_systems: - if system is not None: + for memory_type, config in memory_systems.items(): + if (system := config.get('system')) is not None: + name = config.get('name') try: - system.reset() + reset_fn: Callable = cast(Callable, config.get('reset')) + reset_fn(system) self._logger.log( "info", f"[Crew ({self.name if self.name else self.id})] {name} memory has been reset", @@ -1424,24 +1420,17 @@ class Crew(FlowTrackable, BaseModel): Raises: RuntimeError: If the specified memory system fails to reset """ - reset_functions = { - "long": (getattr(self, "_long_term_memory", None), "long term"), - "short": (getattr(self, "_short_term_memory", None), "short term"), - "entity": (getattr(self, "_entity_memory", None), "entity"), - "knowledge": (getattr(self, "knowledge", None), "knowledge"), - "kickoff_outputs": ( - getattr(self, "_task_output_handler", None), - "task output", - ), - "external": (getattr(self, "_external_memory", None), "external"), - } + memory_systems = self._get_memory_systems() + config = memory_systems[memory_type] + system = config.get('system') + name = config.get('name') - memory_system, name = reset_functions[memory_type] - if memory_system is None: + if system is None: raise RuntimeError(f"{name} memory system is not initialized") - + try: - memory_system.reset() + reset_fn: Callable = cast(Callable, config.get('reset')) + reset_fn(system) self._logger.log( "info", f"[Crew ({self.name if self.name else self.id})] {name} memory has been reset", @@ -1450,3 +1439,64 @@ class Crew(FlowTrackable, BaseModel): raise RuntimeError( f"[Crew ({self.name if self.name else self.id})] Failed to reset {name} memory: {str(e)}" ) from e + + def _get_memory_systems(self): + """Get all available memory systems with their configuration. + + Returns: + Dict containing all memory systems with their reset functions and display names. + """ + def default_reset(memory): + return memory.reset() + def knowledge_reset(memory): + return self.reset_knowledge(memory) + + # Get knowledge for agents + agent_knowledges = [getattr(agent, "knowledge", None) for agent in self.agents + if getattr(agent, "knowledge", None) is not None] + # Get knowledge for crew and agents + crew_knowledge = getattr(self, "knowledge", None) + crew_and_agent_knowledges = ([crew_knowledge] if crew_knowledge is not None else []) + agent_knowledges + + return { + 'short': { + 'system': getattr(self, "_short_term_memory", None), + 'reset': default_reset, + 'name': 'Short Term' + }, + 'entity': { + 'system': getattr(self, "_entity_memory", None), + 'reset': default_reset, + 'name': 'Entity' + }, + 'external': { + 'system': getattr(self, "_external_memory", None), + 'reset': default_reset, + 'name': 'External' + }, + 'long': { + 'system': getattr(self, "_long_term_memory", None), + 'reset': default_reset, + 'name': 'Long Term' + }, + 'kickoff_outputs': { + 'system': getattr(self, "_task_output_handler", None), + 'reset': default_reset, + 'name': 'Task Output' + }, + 'knowledge': { + 'system': crew_and_agent_knowledges if crew_and_agent_knowledges else None, + 'reset': knowledge_reset, + 'name': 'Crew Knowledge and Agent Knowledge' + }, + 'agent_knowledge': { + 'system': agent_knowledges if agent_knowledges else None, + 'reset': knowledge_reset, + 'name': 'Agent Knowledge' + } + } + + def reset_knowledge(self, knowledges: List[Knowledge]) -> None: + """Reset crew and agent knowledge storage.""" + for ks in knowledges: + ks.reset() diff --git a/src/crewai/utilities/string_utils.py b/src/crewai/utilities/string_utils.py index 9a1857781..255e66a0b 100644 --- a/src/crewai/utilities/string_utils.py +++ b/src/crewai/utilities/string_utils.py @@ -59,7 +59,7 @@ def interpolate_only( # The regex pattern to find valid variable placeholders # Matches {variable_name} where variable_name starts with a letter/underscore # and contains only letters, numbers, and underscores - pattern = r"\{([A-Za-z_][A-Za-z0-9_]*)\}" + pattern = r"\{([A-Za-z_][A-Za-z0-9_\-]*)\}" # Find all matching variables in the input string variables = re.findall(pattern, input_string) diff --git a/tests/cassettes/test_task_interpolation_with_hyphens.yaml b/tests/cassettes/test_task_interpolation_with_hyphens.yaml new file mode 100644 index 000000000..f0f1e87c2 --- /dev/null +++ b/tests/cassettes/test_task_interpolation_with_hyphens.yaml @@ -0,0 +1,121 @@ +interactions: +- request: + body: '{"messages": [{"role": "system", "content": "You are Researcher. You''re + an expert researcher, specialized in technology, software engineering, AI and + startups. You work as a freelancer and is now working on doing research and + analysis for a new customer.\nYour personal goal is: be an assistant that responds + with say hello world\nTo give my best complete final answer to the task respond + using the exact following format:\n\nThought: I now can give a great answer\nFinal + Answer: Your final answer must be the great and the most complete as possible, + it must be outcome described.\n\nI MUST use these formats, my job depends on + it!"}, {"role": "user", "content": "\nCurrent Task: be an assistant that responds + with say hello world\n\nThis is the expected criteria for your final answer: + The response should be addressing: say hello world\nyou MUST return the actual + complete content as the final answer, not a summary.\n\nBegin! This is VERY + important to you, use the tools available and give your best Final Answer, your + job depends on it!\n\nThought:"}], "model": "gpt-4o-mini", "stop": ["\nObservation:"]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '1108' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.68.2 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.68.2 + x-stainless-raw-response: + - 'true' + x-stainless-read-timeout: + - '600.0' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: !!binary | + H4sIAAAAAAAAA4xSTW/UMBC951cMPicoScMu3RuIooUDcOOrVeS1J4mp4zG2sy2q9r9XTrqbtBSJ + iyX7zXt+b2buEgCmJNsAEx0Porc6e/vt4nf3xVxweVZ+3v/Q17fF9+pjs92+O//0iqWRQbtfKMKR + 9VJQbzUGRWaChUMeMKoW62pdropVfjYCPUnUkdbakFWU9cqorMzLKsvXWfH6gd2REujZBn4mAAB3 + 4xl9Gom3bAN5enzp0XveItucigCYIx1fGPde+cBNYOkMCjIBzWj9Axi6AcENtGqPwKGNtoEbf4MO + 4NK8V4ZreDPeN7BFrSmFr+S0fLGUdNgMnsdYZtB6AXBjKPDYljHM1QNyONnX1FpHO/+EyhpllO9q + h9yTiVZ9IMtG9JAAXI1tGh4lZ9ZRb0Md6BrH78p8NemxeTozWhzBQIHrBass02f0aomBK+0XjWaC + iw7lTJ2nwgepaAEki9R/u3lOe0quTPs/8jMgBNqAsrYOpRKPE89lDuPy/qvs1OXRMPPo9kpgHRS6 + OAmJDR/0tFLM//EB+7pRpkVnnZr2qrH1utjl5bo6bzhLDsk9AAAA//8DAAxaM/dlAwAA + headers: + CF-RAY: + - 93fdd19cdbfb6428-SJC + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 14 May 2025 22:26:43 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=eCtOgOCsKt_ybdNPdtFAocCmuQbNltR52chaHVe7Y_Q-1747261603-1.0.1.1-827eoA7wHS5SOkTsTqoMq6OSioi0VznQBVjvmabNSVX1bf5PpWZvblw58iggZ_wyKDB0EuVoeLKFspgBJa0kuQYR17hu43Y2C14sgdvOXIE; + path=/; expires=Wed, 14-May-25 22:56:43 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=QUa5MnypdaVxO826bwdQaN4G6CBEV8HYVV.7OLF.qvQ-1747261603742-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + 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: + - '307' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '309' + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999757' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_61d9066e0258b7095517f9f9c01d38e9 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cli/cli_test.py b/tests/cli/cli_test.py index 19439cb82..0ce747637 100644 --- a/tests/cli/cli_test.py +++ b/tests/cli/cli_test.py @@ -162,8 +162,18 @@ def test_reset_knowledge(mock_get_crews, runner): assert call_count == 1, "reset_memories should have been called once" -def test_reset_memory_from_many_crews(mock_get_crews, runner): +def test_reset_agent_knowledge(mock_get_crews, runner): + result = runner.invoke(reset_memories, ["--agent-knowledge"]) + call_count = 0 + for crew in mock_get_crews.return_value: + crew.reset_memories.assert_called_once_with(command_type="agent_knowledge") + assert f"[Crew ({crew.name})] Agents knowledge has been reset." in result.output + call_count += 1 + assert call_count == 1, "reset_memories should have been called once" + + +def test_reset_memory_from_many_crews(mock_get_crews, runner): crews = [] for crew_id in ["id-1234", "id-5678"]: mock_crew = mock.Mock(spec=Crew) diff --git a/tests/crew_test.py b/tests/crew_test.py index 7c242c825..62b934883 100644 --- a/tests/crew_test.py +++ b/tests/crew_test.py @@ -14,6 +14,7 @@ from crewai.agents import CacheHandler from crewai.crew import Crew from crewai.crews.crew_output import CrewOutput from crewai.flow import Flow, start +from crewai.knowledge.knowledge import Knowledge from crewai.knowledge.source.string_knowledge_source import StringKnowledgeSource from crewai.llm import LLM from crewai.memory.contextual.contextual_memory import ContextualMemory @@ -4403,3 +4404,165 @@ def test_sets_parent_flow_when_inside_flow(researcher, writer): flow = MyFlow() result = flow.kickoff() assert result.parent_flow is flow + + +def test_reset_knowledge_with_no_crew_knowledge(researcher,writer): + crew = Crew( + agents=[researcher, writer], + process=Process.sequential, + tasks=[ + Task(description="Task 1", expected_output="output", agent=researcher), + Task(description="Task 2", expected_output="output", agent=writer), + ] + ) + + with pytest.raises(RuntimeError) as excinfo: + crew.reset_memories(command_type='knowledge') + + # Optionally, you can also check the error message + assert "Crew Knowledge and Agent Knowledge memory system is not initialized" in str(excinfo.value) # Replace with the expected message + + +def test_reset_knowledge_with_only_crew_knowledge(researcher,writer): + mock_ks = MagicMock(spec=Knowledge) + + with patch.object(Crew,'reset_knowledge') as mock_reset_agent_knowledge: + crew = Crew( + agents=[researcher, writer], + process=Process.sequential, + tasks=[ + Task(description="Task 1", expected_output="output", agent=researcher), + Task(description="Task 2", expected_output="output", agent=writer), + ], + knowledge=mock_ks + ) + + crew.reset_memories(command_type='knowledge') + mock_reset_agent_knowledge.assert_called_once_with([mock_ks]) + + +def test_reset_knowledge_with_crew_and_agent_knowledge(researcher,writer): + mock_ks_crew = MagicMock(spec=Knowledge) + mock_ks_research = MagicMock(spec=Knowledge) + mock_ks_writer = MagicMock(spec=Knowledge) + + researcher.knowledge = mock_ks_research + writer.knowledge = mock_ks_writer + + with patch.object(Crew,'reset_knowledge') as mock_reset_agent_knowledge: + crew = Crew( + agents=[researcher, writer], + process=Process.sequential, + tasks=[ + Task(description="Task 1", expected_output="output", agent=researcher), + Task(description="Task 2", expected_output="output", agent=writer), + ], + knowledge=mock_ks_crew + ) + + crew.reset_memories(command_type='knowledge') + mock_reset_agent_knowledge.assert_called_once_with([mock_ks_crew,mock_ks_research,mock_ks_writer]) + + +def test_reset_knowledge_with_only_agent_knowledge(researcher,writer): + mock_ks_research = MagicMock(spec=Knowledge) + mock_ks_writer = MagicMock(spec=Knowledge) + + researcher.knowledge = mock_ks_research + writer.knowledge = mock_ks_writer + + with patch.object(Crew,'reset_knowledge') as mock_reset_agent_knowledge: + crew = Crew( + agents=[researcher, writer], + process=Process.sequential, + tasks=[ + Task(description="Task 1", expected_output="output", agent=researcher), + Task(description="Task 2", expected_output="output", agent=writer), + ], + ) + + crew.reset_memories(command_type='knowledge') + mock_reset_agent_knowledge.assert_called_once_with([mock_ks_research,mock_ks_writer]) + + +def test_reset_agent_knowledge_with_no_agent_knowledge(researcher,writer): + crew = Crew( + agents=[researcher, writer], + process=Process.sequential, + tasks=[ + Task(description="Task 1", expected_output="output", agent=researcher), + Task(description="Task 2", expected_output="output", agent=writer), + ], + ) + + with pytest.raises(RuntimeError) as excinfo: + crew.reset_memories(command_type='agent_knowledge') + + # Optionally, you can also check the error message + assert "Agent Knowledge memory system is not initialized" in str(excinfo.value) # Replace with the expected message + + +def test_reset_agent_knowledge_with_only_crew_knowledge(researcher,writer): + mock_ks = MagicMock(spec=Knowledge) + + crew = Crew( + agents=[researcher, writer], + process=Process.sequential, + tasks=[ + Task(description="Task 1", expected_output="output", agent=researcher), + Task(description="Task 2", expected_output="output", agent=writer), + ], + knowledge=mock_ks + ) + + with pytest.raises(RuntimeError) as excinfo: + crew.reset_memories(command_type='agent_knowledge') + + # Optionally, you can also check the error message + assert "Agent Knowledge memory system is not initialized" in str(excinfo.value) # Replace with the expected message + + +def test_reset_agent_knowledge_with_crew_and_agent_knowledge(researcher,writer): + mock_ks_crew = MagicMock(spec=Knowledge) + mock_ks_research = MagicMock(spec=Knowledge) + mock_ks_writer = MagicMock(spec=Knowledge) + + researcher.knowledge = mock_ks_research + writer.knowledge = mock_ks_writer + + with patch.object(Crew,'reset_knowledge') as mock_reset_agent_knowledge: + crew = Crew( + agents=[researcher, writer], + process=Process.sequential, + tasks=[ + Task(description="Task 1", expected_output="output", agent=researcher), + Task(description="Task 2", expected_output="output", agent=writer), + ], + knowledge=mock_ks_crew + ) + + crew.reset_memories(command_type='agent_knowledge') + mock_reset_agent_knowledge.assert_called_once_with([mock_ks_research,mock_ks_writer]) + + +def test_reset_agent_knowledge_with_only_agent_knowledge(researcher,writer): + mock_ks_research = MagicMock(spec=Knowledge) + mock_ks_writer = MagicMock(spec=Knowledge) + + researcher.knowledge = mock_ks_research + writer.knowledge = mock_ks_writer + + with patch.object(Crew,'reset_knowledge') as mock_reset_agent_knowledge: + crew = Crew( + agents=[researcher, writer], + process=Process.sequential, + tasks=[ + Task(description="Task 1", expected_output="output", agent=researcher), + Task(description="Task 2", expected_output="output", agent=writer), + ], + ) + + crew.reset_memories(command_type='agent_knowledge') + mock_reset_agent_knowledge.assert_called_once_with([mock_ks_research,mock_ks_writer]) + + diff --git a/tests/task_test.py b/tests/task_test.py index b09f69646..42d709a4f 100644 --- a/tests/task_test.py +++ b/tests/task_test.py @@ -837,9 +837,6 @@ def test_interpolate_inputs(): def test_interpolate_only(): """Test the interpolate_only method for various scenarios including JSON structure preservation.""" - task = Task( - description="Unused in this test", expected_output="Unused in this test" - ) # Test JSON structure preservation json_string = '{"info": "Look at {placeholder}", "nested": {"val": "{nestedVal}"}}' @@ -871,10 +868,6 @@ def test_interpolate_only(): def test_interpolate_only_with_dict_inside_expected_output(): """Test the interpolate_only method for various scenarios including JSON structure preservation.""" - task = Task( - description="Unused in this test", - expected_output="Unused in this test: {questions}", - ) json_string = '{"questions": {"main_question": "What is the user\'s name?", "secondary_question": "What is the user\'s age?"}}' result = interpolate_only( @@ -1094,11 +1087,6 @@ def test_task_execution_times(): def test_interpolate_with_list_of_strings(): - task = Task( - description="Test list interpolation", - expected_output="List: {items}", - ) - # Test simple list of strings input_str = "Available items: {items}" inputs = {"items": ["apple", "banana", "cherry"]} @@ -1112,11 +1100,6 @@ def test_interpolate_with_list_of_strings(): def test_interpolate_with_list_of_dicts(): - task = Task( - description="Test list of dicts interpolation", - expected_output="People: {people}", - ) - input_data = { "people": [ {"name": "Alice", "age": 30, "skills": ["Python", "AI"]}, @@ -1137,11 +1120,6 @@ def test_interpolate_with_list_of_dicts(): def test_interpolate_with_nested_structures(): - task = Task( - description="Test nested structures", - expected_output="Company: {company}", - ) - input_data = { "company": { "name": "TechCorp", @@ -1165,11 +1143,6 @@ def test_interpolate_with_nested_structures(): def test_interpolate_with_special_characters(): - task = Task( - description="Test special characters in dicts", - expected_output="Data: {special_data}", - ) - input_data = { "special_data": { "quotes": """This has "double" and 'single' quotes""", @@ -1188,11 +1161,6 @@ def test_interpolate_with_special_characters(): def test_interpolate_mixed_types(): - task = Task( - description="Test mixed type interpolation", - expected_output="Mixed: {data}", - ) - input_data = { "data": { "name": "Test Dataset", @@ -1214,11 +1182,6 @@ def test_interpolate_mixed_types(): def test_interpolate_complex_combination(): - task = Task( - description="Test complex combination", - expected_output="Report: {report}", - ) - input_data = { "report": [ { @@ -1243,11 +1206,6 @@ def test_interpolate_complex_combination(): def test_interpolate_invalid_type_validation(): - task = Task( - description="Test invalid type validation", - expected_output="Should never reach here", - ) - # Test with invalid top-level type with pytest.raises(ValueError) as excinfo: interpolate_only("{data}", {"data": set()}) # type: ignore we are purposely testing this failure @@ -1268,11 +1226,6 @@ def test_interpolate_invalid_type_validation(): def test_interpolate_custom_object_validation(): - task = Task( - description="Test custom object rejection", - expected_output="Should never reach here", - ) - class CustomObject: def __init__(self, value): self.value = value @@ -1304,11 +1257,6 @@ def test_interpolate_custom_object_validation(): def test_interpolate_valid_complex_types(): - task = Task( - description="Test valid complex types", - expected_output="Validation should pass", - ) - # Valid complex structure valid_data = { "name": "Valid Dataset", @@ -1328,11 +1276,6 @@ def test_interpolate_valid_complex_types(): def test_interpolate_edge_cases(): - task = Task( - description="Test edge cases", - expected_output="Edge case handling", - ) - # Test empty dict and list assert interpolate_only("{}", {"data": {}}) == "{}" assert interpolate_only("[]", {"data": []}) == "[]" @@ -1347,11 +1290,6 @@ def test_interpolate_edge_cases(): def test_interpolate_valid_types(): - task = Task( - description="Test valid types including null and boolean", - expected_output="Should pass validation", - ) - # Test with boolean and null values (valid JSON types) valid_data = { "name": "Test", @@ -1373,11 +1311,11 @@ def test_interpolate_valid_types(): def test_task_with_no_max_execution_time(): researcher = Agent( - role="Researcher", - goal="Make the best research and analysis on content about AI and AI agents", - backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.", - allow_delegation=False, - max_execution_time=None + role="Researcher", + goal="Make the best research and analysis on content about AI and AI agents", + backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.", + allow_delegation=False, + max_execution_time=None, ) task = Task( @@ -1386,7 +1324,7 @@ def test_task_with_no_max_execution_time(): agent=researcher, ) - with patch.object(Agent, "_execute_without_timeout", return_value = "ok") as execute: + with patch.object(Agent, "_execute_without_timeout", return_value="ok") as execute: result = task.execute_sync(agent=researcher) assert result.raw == "ok" execute.assert_called_once() @@ -1395,6 +1333,7 @@ def test_task_with_no_max_execution_time(): @pytest.mark.vcr(filter_headers=["authorization"]) def test_task_with_max_execution_time(): from crewai.tools import tool + """Test that execution raises TimeoutError when max_execution_time is exceeded.""" @tool("what amazing tool", result_as_answer=True) @@ -1412,7 +1351,7 @@ def test_task_with_max_execution_time(): ), allow_delegation=False, tools=[my_tool], - max_execution_time=4 + max_execution_time=4, ) task = Task( @@ -1428,6 +1367,7 @@ def test_task_with_max_execution_time(): @pytest.mark.vcr(filter_headers=["authorization"]) def test_task_with_max_execution_time_exceeded(): from crewai.tools import tool + """Test that execution raises TimeoutError when max_execution_time is exceeded.""" @tool("what amazing tool", result_as_answer=True) @@ -1445,7 +1385,7 @@ def test_task_with_max_execution_time_exceeded(): ), allow_delegation=False, tools=[my_tool], - max_execution_time=1 + max_execution_time=1, ) task = Task( @@ -1455,4 +1395,28 @@ def test_task_with_max_execution_time_exceeded(): ) with pytest.raises(TimeoutError): - task.execute_sync(agent=researcher) \ No newline at end of file + task.execute_sync(agent=researcher) + + +@pytest.mark.vcr(filter_headers=["authorization"]) +def test_task_interpolation_with_hyphens(): + agent = Agent( + role="Researcher", + goal="be an assistant that responds with {interpolation-with-hyphens}", + backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.", + allow_delegation=False, + ) + task = Task( + description="be an assistant that responds with {interpolation-with-hyphens}", + expected_output="The response should be addressing: {interpolation-with-hyphens}", + agent=agent, + ) + crew = Crew( + agents=[agent], + tasks=[task], + verbose=True, + ) + result = crew.kickoff(inputs={"interpolation-with-hyphens": "say hello world"}) + assert "say hello world" in task.prompt() + + assert result.raw == "Hello, World!" diff --git a/uv.lock b/uv.lock index f3cb68ceb..987fc993d 100644 --- a/uv.lock +++ b/uv.lock @@ -738,7 +738,7 @@ wheels = [ [[package]] name = "crewai" -version = "0.119.0" +version = "0.120.1" source = { editable = "." } dependencies = [ { name = "appdirs" }, @@ -828,7 +828,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.44.0" }, + { name = "crewai-tools", marker = "extra == 'tools'", specifier = "~=0.45.0" }, { name = "docling", marker = "extra == 'docling'", specifier = ">=2.12.0" }, { name = "fastembed", marker = "extra == 'fastembed'", specifier = ">=0.4.1" }, { name = "instructor", specifier = ">=1.3.3" }, @@ -879,7 +879,7 @@ dev = [ [[package]] name = "crewai-tools" -version = "0.44.0" +version = "0.45.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "chromadb" }, @@ -894,9 +894,9 @@ dependencies = [ { name = "pytube" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b8/1f/2977dc72628c1225bf5788ae22a65e5a53df384d19b197646d2c4760684e/crewai_tools-0.44.0.tar.gz", hash = "sha256:44e0c26079396503a326efdd9ff34bf369d410cbf95c362cc523db65b18f3c3a", size = 892004 } +sdist = { url = "https://files.pythonhosted.org/packages/e9/3a/7070dcacef56702c5d83ad1a87021b1666ff1850ff80b3aa7540892406e7/crewai_tools-0.45.0.tar.gz", hash = "sha256:1b2e4eff3f928ce5fac308d6e648719a0e4718a1228ae98980aa0d74fc16bfc7", size = 909723 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/80/b91aa837d06edbb472445ea3c92d7619518894fd3049d480e5fffbf0c21b/crewai_tools-0.44.0-py3-none-any.whl", hash = "sha256:119e2365fe66ee16e18a5e8e222994b19f76bafcc8c1bb87f61609c1e39b2463", size = 583462 }, + { url = "https://files.pythonhosted.org/packages/6e/72/db45626973027c992df75cbc7ef391f18393d631be3bceb6388c1b9f01e1/crewai_tools-0.45.0-py3-none-any.whl", hash = "sha256:9dd34e4792c075ee7a72134aedaab268e78d0e350114fd7fe2426e691c5f52a3", size = 602659 }, ] [[package]]