diff --git a/autogen/agentchat/contrib/agent_builder.py b/autogen/agentchat/contrib/agent_builder.py index aece98edc9..14f2049043 100644 --- a/autogen/agentchat/contrib/agent_builder.py +++ b/autogen/agentchat/contrib/agent_builder.py @@ -269,7 +269,7 @@ def _create_agent( description = agent_config["description"] # Path to the customize **ConversableAgent** class. - model_path = agent_config.get("model_path", None) + agent_path = agent_config.get("agent_path", None) filter_dict = {} if len(model_name_or_hf_repo) > 0: filter_dict.update({"model": model_name_or_hf_repo}) @@ -304,8 +304,8 @@ def _create_agent( ) model_class = autogen.AssistantAgent - if model_path: - module_path, model_class_name = model_path.replace("/", ".").rsplit(".", 1) + if agent_path: + module_path, model_class_name = agent_path.replace("/", ".").rsplit(".", 1) module = importlib.import_module(module_path) model_class = getattr(module, model_class_name) if not issubclass(model_class, autogen.ConversableAgent): @@ -315,7 +315,7 @@ def _create_agent( additional_config = { k: v for k, v in agent_config.items() - if k not in ["model", "name", "system_message", "description", "model_path", "tags"] + if k not in ["model", "name", "system_message", "description", "agent_path", "tags"] } agent = model_class( name=agent_name, llm_config=current_config.copy(), description=description, **additional_config diff --git a/autogen/agentchat/contrib/captainagent.py b/autogen/agentchat/contrib/captainagent.py new file mode 100644 index 0000000000..0ee7496939 --- /dev/null +++ b/autogen/agentchat/contrib/captainagent.py @@ -0,0 +1,474 @@ +import hashlib +import json +import os +from typing import Callable, Dict, List, Literal, Optional, Union + +import autogen +from autogen import UserProxyAgent +from autogen.agentchat.conversable_agent import ConversableAgent + +from .agent_builder import AgentBuilder +from .tool_retriever import ToolBuilder, get_full_tool_description + + +class CaptainAgent(ConversableAgent): + """ + (In preview) Captain agent, designed to solve a task with an agent or a group of agents. + """ + + DEFAULT_NESTED_CONFIG = { + "autobuild_init_config": { + "config_file_or_env": "OAI_CONFIG_LIST", + "builder_model": "gpt-4o-mini", + "agent_model": "gpt-4o-mini", + }, + "autobuild_build_config": { + "default_llm_config": {"temperature": 1, "top_p": 0.95, "max_tokens": 2048}, + "code_execution_config": { + "timeout": 300, + "work_dir": "groupchat", + "last_n_messages": 1, + "use_docker": False, + }, + "coding": True, + }, + "group_chat_config": {"max_round": 10}, + "group_chat_llm_config": None, + "max_turns": 5, + } + + AUTOBUILD_TOOL = { + "type": "function", + "function": { + "name": "seek_experts_help", + "description": """Build a group of experts and let them chat with each other in a group chat.""", + "parameters": { + "type": "object", + "properties": { + "group_name": {"type": "string", "description": "[REQUIRED] Name of the group."}, + "building_task": { + "type": "string", + "description": """Instructions that help a build manager to build a group of experts.""", + }, + "execution_task": { + "type": "string", + "description": """[REQUIRED] The task that needs the experts to solve by conversation.""", + }, + }, + }, + }, + } + + AUTOBUILD_SYSTEM_MESSAGE = """# Your role +You are a perfect manager of a group of advanced experts. + +# How to solve the task +When a task is assigned to you: +1. Analysis of its constraints and conditions for completion. +2. Respond with a specific plan of how to solve the task. + +After that, you can solve the task in two ways: +- Delegate the resolution of tasks to other experts created by seeking a group of experts for help and derive conclusive insights from their conversation summarization. +- Analysis and solve the task with your coding and language skills. + +# How to seek experts help +The tool "seek_experts_help" can build a group of experts according to the building_task and let them chat with each other in a group chat to solve the execution_task you provided. +- This tool will summarize the essence of the experts' conversation and the derived conclusions. +- You should not modify any task information from meta_user_proxy, including code blocks, but you can provide extra information. +- Within a single response, you are limited to initiating one group of experts. + +## building_task +This task helps a build manager to build a group of experts for your task. +You should suggest less then three roles (including a checker for verification) with the following format. + +### Format +- [Detailed description for role 1] +- [Detailed description for role 2] +- [Detailed description for checker] + +## execution_task +This is the task that needs the experts to solve by conversation. +You should Provide the following information in markdown format. + +### Format +## Task description +... +## Plan for solving the task +... +## Output format +... +## Constraints and conditions for completion +... +## [Optional] results (including code blocks) and reason from last response +... + +# After seek_experts_help +You will receive a comprehensive conclusion from the conversation, including the task information, results, reason for the results, conversation contradiction or issues, and additional information. +You **must** conduct a thorough verification for the result and reason's logical compliance by leveraging the step-by-step backward reasoning with the same group of experts (with the same group name) when: +- The conversation has contradictions or issues (need double-check marked as yes), or +- The result is different from the previous results. + +Note that the previous experts will forget everything after you obtain the response from them. You should provide the results (including code blocks) you collected from the previous experts' response and put it in the new execution_task. + +# Some useful instructions +- You only have one tool called "seek_experts_help". +- Provide a answer yourself after "seek_experts_help". +- You should suggest python code in a python coding block (```python...```). If you need to get the value of a variable, you must use the print statement. +- When using code, you must indicate the script type in the code block. +- Do not suggest incomplete code which requires users to modify. +- Be clear about which step uses code, which step uses your language skill, and which step to build a group chat. +- If the code's result indicates there is an error, fix the error and output the whole code again. +- If the error can't be fixed or if the task is not solved even after the code is executed successfully, analyze the problem, revisit your assumption, collect additional info you need, and think of a different approach to try. +- Include verifiable evidence in your response if possible. +- After completing all tasks and verifications, you should conclude the operation and reply "TERMINATE" +""" + + DEFAULT_DESCRIPTION = "A helpful AI assistant that can build a group of agents at a proper time to solve a task." + + # This is used to prompt the LLM to summarize the conversation history between CaptainAgent's tool execution history + DEFAULT_SUMMARY_PROMPT = "Read the following conversation history between an expert and a group of agent experts, summarize the conversation history. You should include the initial task, the experts' plan and the attempt, finally the results of the conversation." + + def __init__( + self, + name: str, + system_message: Optional[str] = None, + llm_config: Optional[Union[Dict, Literal[False]]] = None, + is_termination_msg: Optional[Callable[[Dict], bool]] = None, + max_consecutive_auto_reply: Optional[int] = None, + human_input_mode: Optional[str] = "NEVER", + code_execution_config: Optional[Union[Dict, Literal[False]]] = False, + nested_config: Optional[Dict] = None, + description: Optional[str] = DEFAULT_DESCRIPTION, + **kwargs, + ): + """ + Args: + name (str): agent name. + system_message (str): system message for the ChatCompletion inference. + Please override this attribute if you want to reprogram the agent. + llm_config (dict): llm inference configuration. + Please refer to [OpenAIWrapper.create](/docs/reference/oai/client#create) for available options. + is_termination_msg (function): a function that takes a message in the form of a dictionary + and returns a boolean value indicating if this received message is a termination message. + The dict can contain the following keys: "content", "role", "name", "function_call". + max_consecutive_auto_reply (int): the maximum number of consecutive auto replies. + default to None (no limit provided, class attribute MAX_CONSECUTIVE_AUTO_REPLY will be used as the limit in this case). + The limit only plays a role when human_input_mode is not "ALWAYS". + **kwargs (dict): Please refer to other kwargs in + [ConversableAgent](https://github.com/ag2ai/ag2/blob/main/autogen/agentchat/conversable_agent.py#L74). + """ + super().__init__( + name, + is_termination_msg=is_termination_msg, + max_consecutive_auto_reply=max_consecutive_auto_reply, + human_input_mode=human_input_mode, + code_execution_config=code_execution_config, + llm_config=llm_config, + description=description, + **kwargs, + ) + + if system_message is None: + system_message = self.AUTOBUILD_SYSTEM_MESSAGE + nested_config = self._update_config(self.DEFAULT_NESTED_CONFIG, nested_config) + if nested_config["group_chat_llm_config"] is None: + nested_config["group_chat_llm_config"] = llm_config.copy() + + self.assistant = ConversableAgent( + name="CaptainAgent", + system_message=system_message, + llm_config=llm_config, + ) + self.assistant.update_tool_signature(self.AUTOBUILD_TOOL, is_remove=False) + + self.executor = CaptainUserProxyAgent( + name="Expert_summoner", + code_execution_config=code_execution_config, + nested_config=nested_config, + human_input_mode="NEVER", + ) + + self.register_nested_chats( + [ + { + "sender": self.executor, + "recipient": self.assistant, + "max_turns": nested_config["max_turns"], + "summary_method": "reflection_with_llm", + "summary_args": { + "summary_prompt": self.DEFAULT_SUMMARY_PROMPT, + }, + } + ], + trigger=UserProxyAgent, + # reply_func_from_nested_chats=None, + position=0, + ) + + @staticmethod + def _update_config(default_dict: Dict, update_dict: Optional[Dict]) -> Dict: + """ + Recursively updates the default_dict with values from update_dict. + """ + if update_dict is None: + return default_dict + + for key, value in update_dict.items(): + default_value = default_dict.get(key) + if isinstance(default_value, dict) and isinstance(value, dict): + # Recursively update nested dictionaries + default_dict[key] = CaptainAgent._update_config(default_value, value) + else: + # Update the value or add new key + default_dict[key] = value + + return default_dict + + +class CaptainUserProxyAgent(ConversableAgent): + """(In preview) A proxy agent for the captain agent, that can execute code and provide feedback to the other agents.""" + + CONVERSATION_REVIEW_PROMPT = """# Your task +Briefly summarize the conversation history derived from an experts' group chat by following the answer format. +If you found non-trivial errors or issues in the conversation, point it out with a detailed reason and mark the "Need double-check" as "Yes." if you think it is worth further verification. + +# Conversation history: +{chat_history} + +# Answer format +## Task +... + +## Results +... + +## Reason for the results +... + +## Errors or issues in the conversation +... + +### Need to double-check? +[Yes or No] + +## Additional information (file path, code blocks, url, etc.) +... +""" + + AUTOBUILD_TASK_DESC = """You are given: (1) a task and advises from your manager with a specific plan and (2) a general task. +Collect information from the general task, follow the suggestions from manager to solve the task. + +# General Task +{general_task} + +# Task and suggestions from manager +{manager_task} """ + + DEFAULT_AUTO_REPLY = "I'm a proxy and I can only execute your tool or end the conversation. If you think the problem is solved, please reply me only with 'TERMINATE'." + + DEFAULT_USER_PROXY_AGENT_DESCRIPTIONS = { + "ALWAYS": "An attentive HUMAN user who can answer questions about the task, and can perform tasks such as running Python code or inputting command line commands at a Linux terminal and reporting back the execution results.", + "TERMINATE": "A user that can run Python code or input command line commands at a Linux terminal and report back the execution results.", + "NEVER": "A computer terminal that can running Python scripts (provided to it quoted in ```python code blocks), or sh shell scripts (provided to it quoted in ```sh code blocks), or the conversation history and result of a group of agents", + } + + def __init__( + self, + name: str, + nested_config: Dict, + agent_config_save_path: str = None, + is_termination_msg: Optional[Callable[[Dict], bool]] = None, + max_consecutive_auto_reply: Optional[int] = None, + human_input_mode: Optional[str] = "NEVER", + code_execution_config: Optional[Union[Dict, Literal[False]]] = None, + default_auto_reply: Optional[Union[str, Dict, None]] = DEFAULT_AUTO_REPLY, + llm_config: Optional[Union[Dict, Literal[False]]] = False, + system_message: Optional[Union[str, List]] = "", + description: Optional[str] = None, + ): + """ + Args: + name (str): name of the agent. + nested_config (dict): the configuration for the nested chat instantiated by CaptainAgent. + is_termination_msg (function): a function that takes a message in the form of a dictionary + and returns a boolean value indicating if this received message is a termination message. + The dict can contain the following keys: "content", "role", "name", "function_call". + max_consecutive_auto_reply (int): the maximum number of consecutive auto replies. + default to None (no limit provided, class attribute MAX_CONSECUTIVE_AUTO_REPLY will be used as the limit in this case). + The limit only plays a role when human_input_mode is not "ALWAYS". + human_input_mode (str): whether to ask for human inputs every time a message is received. + Possible values are "ALWAYS", "TERMINATE", "NEVER". + (1) When "ALWAYS", the agent prompts for human input every time a message is received. + Under this mode, the conversation stops when the human input is "exit", + or when is_termination_msg is True and there is no human input. + (2) When "TERMINATE", the agent only prompts for human input only when a termination message is received or + the number of auto reply reaches the max_consecutive_auto_reply. + (3) When "NEVER", the agent will never prompt for human input. Under this mode, the conversation stops + when the number of auto reply reaches the max_consecutive_auto_reply or when is_termination_msg is True. + code_execution_config (dict or False): config for the code execution. + To disable code execution, set to False. Otherwise, set to a dictionary with the following keys: + - work_dir (Optional, str): The working directory for the code execution. + If None, a default working directory will be used. + The default working directory is the "extensions" directory under + "path_to_autogen". + - use_docker (Optional, list, str or bool): The docker image to use for code execution. + Default is True, which means the code will be executed in a docker container. A default list of images will be used. + If a list or a str of image name(s) is provided, the code will be executed in a docker container + with the first image successfully pulled. + If False, the code will be executed in the current environment. + We strongly recommend using docker for code execution. + - timeout (Optional, int): The maximum execution time in seconds. + - last_n_messages (Experimental, Optional, int): The number of messages to look back for code execution. Default to 1. + default_auto_reply (str or dict or None): the default auto reply message when no code execution or llm based reply is generated. + llm_config (dict or False): llm inference configuration. + Please refer to [OpenAIWrapper.create](/docs/reference/oai/client#create) + for available options. + Default to false, which disables llm-based auto reply. + system_message (str or List): system message for ChatCompletion inference. + Only used when llm_config is not False. Use it to reprogram the agent. + description (str): a short description of the agent. This description is used by other agents + (e.g. the GroupChatManager) to decide when to call upon this agent. (Default: system_message) + """ + description = ( + description if description is not None else self.DEFAULT_USER_PROXY_AGENT_DESCRIPTIONS[human_input_mode] + ) + super().__init__( + name=name, + system_message=system_message, + is_termination_msg=is_termination_msg, + max_consecutive_auto_reply=max_consecutive_auto_reply, + human_input_mode=human_input_mode, + code_execution_config=code_execution_config, + llm_config=llm_config, + default_auto_reply=default_auto_reply, + description=description, + ) + self.register_function( + function_map={ + "seek_experts_help": lambda **args: self._run_autobuild(**args), + } + ) + self._agent_config_save_path = agent_config_save_path + self._nested_config = nested_config.copy() + self._code_execution_config = code_execution_config + self.build_history = {} + self.tool_history = {} + self.build_times = 0 + + def _run_autobuild(self, group_name: str, execution_task: str, building_task: str = "") -> str: + """ + Build a group of agents by AutoBuild to solve the task. + This function requires the nested_config to contain the autobuild_init_config, autobuild_llm_config, group_chat_llm_config. + """ + print("==> Running AutoBuild...", flush=True) + print("\n==> Building task: ", building_task, flush=True) + print("\n==> Execution task: ", execution_task, flush=True) + + builder = AgentBuilder(**self._nested_config["autobuild_init_config"]) + # if the group is already built, load from history + if group_name in self.build_history.keys(): + agent_list, agent_configs = builder.load(config_json=json.dumps(self.build_history[group_name])) + if self._nested_config.get("autobuild_tool_config", None) and agent_configs["coding"] is True: + # tool library is enabled, reload tools and bind them to the agents + tool_root_dir = self.tool_root_dir + tool_builder = ToolBuilder( + corpus_path=os.path.join(tool_root_dir, "tool_description.tsv"), + retriever=self._nested_config["autobuild_tool_config"]["retriever"], + ) + for idx, agent in enumerate(agent_list): + if idx == len(self.tool_history[group_name]): + break + tool_builder.bind(agent, "\n\n".join(self.tool_history[group_name][idx])) + agent_list[-1] = tool_builder.bind_user_proxy(agent_list[-1], tool_root_dir) + else: + if self._nested_config["autobuild_build_config"].get("library_path_or_json", None): + # Build from retrieval + agent_list, agent_configs = builder.build_from_library( + building_task, **self._nested_config["autobuild_build_config"] + ) + self.build_history[group_name] = agent_configs.copy() + + if self._nested_config.get("autobuild_tool_config", None) and agent_configs["coding"] is True: + print("==> Retrieving tools...", flush=True) + skills = building_task.split("\n") + if len(skills) == 0: + skills = [building_task] + + if self._nested_config["autobuild_tool_config"].get("tool_root", "default") == "default": + cur_path = os.path.dirname(os.path.abspath(__file__)) + tool_root_dir = os.path.join(cur_path, "captainagent", "tools") + else: + tool_root_dir = self._nested_config["autobuild_tool_config"]["tool_root"] + self.tool_root_dir = tool_root_dir + + # Retrieve and build tools based on the smilarities between the skills and the tool description + tool_builder = ToolBuilder( + corpus_path=os.path.join(tool_root_dir, "tool_description.tsv"), + retriever=self._nested_config["autobuild_tool_config"]["retriever"], + ) + for idx, skill in enumerate(skills): + tools = tool_builder.retrieve(skill) + docstrings = [] + for tool in tools: + category, tool_name = tool.split(" ")[0], tool.split(" ")[1] + tool_path = os.path.join(tool_root_dir, category, f"{tool_name}.py") + docstring = get_full_tool_description(tool_path) + docstrings.append(docstring) + tool_builder.bind(agent_list[idx], "\n\n".join(docstrings)) + # log tools + tool_history = self.tool_history.get(group_name, []) + tool_history.append(docstrings) + self.tool_history[group_name] = tool_history + + agent_list[-1] = tool_builder.bind_user_proxy(agent_list[-1], tool_root_dir) + + else: + # Build agents from scratch + agent_list, agent_configs = builder.build( + building_task, **self._nested_config["autobuild_build_config"] + ) + self.build_history[group_name] = agent_configs.copy() + + if self._agent_config_save_path is not None: + building_task_md5 = hashlib.md5(building_task.encode("utf-8")).hexdigest() + with open(f"{self._agent_config_save_path}/build_history_{building_task_md5}.json", "w") as f: + json.dump(self.build_history, f) + + self.build_times += 1 + # start nested chat + nested_group_chat = autogen.GroupChat( + agents=agent_list, + messages=[], + allow_repeat_speaker=agent_list[:-1] if agent_configs["coding"] is True else agent_list, + **self._nested_config["group_chat_config"], + ) + manager = autogen.GroupChatManager( + groupchat=nested_group_chat, llm_config=self._nested_config["group_chat_llm_config"] + ) + key = list(self.chat_messages.keys())[0] + general_task = self.chat_messages[key][0]["content"] + agent_list[0].initiate_chat( + manager, message=self.AUTOBUILD_TASK_DESC.format(general_task=general_task, manager_task=execution_task) + ) + chat_history = [] + key = list(agent_list[0].chat_messages.keys())[0] + chat_messages = agent_list[0].chat_messages[key] + for item in chat_messages: + chat_history.append(item) + + # Review the group chat history. + summary_model = builder.builder_model + summarized_history = ( + summary_model.create( + messages=[ + { + "role": "user", + "content": self.CONVERSATION_REVIEW_PROMPT.format(chat_history=chat_history), + } + ] + ) + .choices[0] + .message.content + ) + + return f"# Response from seek_agent_help: \n{summarized_history}" diff --git a/autogen/agentchat/contrib/tool_retriever.py b/autogen/agentchat/contrib/tool_retriever.py new file mode 100644 index 0000000000..1752fbe19c --- /dev/null +++ b/autogen/agentchat/contrib/tool_retriever.py @@ -0,0 +1,114 @@ +import importlib.util +import inspect +import os +from textwrap import dedent, indent + +import pandas as pd +from sentence_transformers import SentenceTransformer, util + +from autogen import AssistantAgent, UserProxyAgent +from autogen.coding import LocalCommandLineCodeExecutor + + +class ToolBuilder: + TOOL_USING_PROMPT = """# Functions + You have access to the following functions. They can be accessed from the module called 'functions' by their function names. +For example, if there is a function called `foo` you could import it by writing `from functions import foo` +{functions} +""" + + def __init__(self, corpus_path, retriever="all-mpnet-base-v2"): + + self.df = pd.read_csv(corpus_path, sep="\t") + document_list = self.df["document_content"].tolist() + + self.model = SentenceTransformer(retriever) + self.embeddings = self.model.encode(document_list) + + def retrieve(self, query, top_k=3): + # Encode the query using the Sentence Transformer model + query_embedding = self.model.encode([query]) + + hits = util.semantic_search(query_embedding, self.embeddings, top_k=top_k) + + results = [] + for hit in hits[0]: + results.append(self.df.iloc[hit["corpus_id"], 1]) + return results + + def bind(self, agent: AssistantAgent, functions: str): + """Binds the function to the agent so that agent is aware of it.""" + sys_message = agent.system_message + sys_message += self.TOOL_USING_PROMPT.format(functions=functions) + agent.update_system_message(sys_message) + return + + def bind_user_proxy(self, agent: UserProxyAgent, tool_root: str): + """ + Updates user proxy agent with a executor so that code executor can successfully execute function-related code. + Returns an updated user proxy. + """ + # Find all the functions in the tool root + functions = find_callables(tool_root) + + code_execution_config = agent._code_execution_config + executor = LocalCommandLineCodeExecutor( + timeout=code_execution_config.get("timeout", 180), + work_dir=code_execution_config.get("work_dir", "coding"), + functions=functions, + ) + code_execution_config = { + "executor": executor, + "last_n_messages": code_execution_config.get("last_n_messages", 1), + } + updated_user_proxy = UserProxyAgent( + name=agent.name, + is_termination_msg=agent._is_termination_msg, + code_execution_config=code_execution_config, + human_input_mode="NEVER", + default_auto_reply=agent._default_auto_reply, + ) + return updated_user_proxy + + +def get_full_tool_description(py_file): + """ + Retrieves the function signature for a given Python file. + """ + with open(py_file, "r") as f: + code = f.read() + exec(code) + function_name = os.path.splitext(os.path.basename(py_file))[0] + if function_name in locals(): + func = locals()[function_name] + content = f"def {func.__name__}{inspect.signature(func)}:\n" + docstring = func.__doc__ + + if docstring: + docstring = dedent(docstring) + docstring = '"""' + docstring + '"""' + docstring = indent(docstring, " ") + content += docstring + "\n" + return content + else: + raise ValueError(f"Function {function_name} not found in {py_file}") + + +def find_callables(directory): + """ + Find all callable objects defined in Python files within the specified directory. + """ + callables = [] + for root, dirs, files in os.walk(directory): + for file in files: + if file.endswith(".py"): + module_name = os.path.splitext(file)[0] + module_path = os.path.join(root, file) + spec = importlib.util.spec_from_file_location(module_name, module_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + for name, value in module.__dict__.items(): + if callable(value) and name == module_name: + callables.append(value) + break + return callables diff --git a/notebook/test_captainagent.ipynb b/notebook/test_captainagent.ipynb new file mode 100644 index 0000000000..75dffb175d --- /dev/null +++ b/notebook/test_captainagent.ipynb @@ -0,0 +1,474 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/vscode/.local/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "import autogen\n", + "from autogen import UserProxyAgent\n", + "from autogen.agentchat.contrib.captainagent import CaptainAgent" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Test CaptainAgent w/o libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "llm_config = {\n", + " \"config_list\": autogen.config_list_from_json(\"OAI_CONFIG_LIST\", filter_dict={\"model\": \"gpt-4o-mini\"}),\n", + "}\n", + "\n", + "captain_agent = CaptainAgent(\n", + " name=\"captain_agent\",\n", + " llm_config=llm_config,\n", + " code_execution_config={\"use_docker\": False, \"work_dir\": \"groupchat\"},\n", + " # nested_config=nested_config,\n", + ")\n", + "user_proxy = UserProxyAgent(name=\"user_proxy\", human_input_mode=\"NEVER\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33muser_proxy\u001b[0m (to captain_agent):\n", + "\n", + "Assemble a team of experts, find the stock price of Microsoft in the past 1 year and plot a line chart to show the trend. Save the line chart as \"microsoft_stock_price.png\".\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[34m\n", + "********************************************************************************\u001b[0m\n", + "\u001b[34mStarting a new chat....\u001b[0m\n", + "\u001b[34m\n", + "********************************************************************************\u001b[0m\n", + "\u001b[33mExpert_summoner\u001b[0m (to CaptainAgent):\n", + "\n", + "Assemble a team of experts, find the stock price of Microsoft in the past 1 year and plot a line chart to show the trend. Save the line chart as \"microsoft_stock_price.png\".\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[31m\n", + ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", + "\u001b[33mCaptainAgent\u001b[0m (to Expert_summoner):\n", + "\n", + "\u001b[32m***** Suggested tool call (call_2JJvuE6dC1HNW1lf4oMAvton): seek_experts_help *****\u001b[0m\n", + "Arguments: \n", + "{\"group_name\":\"Stock_analysis_experts\",\"building_task\":\"- A financial analyst with experience in analyzing stock market data, proficient in using Python libraries such as yfinance to fetch financial data from the internet.\\n- A data visualization expert who specializes in creating line charts using Python libraries such as matplotlib.\\n- A data verification specialist to ensure the accuracy and completeness of the financial data and the resulting visualizations.\",\"execution_task\":\"## Task description\\nFind the stock price of Microsoft (MSFT) for the past 1 year and plot a line chart to show the price trend. Save the resulting line chart as a file named \\\"microsoft_stock_price.png\\\".\\n\\n## Plan for solving the task\\n1. Use a Python library like `yfinance` to fetch Microsoft's stock price data for the last 1 year.\\n2. Utilize a data visualization library such as `matplotlib` to create a line chart showing the trend of the stock price data obtained.\\n3. Save the plotted line chart as a PNG file with the name \\\"microsoft_stock_price.png\\\".\\n\\n## Output format\\n- A file named \\\"microsoft_stock_price.png\\\" containing the line chart of Microsoft's stock prices over the past 1 year.\\n\\n## Constraints and conditions for completion\\n- The stock data must cover a 1-year period from the current date.\\n- The line chart should accurately reflect the trends in the stock price data for Microsoft.\\n- The PNG file must be saved without any errors or corruptions.\"}\n", + "\u001b[32m**********************************************************************************\u001b[0m\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[35m\n", + ">>>>>>>> EXECUTING FUNCTION seek_experts_help...\u001b[0m\n", + "==> Running AutoBuild...\n", + "\n", + "==> Building task: - A financial analyst with experience in analyzing stock market data, proficient in using Python libraries such as yfinance to fetch financial data from the internet.\n", + "- A data visualization expert who specializes in creating line charts using Python libraries such as matplotlib.\n", + "- A data verification specialist to ensure the accuracy and completeness of the financial data and the resulting visualizations.\n", + "\n", + "==> Execution task: ## Task description\n", + "Find the stock price of Microsoft (MSFT) for the past 1 year and plot a line chart to show the price trend. Save the resulting line chart as a file named \"microsoft_stock_price.png\".\n", + "\n", + "## Plan for solving the task\n", + "1. Use a Python library like `yfinance` to fetch Microsoft's stock price data for the last 1 year.\n", + "2. Utilize a data visualization library such as `matplotlib` to create a line chart showing the trend of the stock price data obtained.\n", + "3. Save the plotted line chart as a PNG file with the name \"microsoft_stock_price.png\".\n", + "\n", + "## Output format\n", + "- A file named \"microsoft_stock_price.png\" containing the line chart of Microsoft's stock prices over the past 1 year.\n", + "\n", + "## Constraints and conditions for completion\n", + "- The stock data must cover a 1-year period from the current date.\n", + "- The line chart should accurately reflect the trends in the stock price data for Microsoft.\n", + "- The PNG file must be saved without any errors or corruptions.\n", + "\u001b[32m==> Generating agents...\u001b[0m\n", + "['Financial_Analyst_Expert', 'Data_Visualization_Expert', 'Data_Verification_Specialist_Expert'] are generated.\n", + "\u001b[32m==> Generating system message...\u001b[0m\n", + "Preparing system message for Financial_Analyst_Expert\n", + "Preparing system message for Data_Visualization_Expert\n", + "Preparing system message for Data_Verification_Specialist_Expert\n", + "\u001b[32m==> Generating description...\u001b[0m\n", + "Preparing description for Financial_Analyst_Expert\n", + "Preparing description for Data_Visualization_Expert\n", + "Preparing description for Data_Verification_Specialist_Expert\n", + "\u001b[32m==> Creating agents...\u001b[0m\n", + "Creating agent Financial_Analyst_Expert...\n", + "Creating agent Data_Visualization_Expert...\n", + "Creating agent Data_Verification_Specialist_Expert...\n", + "Adding user console proxy...\n", + "\u001b[33mFinancial_Analyst_Expert\u001b[0m (to chat_manager):\n", + "\n", + "You are given: (1) a task and advises from your manager with a specific plan and (2) a general task.\n", + "Collect information from the general task, follow the suggestions from manager to solve the task.\n", + "\n", + "# General Task\n", + "Assemble a team of experts, find the stock price of Microsoft in the past 1 year and plot a line chart to show the trend. Save the line chart as \"microsoft_stock_price.png\".\n", + "\n", + "# Task and suggestions from manager\n", + "## Task description\n", + "Find the stock price of Microsoft (MSFT) for the past 1 year and plot a line chart to show the price trend. Save the resulting line chart as a file named \"microsoft_stock_price.png\".\n", + "\n", + "## Plan for solving the task\n", + "1. Use a Python library like `yfinance` to fetch Microsoft's stock price data for the last 1 year.\n", + "2. Utilize a data visualization library such as `matplotlib` to create a line chart showing the trend of the stock price data obtained.\n", + "3. Save the plotted line chart as a PNG file with the name \"microsoft_stock_price.png\".\n", + "\n", + "## Output format\n", + "- A file named \"microsoft_stock_price.png\" containing the line chart of Microsoft's stock prices over the past 1 year.\n", + "\n", + "## Constraints and conditions for completion\n", + "- The stock data must cover a 1-year period from the current date.\n", + "- The line chart should accurately reflect the trends in the stock price data for Microsoft.\n", + "- The PNG file must be saved without any errors or corruptions. \n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Data_Visualization_Expert\n", + "\u001b[0m\n", + "\u001b[33mData_Visualization_Expert\u001b[0m (to chat_manager):\n", + "\n", + "Let's follow the plan outlined to complete the task of fetching Microsoft's stock price for the past year and visualizing it.\n", + "\n", + "### Step 1: Install necessary libraries\n", + "We need to ensure that the required libraries (`yfinance` and `matplotlib`) are installed. I'll proceed to install them. \n", + "\n", + "```sh\n", + "pip install yfinance matplotlib\n", + "```\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[31m\n", + ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is sh)...\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "exitcode: 0 (execution succeeded)\n", + "Code output: \n", + "Defaulting to user installation because normal site-packages is not writeable\n", + "Requirement already satisfied: yfinance in /home/vscode/.local/lib/python3.10/site-packages (0.2.50)\n", + "Requirement already satisfied: matplotlib in /home/vscode/.local/lib/python3.10/site-packages (3.9.2)\n", + "Requirement already satisfied: pandas>=1.3.0 in /home/vscode/.local/lib/python3.10/site-packages (from yfinance) (2.2.3)\n", + "Requirement already satisfied: numpy>=1.16.5 in /home/vscode/.local/lib/python3.10/site-packages (from yfinance) (1.26.4)\n", + "Requirement already satisfied: requests>=2.31 in /usr/local/lib/python3.10/site-packages (from yfinance) (2.32.3)\n", + "Requirement already satisfied: multitasking>=0.0.7 in /home/vscode/.local/lib/python3.10/site-packages (from yfinance) (0.0.11)\n", + "Requirement already satisfied: lxml>=4.9.1 in /home/vscode/.local/lib/python3.10/site-packages (from yfinance) (5.3.0)\n", + "Requirement already satisfied: platformdirs>=2.0.0 in /usr/local/lib/python3.10/site-packages (from yfinance) (4.3.6)\n", + "Requirement already satisfied: pytz>=2022.5 in /home/vscode/.local/lib/python3.10/site-packages (from yfinance) (2024.2)\n", + "Requirement already satisfied: frozendict>=2.3.4 in /home/vscode/.local/lib/python3.10/site-packages (from yfinance) (2.4.6)\n", + "Requirement already satisfied: peewee>=3.16.2 in /home/vscode/.local/lib/python3.10/site-packages (from yfinance) (3.17.8)\n", + "Requirement already satisfied: beautifulsoup4>=4.11.1 in /home/vscode/.local/lib/python3.10/site-packages (from yfinance) (4.12.3)\n", + "Requirement already satisfied: html5lib>=1.1 in /home/vscode/.local/lib/python3.10/site-packages (from yfinance) (1.1)\n", + "Requirement already satisfied: contourpy>=1.0.1 in /home/vscode/.local/lib/python3.10/site-packages (from matplotlib) (1.3.1)\n", + "Requirement already satisfied: cycler>=0.10 in /home/vscode/.local/lib/python3.10/site-packages (from matplotlib) (0.12.1)\n", + "Requirement already satisfied: fonttools>=4.22.0 in /home/vscode/.local/lib/python3.10/site-packages (from matplotlib) (4.55.0)\n", + "Requirement already satisfied: kiwisolver>=1.3.1 in /home/vscode/.local/lib/python3.10/site-packages (from matplotlib) (1.4.7)\n", + "Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.10/site-packages (from matplotlib) (24.2)\n", + "Requirement already satisfied: pillow>=8 in /home/vscode/.local/lib/python3.10/site-packages (from matplotlib) (11.0.0)\n", + "Requirement already satisfied: pyparsing>=2.3.1 in /home/vscode/.local/lib/python3.10/site-packages (from matplotlib) (3.2.0)\n", + "Requirement already satisfied: python-dateutil>=2.7 in /home/vscode/.local/lib/python3.10/site-packages (from matplotlib) (2.9.0.post0)\n", + "Requirement already satisfied: soupsieve>1.2 in /home/vscode/.local/lib/python3.10/site-packages (from beautifulsoup4>=4.11.1->yfinance) (2.6)\n", + "Requirement already satisfied: six>=1.9 in /home/vscode/.local/lib/python3.10/site-packages (from html5lib>=1.1->yfinance) (1.16.0)\n", + "Requirement already satisfied: webencodings in /home/vscode/.local/lib/python3.10/site-packages (from html5lib>=1.1->yfinance) (0.5.1)\n", + "Requirement already satisfied: tzdata>=2022.7 in /home/vscode/.local/lib/python3.10/site-packages (from pandas>=1.3.0->yfinance) (2024.2)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/site-packages (from requests>=2.31->yfinance) (3.4.0)\n", + "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/site-packages (from requests>=2.31->yfinance) (3.10)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/site-packages (from requests>=2.31->yfinance) (2.2.3)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.10/site-packages (from requests>=2.31->yfinance) (2024.8.30)\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Data_Visualization_Expert\n", + "\u001b[0m\n", + "\u001b[33mData_Visualization_Expert\u001b[0m (to chat_manager):\n", + "\n", + "The required libraries `yfinance` and `matplotlib` are installed successfully.\n", + "\n", + "### Step 2: Fetch Microsoft's stock price data for the past year\n", + "Now, I will write a Python script to fetch the stock price data for Microsoft (MSFT) for the past year.\n", + "\n", + "```python\n", + "import yfinance as yf\n", + "import matplotlib.pyplot as plt\n", + "import datetime\n", + "\n", + "# Set the date range\n", + "end_date = datetime.datetime.now()\n", + "start_date = end_date - datetime.timedelta(days=365)\n", + "\n", + "# Fetch the data\n", + "msft_data = yf.download('MSFT', start=start_date, end=end_date)\n", + "\n", + "# Plotting the data\n", + "plt.figure(figsize=(10, 5))\n", + "plt.plot(msft_data['Close'], label='MSFT Closing Price')\n", + "plt.title('Microsoft Stock Price Over the Last Year')\n", + "plt.xlabel('Date')\n", + "plt.ylabel('Price (USD)')\n", + "plt.legend()\n", + "plt.grid()\n", + "plt.savefig('microsoft_stock_price.png')\n", + "plt.close()\n", + "```\n", + "\n", + "Let's execute this code to fetch the data and create the line chart.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[31m\n", + ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "exitcode: 0 (execution succeeded)\n", + "Code output: \n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Data_Verification_Specialist_Expert\n", + "\u001b[0m\n", + "\u001b[33mData_Verification_Specialist_Expert\u001b[0m (to chat_manager):\n", + "\n", + "The script to fetch Microsoft's stock price data for the past year and create the line chart executed successfully without any errors.\n", + "\n", + "### Step 3: Verify the output\n", + "Now, I will verify that the file \"microsoft_stock_price.png\" has been created and is not corrupted. I will check the existence of the file and its size to ensure it's a valid PNG file.\n", + "\n", + "```python\n", + "import os\n", + "\n", + "# Verify if the file exists and check its size\n", + "file_path = 'microsoft_stock_price.png'\n", + "if os.path.exists(file_path):\n", + " file_size = os.path.getsize(file_path)\n", + " print(f\"File '{file_path}' exists with size: {file_size} bytes\")\n", + "else:\n", + " print(f\"File '{file_path}' does not exist.\")\n", + "``` \n", + "\n", + "Let's execute this verification code.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[31m\n", + ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "exitcode: 0 (execution succeeded)\n", + "Code output: \n", + "File 'microsoft_stock_price.png' exists with size: 53717 bytes\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Financial_Analyst_Expert\n", + "\u001b[0m\n", + "\u001b[33mFinancial_Analyst_Expert\u001b[0m (to chat_manager):\n", + "\n", + "The file \"microsoft_stock_price.png\" exists and its size is 53,717 bytes, indicating it is a valid PNG file. \n", + "\n", + "With successful execution and verification, we can conclude that the task of fetching Microsoft's stock price for the past year and visualizing it has been completed successfully. \n", + "\n", + "If all members agree, we can proceed to terminate the conversation.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Data_Visualization_Expert\n", + "\u001b[0m\n", + "\u001b[33mData_Visualization_Expert\u001b[0m (to chat_manager):\n", + "\n", + "I agree that all tasks have been successfully completed and verified. We can proceed to terminate the conversation.\n", + "\n", + "TERMINATE\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Financial_Analyst_Expert\n", + "\u001b[0m\n", + "\u001b[33mFinancial_Analyst_Expert\u001b[0m (to chat_manager):\n", + "\n", + "TERMINATE\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mExpert_summoner\u001b[0m (to CaptainAgent):\n", + "\n", + "\u001b[33mExpert_summoner\u001b[0m (to CaptainAgent):\n", + "\n", + "\u001b[32m***** Response from calling tool (call_2JJvuE6dC1HNW1lf4oMAvton) *****\u001b[0m\n", + "# Response from seek_agent_help: \n", + "## Task\n", + "Assemble a team of experts to find the stock price of Microsoft (MSFT) for the past year and plot a line chart showing the trend. Save the chart as \"microsoft_stock_price.png\".\n", + "\n", + "## Results\n", + "Successfully completed the tasks as follows:\n", + "1. Installed required libraries (`yfinance` and `matplotlib`).\n", + "2. Fetched Microsoft’s stock price data for the past year and created a line chart.\n", + "3. Saved the line chart as \"microsoft_stock_price.png\".\n", + "4. Verified the existence and size of the file, confirming it is a valid PNG file.\n", + "\n", + "## Reason for the results\n", + "The task was executed step-by-step according to the outlined plan, with each phase verified for success. The libraries required for data fetching and visualization were installed without issues. The data was then successfully fetched, plotted, and saved, which was further confirmed by checking the file's existence and size, ensuring it wasn’t corrupted.\n", + "\n", + "## Errors or issues in the conversation\n", + "There are no obvious errors or issues in the conversation.\n", + "\n", + "### Need to double-check?\n", + "No\n", + "\n", + "## Additional information (file path, code blocks, url, etc.)\n", + "File saved at: \"microsoft_stock_price.png\" \n", + "```python\n", + "import yfinance as yf\n", + "import matplotlib.pyplot as plt\n", + "import datetime\n", + "\n", + "# Set the date range\n", + "end_date = datetime.datetime.now()\n", + "start_date = end_date - datetime.timedelta(days=365)\n", + "\n", + "# Fetch the data\n", + "msft_data = yf.download('MSFT', start=start_date, end=end_date)\n", + "\n", + "# Plotting the data\n", + "plt.figure(figsize=(10, 5))\n", + "plt.plot(msft_data['Close'], label='MSFT Closing Price')\n", + "plt.title('Microsoft Stock Price Over the Last Year')\n", + "plt.xlabel('Date')\n", + "plt.ylabel('Price (USD)')\n", + "plt.legend()\n", + "plt.grid()\n", + "plt.savefig('microsoft_stock_price.png')\n", + "plt.close()\n", + "```\n", + "\u001b[32m**********************************************************************\u001b[0m\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[31m\n", + ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", + "\u001b[33mCaptainAgent\u001b[0m (to Expert_summoner):\n", + "\n", + "The task to find the stock price of Microsoft for the past year and create a line chart showing the trend has been successfully completed. The line chart has been saved as the file \"microsoft_stock_price.png\". The execution proceeded smoothly with no issues detected. Here is the code used for the task:\n", + "\n", + "```python\n", + "import yfinance as yf\n", + "import matplotlib.pyplot as plt\n", + "import datetime\n", + "\n", + "# Set the date range\n", + "end_date = datetime.datetime.now()\n", + "start_date = end_date - datetime.timedelta(days=365)\n", + "\n", + "# Fetch the data\n", + "msft_data = yf.download('MSFT', start=start_date, end=end_date)\n", + "\n", + "# Plotting the data\n", + "plt.figure(figsize=(10, 5))\n", + "plt.plot(msft_data['Close'], label='MSFT Closing Price')\n", + "plt.title('Microsoft Stock Price Over the Last Year')\n", + "plt.xlabel('Date')\n", + "plt.ylabel('Price (USD)')\n", + "plt.legend()\n", + "plt.grid()\n", + "plt.savefig('microsoft_stock_price.png')\n", + "plt.close()\n", + "```\n", + "\n", + "This code fetches Microsoft's stock price data over the last year and visualizes it in a line chart, which is then saved as a PNG file. The file has been verified to ensure there are no errors. \n", + "\n", + "TERMINATE\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[31m\n", + ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", + "\u001b[33mExpert_summoner\u001b[0m (to CaptainAgent):\n", + "\n", + "exitcode: 0 (execution succeeded)\n", + "Code output: \n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[31m\n", + ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", + "\u001b[33mCaptainAgent\u001b[0m (to Expert_summoner):\n", + "\n", + "TERMINATE\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mcaptain_agent\u001b[0m (to user_proxy):\n", + "\n", + "In the conversation, a task was carried out by a group of experts to assemble a team required to find and plot the stock price trend of Microsoft (MSFT) over the past year. The team included a financial analyst, a data visualization expert, and a data verification specialist. By using Python libraries such as `yfinance` for fetching data and `matplotlib` for plotting, they successfully retrieved the stock price data and created a line chart. The chart was then saved as a PNG file named \"microsoft_stock_price.png\". The process was executed without errors, and the file was verified for accuracy and integrity.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33muser_proxy\u001b[0m (to captain_agent):\n", + "\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + } + ], + "source": [ + "result = user_proxy.initiate_chat(\n", + " captain_agent,\n", + " message='Assemble a team of experts, find the stock price of Microsoft in the past 1 year and plot a line chart to show the trend. Save the line chart as \"microsoft_stock_price.png\".',\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.15" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/website/blog/2024-11-15-CaptainAgent/img/overall.png b/website/blog/2024-11-15-CaptainAgent/img/overall.png index 43af19062b..93fa46eb3a 100644 --- a/website/blog/2024-11-15-CaptainAgent/img/overall.png +++ b/website/blog/2024-11-15-CaptainAgent/img/overall.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75df2ea45d3d26f1eae5ab5c2315041356eaec5f5c5e8e179302d10d0b7a8c9a -size 1961668 +oid sha256:087a8f647f1281b9214fb2263c1964e3d6ca98ac018557d17bf99de53703d933 +size 941181