diff --git a/docs/core-concepts/Crews.md b/docs/core-concepts/Crews.md index 6837eea59..c60566ed0 100644 --- a/docs/core-concepts/Crews.md +++ b/docs/core-concepts/Crews.md @@ -27,6 +27,8 @@ A crew in crewAI represents a collaborative group of agents working together to | **Step Callback** *(optional)* | A function that is called after each step of every agent. This can be used to log the agent's actions or to perform other operations; it won't override the agent-specific `step_callback`. | | **Task Callback** *(optional)* | A function that is called after the completion of each task. Useful for monitoring or additional operations post-task execution. | | **Share Crew** *(optional)* | Whether you want to share the complete crew information and execution with the crewAI team to make the library better, and allow us to train models. | +| **Output Log File** *(optional)* | Whether you want to have a file with the complete crew output and execution. You can set it using True and it will default to the folder you are currently and it will be called logs.txt or passing a string with the full path and name of the file. | + !!! note "Crew Max RPM" The `max_rpm` attribute sets the maximum number of requests per minute the crew can perform to avoid rate limits and will override individual agents' `max_rpm` settings if you set it. diff --git a/docs/core-concepts/Tasks.md b/docs/core-concepts/Tasks.md index 7ab78faf8..b04465bc2 100644 --- a/docs/core-concepts/Tasks.md +++ b/docs/core-concepts/Tasks.md @@ -96,26 +96,26 @@ This is useful when you have a task that depends on the output of another task t # ... research_ai_task = Task( - description='Find and summarize the latest AI news', - expected_output='A bullet list summary of the top 5 most important AI news', - async_execution=True, - agent=research_agent, - tools=[search_tool] + description='Find and summarize the latest AI news', + expected_output='A bullet list summary of the top 5 most important AI news', + async_execution=True, + agent=research_agent, + tools=[search_tool] ) research_ops_task = Task( - description='Find and summarize the latest AI Ops news', - expected_output='A bullet list summary of the top 5 most important AI Ops news', - async_execution=True, - agent=research_agent, - tools=[search_tool] + description='Find and summarize the latest AI Ops news', + expected_output='A bullet list summary of the top 5 most important AI Ops news', + async_execution=True, + agent=research_agent, + tools=[search_tool] ) write_blog_task = Task( - description="Write a full blog post about the importance of AI and its latest news", - expected_output='Full blog post that is 4 paragraphs long', - agent=writer_agent, - context=[research_ai_task, research_ops_task] + description="Write a full blog post about the importance of AI and its latest news", + expected_output='Full blog post that is 4 paragraphs long', + agent=writer_agent, + context=[research_ai_task, research_ops_task] ) #... @@ -131,24 +131,24 @@ You can then use the `context` attribute to define in a future task that it shou #... list_ideas = Task( - description="List of 5 interesting ideas to explore for an article about AI.", - expected_output="Bullet point list of 5 ideas for an article.", - agent=researcher, - async_execution=True # Will be executed asynchronously + description="List of 5 interesting ideas to explore for an article about AI.", + expected_output="Bullet point list of 5 ideas for an article.", + agent=researcher, + async_execution=True # Will be executed asynchronously ) list_important_history = Task( - description="Research the history of AI and give me the 5 most important events.", - expected_output="Bullet point list of 5 important events.", - agent=researcher, - async_execution=True # Will be executed asynchronously + description="Research the history of AI and give me the 5 most important events.", + expected_output="Bullet point list of 5 important events.", + agent=researcher, + async_execution=True # Will be executed asynchronously ) write_article = Task( - description="Write an article about AI, its history, and interesting ideas.", - expected_output="A 4 paragraph article about AI.", - agent=writer, - context=[list_ideas, list_important_history] # Will wait for the output of the two tasks to be completed + description="Write an article about AI, its history, and interesting ideas.", + expected_output="A 4 paragraph article about AI.", + agent=writer, + context=[list_ideas, list_important_history] # Will wait for the output of the two tasks to be completed ) #... @@ -162,20 +162,20 @@ The callback function is executed after the task is completed, allowing for acti # ... def callback_function(output: TaskOutput): - # Do something after the task is completed - # Example: Send an email to the manager - print(f""" - Task completed! - Task: {output.description} - Output: {output.raw_output} - """) + # Do something after the task is completed + # Example: Send an email to the manager + print(f""" + Task completed! + Task: {output.description} + Output: {output.raw_output} + """) research_task = Task( - description='Find and summarize the latest AI news', - expected_output='A bullet list summary of the top 5 most important AI news', - agent=research_agent, - tools=[search_tool], - callback=callback_function + description='Find and summarize the latest AI news', + expected_output='A bullet list summary of the top 5 most important AI news', + agent=research_agent, + tools=[search_tool], + callback=callback_function ) #... @@ -188,27 +188,27 @@ Once a crew finishes running, you can access the output of a specific task by us ```python # ... task1 = Task( - description='Find and summarize the latest AI news', - expected_output='A bullet list summary of the top 5 most important AI news', - agent=research_agent, - tools=[search_tool] + description='Find and summarize the latest AI news', + expected_output='A bullet list summary of the top 5 most important AI news', + agent=research_agent, + tools=[search_tool] ) #... crew = Crew( - agents=[research_agent], - tasks=[task1, task2, task3], - verbose=2 + agents=[research_agent], + tasks=[task1, task2, task3], + verbose=2 ) result = crew.kickoff() # Returns a TaskOutput object with the description and results of the task print(f""" - Task completed! - Task: {task1.output.description} - Output: {task1.output.raw_output} + Task completed! + Task: {task1.output.description} + Output: {task1.output.raw_output} """) ``` diff --git a/docs/tools/GitHubSearchTool.md b/docs/tools/GitHubSearchTool.md index 6751da598..707e4c86c 100644 --- a/docs/tools/GitHubSearchTool.md +++ b/docs/tools/GitHubSearchTool.md @@ -22,15 +22,15 @@ from crewai_tools import GithubSearchTool # Initialize the tool for semantic searches within a specific GitHub repository tool = GithubSearchTool( - github_repo='https://github.com/example/repo', - content_types=['code', 'issue'] # Options: code, repo, pr, issue + github_repo='https://github.com/example/repo', + content_types=['code', 'issue'] # Options: code, repo, pr, issue ) # OR # Initialize the tool for semantic searches within a specific GitHub repository, so the agent can search any repository if it learns about during its execution tool = GithubSearchTool( - content_types=['code', 'issue'] # Options: code, repo, pr, issue + content_types=['code', 'issue'] # Options: code, repo, pr, issue ) ``` diff --git a/poetry.lock b/poetry.lock index f89e2e1f2..1012dba2b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "agentops" diff --git a/src/crewai/crew.py b/src/crewai/crew.py index 081e9037c..a7c6f3db1 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -25,12 +25,13 @@ from crewai.process import Process from crewai.task import Task from crewai.telemetry import Telemetry from crewai.tools.agent_tools import AgentTools -from crewai.utilities import I18N, Logger, RPMController +from crewai.utilities import I18N, Logger, RPMController, FileHandler try: import agentops except ImportError: agentops = None + class Crew(BaseModel): """ Represents a group of agents, defining how they should collaborate and the tasks they should perform. @@ -58,6 +59,7 @@ class Crew(BaseModel): _execution_span: Any = PrivateAttr() _rpm_controller: RPMController = PrivateAttr() _logger: Logger = PrivateAttr() + _file_handler: FileHandler = PrivateAttr() _cache_handler: InstanceOf[CacheHandler] = PrivateAttr(default=CacheHandler()) _short_term_memory: Optional[InstanceOf[ShortTermMemory]] = PrivateAttr() _long_term_memory: Optional[InstanceOf[LongTermMemory]] = PrivateAttr() @@ -118,6 +120,10 @@ class Crew(BaseModel): default=None, description="Path to the language file to be used for the crew.", ) + output_log_file: Optional[Union[bool, str]] = Field( + default=False, + description="output_log_file", + ) @field_validator("id", mode="before") @classmethod @@ -148,6 +154,8 @@ class Crew(BaseModel): """Set private attributes.""" self._cache_handler = CacheHandler() self._logger = Logger(self.verbose) + if self.output_log_file: + self._file_handler = FileHandler(self.output_log_file) self._rpm_controller = RPMController(max_rpm=self.max_rpm, logger=self._logger) self._telemetry = Telemetry() self._telemetry.set_tracer() @@ -284,6 +292,11 @@ class Crew(BaseModel): "info", f"== Starting Task: {task.description}", color="bold_purple" ) + if self.output_log_file: + self._file_handler.log( + agent=role, task=task.description, status="started" + ) + output = task.execute(context=task_output) if not task.async_execution: task_output = output @@ -291,6 +304,9 @@ class Crew(BaseModel): role = task.agent.role if task.agent is not None else "None" self._logger.log("debug", f"== [{role}] Task output: {task_output}\n\n") + if self.output_log_file: + self._file_handler.log(agent=role, task=task_output, status="completed") + self._finish_execution(task_output) return self._format_output(task_output) @@ -312,12 +328,22 @@ class Crew(BaseModel): self._logger.log("debug", f"Working Agent: {manager.role}") self._logger.log("info", f"Starting Task: {task.description}") + if self.output_log_file: + self._file_handler.log( + agent=manager.role, task=task.description, status="started" + ) + task_output = task.execute( agent=manager, context=task_output, tools=manager.tools ) self._logger.log("debug", f"[{manager.role}] Task output: {task_output}") + if self.output_log_file: + self._file_handler.log( + agent=manager.role, task=task_output, status="completed" + ) + self._finish_execution(task_output) return self._format_output(task_output), manager._token_process.get_summary() diff --git a/src/crewai/utilities/__init__.py b/src/crewai/utilities/__init__.py index 21f9616ad..92bd9cd69 100644 --- a/src/crewai/utilities/__init__.py +++ b/src/crewai/utilities/__init__.py @@ -5,3 +5,4 @@ from .logger import Logger from .printer import Printer from .prompts import Prompts from .rpm_controller import RPMController +from .fileHandler import FileHandler diff --git a/src/crewai/utilities/fileHandler.py b/src/crewai/utilities/fileHandler.py new file mode 100644 index 000000000..0a550dbc5 --- /dev/null +++ b/src/crewai/utilities/fileHandler.py @@ -0,0 +1,20 @@ +import os +from datetime import datetime + + +class FileHandler: + """take care of file operations, currently it only logs messages to a file""" + + def __init__(self, file_path): + if isinstance(file_path, bool): + self._path = os.path.join(os.curdir, "logs.txt") + elif isinstance(file_path, str): + self._path = file_path + else: + raise ValueError("file_path must be either a boolean or a string.") + + def log(self, **kwargs): + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + message = f"{now}: ".join([f"{key}={value}" for key, value in kwargs.items()]) + with open(self._path, "a") as file: + file.write(message + "\n") diff --git a/tests/cassettes/test_crew_log_file_output.yaml b/tests/cassettes/test_crew_log_file_output.yaml new file mode 100644 index 000000000..79df20a82 --- /dev/null +++ b/tests/cassettes/test_crew_log_file_output.yaml @@ -0,0 +1,160 @@ +interactions: +- request: + body: '{"messages": [{"role": "user", "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: Make the best research + and analysis on content about AI and AI agentsTo give my best complete final + answer to the task use the exact following format:\n\nThought: I now can give + a great answer\nFinal Answer: my best complete final answer to the task.\nYour + 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!\nCurrent + Task: Say Hi\n\nThis is the expect criteria for your final answer: The word: + Hi \n you 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: \n"}], "model": + "gpt-4", "n": 1, "stop": ["\nObservation"], "stream": true, "temperature": 0.7}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, br + connection: + - keep-alive + content-length: + - '1082' + content-type: + - application/json + cookie: + - _cfuvid=zVKnitRNLhrt8b2P3MHGfXS_82YiqkGpi46seIwshAM-1709396719694-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.16.2 + x-stainless-arch: + - other:amd64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - Windows + x-stainless-package-version: + - 1.16.2 + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.10.10 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: 'data: {"id":"chatcmpl-9AjeorqO0QfP8DJ8NwGIUKSlhQqav","object":"chat.completion.chunk","created":1712345814,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"chatcmpl-9AjeorqO0QfP8DJ8NwGIUKSlhQqav","object":"chat.completion.chunk","created":1712345814,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"I"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"chatcmpl-9AjeorqO0QfP8DJ8NwGIUKSlhQqav","object":"chat.completion.chunk","created":1712345814,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" + now"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"chatcmpl-9AjeorqO0QfP8DJ8NwGIUKSlhQqav","object":"chat.completion.chunk","created":1712345814,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" + can"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"chatcmpl-9AjeorqO0QfP8DJ8NwGIUKSlhQqav","object":"chat.completion.chunk","created":1712345814,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" + give"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"chatcmpl-9AjeorqO0QfP8DJ8NwGIUKSlhQqav","object":"chat.completion.chunk","created":1712345814,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" + a"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"chatcmpl-9AjeorqO0QfP8DJ8NwGIUKSlhQqav","object":"chat.completion.chunk","created":1712345814,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" + great"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"chatcmpl-9AjeorqO0QfP8DJ8NwGIUKSlhQqav","object":"chat.completion.chunk","created":1712345814,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" + answer"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"chatcmpl-9AjeorqO0QfP8DJ8NwGIUKSlhQqav","object":"chat.completion.chunk","created":1712345814,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"\n\n"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"chatcmpl-9AjeorqO0QfP8DJ8NwGIUKSlhQqav","object":"chat.completion.chunk","created":1712345814,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"Final"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"chatcmpl-9AjeorqO0QfP8DJ8NwGIUKSlhQqav","object":"chat.completion.chunk","created":1712345814,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" + Answer"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"chatcmpl-9AjeorqO0QfP8DJ8NwGIUKSlhQqav","object":"chat.completion.chunk","created":1712345814,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":":"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"chatcmpl-9AjeorqO0QfP8DJ8NwGIUKSlhQqav","object":"chat.completion.chunk","created":1712345814,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" + Hi"},"logprobs":null,"finish_reason":null}]} + + + data: {"id":"chatcmpl-9AjeorqO0QfP8DJ8NwGIUKSlhQqav","object":"chat.completion.chunk","created":1712345814,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} + + + data: [DONE] + + + ' + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 86fbfd5e4e45012b-GRU + Cache-Control: + - no-cache, must-revalidate + Connection: + - keep-alive + Content-Type: + - text/event-stream + Date: + - Fri, 05 Apr 2024 19:36:55 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=Dup92ckJhbI_FxZty6XIjohW.sTaChSMX.8lwju_iA8-1712345815-1.0.1.1-DYCBIKozKcyEYWv.mE5gRee5frdxJU8EBOeZrex7BOH_U4HLjPJ4IMUP0m_YMiO3fKf5IClhW3KIzE8cl2C.ww; + path=/; expires=Fri, 05-Apr-24 20:06:55 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=.Ctsps4oQpopvSdn2mneN2jnLB0vatjzGjPz1HgR734-1712345815450-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + access-control-allow-origin: + - '*' + alt-svc: + - h3=":443"; ma=86400 + openai-model: + - gpt-4-0613 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '363' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=15724800; includeSubDomains + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '300000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '299750' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 50ms + x-request-id: + - req_83d3bc5e55b3d012f700b51707cc46e0 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/crew_test.py b/tests/crew_test.py index d69d49c28..7121eab00 100644 --- a/tests/crew_test.py +++ b/tests/crew_test.py @@ -894,3 +894,21 @@ def test_disabled_memory_using_contextual_memory(): with patch.object(ContextualMemory, "build_context_for_task") as contextual_mem: crew.kickoff() contextual_mem.assert_not_called() + + +@pytest.mark.vcr(filter_headers=["authorization"]) +def test_crew_log_file_output(tmp_path): + test_file = tmp_path / "logs.txt" + tasks = [ + Task( + description="Say Hi", + expected_output="The word: Hi", + agent=researcher, + ) + ] + + test_message = {"agent": "Researcher", "task": "Say Hi"} + + crew = Crew(agents=[researcher], tasks=tasks, output_log_file=str(test_file)) + crew.kickoff() + assert test_file.exists()