diff --git a/.gitignore b/.gitignore
index 7fe0853..e5db574 100644
--- a/.gitignore
+++ b/.gitignore
@@ -232,3 +232,4 @@ fabric.properties
# Can't be having folks committing the crime of sharing their creds
.env
+expt/*
diff --git a/README.md b/README.md
index d804998..212a6f3 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ Repo-GPT is a Python CLI tool designed to utilize the power of OpenAI's GPT-3 mo
Repo-GPT can be installed via [pip](https://pip.pypa.io/en/stable/):
```bash
+brew install graphviz
pip install repo-gpt
```
@@ -93,6 +94,16 @@ repo-gpt add-test function_name --test_save_file_path $PWD/test.py --testing_pac
We welcome your contributions! Before starting, please make sure to install Python `3.11` and the latest version of [poetry](https://python-poetry.org/docs/#installing-with-pipx). [Pyenv](https://github.com/pyenv/pyenv) is a convenient tool to manage multiple Python versions on your computer.
Here are the steps to set up your development environment:
+0. Install global dependencies:
+
+ ```shell
+ nvm use --lts
+
+ brew install graphviz
+ export CFLAGS="-I $(brew --prefix graphviz)/include"
+ export LDFLAGS="-L $(brew --prefix graphviz)/lib"
+ pip install poetry
+ ```
1. Export your OpenAI key to your environment variables:
@@ -104,6 +115,7 @@ Here are the steps to set up your development environment:
```shell
poetry install --no-root
+ jupyter lab build
```
3. Install pre-commit hooks:
@@ -129,9 +141,20 @@ Here are the steps to set up your development environment:
You can view the output of the `code_embeddings.pkl` using the following command:
```shell
+poetry shell
+python
+import pandas as pd
pd.read_pickle('./.repo_gpt/code_embeddings.pkl', compression='infer')
```
+#### Interpreter
+```shell
+poetry shell
+ipython
+%load_ext autoreload
+%autoreload 2
+```
+
## Roadmap
Here are the improvements we are currently considering:
@@ -144,4 +167,7 @@ Here are the improvements we are currently considering:
- [ ] Save # of tokens each code snippet has so we can ensure we don't pass too many tokens to GPT
- [X] Add SQL file handler
- [ ] Add DBT file handler -- this may be a break in pattern as we'd want to use the manifest.json file
-- [ ] Create VSCode extension
+- [X] Create VSCode extension
+- [ ] Ensure files can be added & deleted and the indexing picks up on the changes.
+- [ ] Add .repogptignore file to config & use it in the indexing command
+- [ ] Use pygments library for prettier code formatting
diff --git a/expt/autogen-agents.ipynb b/expt/autogen-agents.ipynb
new file mode 100644
index 0000000..0704e80
--- /dev/null
+++ b/expt/autogen-agents.ipynb
@@ -0,0 +1,757 @@
+{
+ "cells": [
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ ""
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Auto Generated Agent Chat: Performs Research with Multi-Agent Group Chat\n",
+ "\n",
+ "AutoGen offers conversable agents powered by LLM, tool or human, which can be used to perform tasks collectively via automated chat. This framwork allows tool use and human participance through multi-agent conversation.\n",
+ "Please find documentation about this feature [here](https://microsoft.github.io/autogen/docs/Use-Cases/agent_chat).\n",
+ "\n",
+ "[More useful research paper](https://arxiv.org/pdf/2308.08155.pdf)\n",
+ "\n",
+ "## Requirements\n",
+ "\n",
+ "AutoGen requires `Python>=3.8`. To run this notebook example, please install:\n",
+ "```bash\n",
+ "pip install pyautogen\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%capture --no-stderr\n",
+ "# %pip install pyautogen~=0.1.0"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Encode the config list as a JSON string\n",
+ "OPENAI_API_KEY = ''"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Set your API Endpoint\n",
+ "\n",
+ "The [`config_list_from_json`](https://microsoft.github.io/autogen/docs/reference/oai/openai_utils#config_list_from_json) function loads a list of configurations from an environment variable or a json file."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from repo_gpt.file_handler.generic_code_file_handler import PythonFileHandler\n",
+ "from repo_gpt.search_service import SearchService, convert_search_df_to_json\n",
+ "from pathlib import Path\n",
+ "from repo_gpt.openai_service import OpenAIService\n",
+ "\n",
+ "pythonfilehandler = PythonFileHandler()\n",
+ "root_path = Path(\"/Users/shrutipatel/projects/work/repo-gpt/\")\n",
+ "embedding_path = root_path / \".repo_gpt/code_embeddings.pkl\"\n",
+ "openai_service = OpenAIService(OPENAI_API_KEY)\n",
+ "search_service = SearchService(openai_service, embedding_path)\n",
+ " \n",
+ "\n",
+ "def completed_all_code_updates(code_changes):\n",
+ " return code_changes\n",
+ "\n",
+ "def create_file(file_path, content):\n",
+ " \"\"\"\n",
+ " Create a new file with the provided content.\n",
+ "\n",
+ " Args:\n",
+ " - file_path (str): Path to the new file to be created.\n",
+ " - content (str): Content to write in the new file.\n",
+ "\n",
+ " Returns:\n",
+ " - str: Success or error message.\n",
+ " \"\"\"\n",
+ " full_path = root_path / Path(file_path)\n",
+ "\n",
+ " # Check if file already exists\n",
+ " if full_path.exists():\n",
+ " return (\n",
+ " f\"File {file_path} already exists. To update it, use append_to_file().\"\n",
+ " )\n",
+ "\n",
+ " with open(full_path, \"w\") as f:\n",
+ " f.write(content)\n",
+ "\n",
+ " return f\"File {file_path} has been created successfully.\"\n",
+ "\n",
+ "def append_to_file(file_path, content):\n",
+ " \"\"\"\n",
+ " Append content to an existing file.\n",
+ "\n",
+ " Args:\n",
+ " - file_path (str): Path to the file to be updated.\n",
+ " - content (str): Content to append in the file.\n",
+ "\n",
+ " Returns:\n",
+ " - str: Success or error message.\n",
+ " \"\"\"\n",
+ " full_path = root_path / Path(file_path)\n",
+ "\n",
+ " # Check if file exists\n",
+ " if not full_path.exists():\n",
+ " return f\"File {file_path} does not exist. To create it, use create_file().\"\n",
+ "\n",
+ " with open(full_path, \"a\") as f:\n",
+ " f.write(content)\n",
+ "\n",
+ " return f\"Content has been appended to {file_path} successfully.\"\n",
+ "\n",
+ "def view_function_code(function_name):\n",
+ " logger.info(f\"Reading the code for: {function_name}\")\n",
+ " functions_df, classes_df = search_service.find_function_match(\n",
+ " function_name\n",
+ " )\n",
+ "\n",
+ " if (classes_df is None or classes_df.empty) and (\n",
+ " functions_df is None or functions_df.empty\n",
+ " ):\n",
+ " return \"\"\n",
+ " elif functions_df is None or functions_df.empty:\n",
+ " return convert_search_df_to_json(classes_df)\n",
+ " elif classes_df is None or classes_df.empty:\n",
+ " return convert_search_df_to_json(functions_df)\n",
+ " else:\n",
+ " return convert_search_df_to_json(functions_df)\n",
+ "\n",
+ "def semantic_search(query):\n",
+ " logger.info(f\"Searching the codebase for: {query}\")\n",
+ " return convert_search_df_to_json(\n",
+ " search_service.semantic_search_similar_code(query)\n",
+ " )\n",
+ "\n",
+ "def view_file_functions_and_classes(file_paths):\n",
+ " logger.info(f\"Skimming the code in: {file_paths}\")\n",
+ " results = []\n",
+ " for file_path in file_paths:\n",
+ " full_path = root_path / Path(file_path)\n",
+ "\n",
+ " if not full_path.exists():\n",
+ " results.append(f\"File not found: {file_path}\")\n",
+ " continue # Skip to the next iteration\n",
+ " elif full_path.is_dir():\n",
+ " results.append(\n",
+ " f\"This is not a file, but a directory, pass a filepath instead: {file_path}\"\n",
+ " )\n",
+ " continue # Skip to the next iteration\n",
+ "\n",
+ " # TODO select the correct filehandler and then summarize file\n",
+ " results.append(pythonfilehandler.summarize_file(full_path))\n",
+ "\n",
+ " return \"\\n\".join(results)\n",
+ "\n",
+ "def create_plan_to_complete_user_task(plan):\n",
+ " return plan"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from repo_gpt.file_handler.generic_code_file_handler import PythonFileHandler\n",
+ "from repo_gpt.search_service import SearchService, convert_search_df_to_json\n",
+ "from pathlib import Path\n",
+ "from repo_gpt.openai_service import OpenAIService\n",
+ "\n",
+ "pythonfilehandler = PythonFileHandler()\n",
+ "root_path = Path(\"/Users/shrutipatel/projects/work/repo-gpt/\")\n",
+ "embedding_path = root_path / \".repo_gpt/code_embeddings.pkl\"\n",
+ "openai_service = OpenAIService(OPENAI_API_KEY)\n",
+ "search_service = SearchService(openai_service, embedding_path)\n",
+ " \n",
+ "\n",
+ "def completed_all_code_updates(code_changes):\n",
+ " return code_changes\n",
+ "\n",
+ "def create_file(file_path, content):\n",
+ " \"\"\"\n",
+ " Create a new file with the provided content.\n",
+ "\n",
+ " Args:\n",
+ " - file_path (str): Path to the new file to be created.\n",
+ " - content (str): Content to write in the new file.\n",
+ "\n",
+ " Returns:\n",
+ " - str: Success or error message.\n",
+ " \"\"\"\n",
+ " full_path = root_path / Path(file_path)\n",
+ "\n",
+ " # Check if file already exists\n",
+ " if full_path.exists():\n",
+ " return (\n",
+ " f\"File {file_path} already exists. To update it, use append_to_file().\"\n",
+ " )\n",
+ "\n",
+ " with open(full_path, \"w\") as f:\n",
+ " f.write(content)\n",
+ "\n",
+ " return f\"File {file_path} has been created successfully.\"\n",
+ "\n",
+ "def append_to_file(file_path, content):\n",
+ " \"\"\"\n",
+ " Append content to an existing file.\n",
+ "\n",
+ " Args:\n",
+ " - file_path (str): Path to the file to be updated.\n",
+ " - content (str): Content to append in the file.\n",
+ "\n",
+ " Returns:\n",
+ " - str: Success or error message.\n",
+ " \"\"\"\n",
+ " full_path = root_path / Path(file_path)\n",
+ "\n",
+ " # Check if file exists\n",
+ " if not full_path.exists():\n",
+ " return f\"File {file_path} does not exist. To create it, use create_file().\"\n",
+ "\n",
+ " with open(full_path, \"a\") as f:\n",
+ " f.write(content)\n",
+ "\n",
+ " return f\"Content has been appended to {file_path} successfully.\"\n",
+ "\n",
+ "def view_function_code(function_name):\n",
+ " logger.info(f\"Reading the code for: {function_name}\")\n",
+ " functions_df, classes_df = search_service.find_function_match(\n",
+ " function_name\n",
+ " )\n",
+ "\n",
+ " if (classes_df is None or classes_df.empty) and (\n",
+ " functions_df is None or functions_df.empty\n",
+ " ):\n",
+ " return \"\"\n",
+ " elif functions_df is None or functions_df.empty:\n",
+ " return convert_search_df_to_json(classes_df)\n",
+ " elif classes_df is None or classes_df.empty:\n",
+ " return convert_search_df_to_json(functions_df)\n",
+ " else:\n",
+ " return convert_search_df_to_json(functions_df)\n",
+ "\n",
+ "def semantic_search(query):\n",
+ " logger.info(f\"Searching the codebase for: {query}\")\n",
+ " return convert_search_df_to_json(\n",
+ " search_service.semantic_search_similar_code(query)\n",
+ " )\n",
+ "\n",
+ "def view_file_functions_and_classes(file_paths):\n",
+ " logger.info(f\"Skimming the code in: {file_paths}\")\n",
+ " results = []\n",
+ " for file_path in file_paths:\n",
+ " full_path = root_path / Path(file_path)\n",
+ "\n",
+ " if not full_path.exists():\n",
+ " results.append(f\"File not found: {file_path}\")\n",
+ " continue # Skip to the next iteration\n",
+ " elif full_path.is_dir():\n",
+ " results.append(\n",
+ " f\"This is not a file, but a directory, pass a filepath instead: {file_path}\"\n",
+ " )\n",
+ " continue # Skip to the next iteration\n",
+ "\n",
+ " # TODO select the correct filehandler and then summarize file\n",
+ " results.append(pythonfilehandler.summarize_file(full_path))\n",
+ "\n",
+ " return \"\\n\".join(results)\n",
+ "\n",
+ "def create_plan_to_complete_user_task(plan):\n",
+ " return plan"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import autogen\n",
+ "import os\n",
+ "import json\n",
+ "\n",
+ "import tempfile\n",
+ "from dotenv import find_dotenv, load_dotenv\n",
+ "load_dotenv(find_dotenv())\n",
+ "\n",
+ "env_var = [\n",
+ "{\n",
+ " 'model': 'gpt-3.5-turbo',\n",
+ " 'api_key': OPENAI_API_KEY,\n",
+ " },\n",
+ " {\n",
+ " 'model': 'gpt-3.5-turbo-16k',\n",
+ " 'api_key': OPENAI_API_KEY,\n",
+ " },\n",
+ " {\n",
+ " 'model': 'gpt-4-32k',\n",
+ " 'api_key': OPENAI_API_KEY,\n",
+ " },\n",
+ "]\n",
+ "\n",
+ "# Create a temporary file\n",
+ "# Write the JSON structure to a temporary file and pass it to config_list_from_json\n",
+ "with tempfile.NamedTemporaryFile(mode='w+', delete=True) as temp:\n",
+ " env_var = json.dumps(env_var)\n",
+ " temp.write(env_var)\n",
+ " temp.flush()\n",
+ "\n",
+ "# OAI_CONFIG_LIST = json.dumps([\n",
+ "# {\n",
+ "# 'model': 'gpt-3.5-turbo',\n",
+ "# 'api_key': OPENAI_API_KEY,\n",
+ "# },\n",
+ "# {\n",
+ "# 'model': 'gpt-3.5-turbo-16k',\n",
+ "# 'api_key': OPENAI_API_KEY,\n",
+ "# },\n",
+ "# {\n",
+ "# 'model': 'gpt-4-32k',\n",
+ "# 'api_key': OPENAI_API_KEY,\n",
+ "# },\n",
+ "# ])\n",
+ "\n",
+ "# Set it as an environment variable\n",
+ "# os.environ['OAI_CONFIG'] = OAI_CONFIG_LIST\n",
+ "\n",
+ " config_list_gpt4 = autogen.config_list_from_json(\n",
+ " temp.name,\n",
+ " filter_dict={\n",
+ " \"model\": [\"gpt-3.5-turbo\", \"gpt-3.5-turbo-16k\"],\n",
+ " },\n",
+ " )\n",
+ "\n",
+ "source_code_librarian_config = {\n",
+ " \"functions\": [\n",
+ " {\n",
+ " \"name\": \"semantic_search\",\n",
+ " \"description\": \"Use this function to search the entire codebase semantically. The input should be the search query string.\",\n",
+ " \"parameters\": {\n",
+ " \"type\": \"object\",\n",
+ " \"properties\": {\n",
+ " \"query\": {\n",
+ " \"type\": \"string\",\n",
+ " \"description\": f\"\"\"\n",
+ " The semantic search query to use to search the code base.\n",
+ " \"\"\",\n",
+ " }\n",
+ " },\n",
+ " \"required\": [\"query\"],\n",
+ " },\n",
+ " },\n",
+ " {\n",
+ " \"name\": \"view_function_code\",\n",
+ " \"description\": \"Use this function to search for and view a function's code in the user's codebase. Input should be the name of the function you want to search for. An empty response means the given files don't exist.\",\n",
+ " \"parameters\": {\n",
+ " \"type\": \"object\",\n",
+ " \"properties\": {\n",
+ " \"function_name\": {\n",
+ " \"type\": \"string\",\n",
+ " \"description\": f\"\"\"\n",
+ " The name of the function or its description.\n",
+ " \"\"\",\n",
+ " }\n",
+ " },\n",
+ " \"required\": [\"function_name\"],\n",
+ " },\n",
+ " },\n",
+ " {\n",
+ " \"name\": \"view_file_functions_and_classes\",\n",
+ " \"description\": \"Use this function to retrieve a list of the functions and classes in a file from the user's codebase. An empty response means the given files don't exist.\",\n",
+ " \"parameters\": {\n",
+ " \"type\": \"object\",\n",
+ " \"properties\": {\n",
+ " \"file_paths\": {\n",
+ " \"type\": \"array\",\n",
+ " \"items\": {\n",
+ " \"type\": \"string\",\n",
+ " \"description\": \"An array of one or more file paths of a file you want to retrieve functions and classes from. If a file doesn't exist, the function will return a string saying so.\",\n",
+ " },\n",
+ " \"description\": f\"\"\"\n",
+ " The file paths of the files you want to retrieve functions and classes for to better understand the user's task. Below are the files within the user's repository:\n",
+ " {get_relative_path_directory_structure(\"/Users/shrutipatel/projects/work/repo-gpt\")}\n",
+ " \"\"\",\n",
+ " }\n",
+ " },\n",
+ " \"required\": [\"file_paths\"],\n",
+ " },\n",
+ " },\n",
+ " {\n",
+ " \"name\": \"create_plan_to_complete_user_task\",\n",
+ " \"description\": \"Use this function when you understand the user's task and have a detailed plan ready for completing the user's task. The input should be a step-by-step plan on how to complete the user's task. It can include things like 'Create a new file with a given file path', 'Add the given code to the file', etc.\",\n",
+ " \"parameters\": {\n",
+ " \"type\": \"object\",\n",
+ " \"properties\": {\n",
+ " \"plan\": {\n",
+ " \"type\": \"string\",\n",
+ " \"description\": f\"\"\"\n",
+ " A step-by-step plan on how to complete the user's task. It can include things like \"Create a new file with a given file path\", \"Add the given code to the file\", etc.\n",
+ " \"\"\",\n",
+ " }\n",
+ " },\n",
+ " \"required\": [\"plan\"],\n",
+ " },\n",
+ " },\n",
+ " ],\n",
+ " \"config_list\": config_list_gpt4,\n",
+ " \"request_timeout\": 120,\n",
+ "}\n",
+ "\n",
+ "\n",
+ "engineer_config = {\n",
+ " \"functions\": [\n",
+ " {\n",
+ " \"name\": \"create_file\",\n",
+ " \"description\": \"Create a new file with the provided content.\",\n",
+ " \"parameters\": {\n",
+ " \"type\": \"object\",\n",
+ " \"properties\": {\n",
+ " \"file_path\": {\n",
+ " \"type\": \"string\",\n",
+ " \"description\": \"Path to the new file to be created.\",\n",
+ " },\n",
+ " \"content\": {\n",
+ " \"type\": \"string\",\n",
+ " \"description\": \"Content to write in the new file.\",\n",
+ " },\n",
+ " },\n",
+ " \"required\": [\"file_path\", \"content\"],\n",
+ " },\n",
+ " },\n",
+ " {\n",
+ " \"name\": \"append_to_file\",\n",
+ " \"description\": \"Append content to an existing file.\",\n",
+ " \"parameters\": {\n",
+ " \"type\": \"object\",\n",
+ " \"properties\": {\n",
+ " \"file_path\": {\n",
+ " \"type\": \"string\",\n",
+ " \"description\": \"Path to the file to be updated.\",\n",
+ " },\n",
+ " \"content\": {\n",
+ " \"type\": \"string\",\n",
+ " \"description\": \"Content to append to the file.\",\n",
+ " },\n",
+ " },\n",
+ " \"required\": [\"file_path\", \"content\"],\n",
+ " },\n",
+ " },\n",
+ " {\n",
+ " \"name\": \"completed_all_code_updates\",\n",
+ " \"description\": \"Call this function when all the code updates are completed.\",\n",
+ " \"parameters\": {\n",
+ " \"type\": \"object\",\n",
+ " \"properties\": {\n",
+ " \"code_changes\": {\n",
+ " \"type\": \"string\",\n",
+ " \"description\": \"Enumeration of all the changes that were made to the code.\",\n",
+ " }\n",
+ " },\n",
+ " \"required\": [\"code_changes\"],\n",
+ " },\n",
+ " },\n",
+ " ],\n",
+ " \"config_list\": config_list_gpt4,\n",
+ " \"request_timeout\": 120,\n",
+ "}\n",
+ "\n",
+ " "
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "It first looks for environment variable \"OAI_CONFIG_LIST\" which needs to be a valid json string. If that variable is not found, it then looks for a json file named \"OAI_CONFIG_LIST\". It filters the configs by models (you can filter by other keys as well).\n",
+ "\n",
+ "The config list looks like the following:\n",
+ "```python\n",
+ "config_list = [\n",
+ " {\n",
+ " 'model': 'gpt-4-32k',\n",
+ " 'api_key': '',\n",
+ " },\n",
+ " {\n",
+ " 'model': 'gpt-4-32k',\n",
+ " 'api_key': '',\n",
+ " 'api_base': '',\n",
+ " 'api_type': 'azure',\n",
+ " 'api_version': '2023-06-01-preview',\n",
+ " },\n",
+ " {\n",
+ " 'model': 'gpt-4-32k-0314',\n",
+ " 'api_key': '',\n",
+ " 'api_base': '',\n",
+ " 'api_type': 'azure',\n",
+ " 'api_version': '2023-06-01-preview',\n",
+ " },\n",
+ "]\n",
+ "```\n",
+ "\n",
+ "If you open this notebook in colab, you can upload your files by clicking the file icon on the left panel and then choose \"upload file\" icon.\n",
+ "\n",
+ "You can set the value of config_list in other ways you prefer, e.g., loading from a YAML file."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Plan\n",
+ "1. Create a UseProxyAgent + Assistant for searching the codebase\n",
+ "2. If code updates are necessary, generate a detailed plan of next steps\n",
+ "3. Engineer + UserProxyAgent writes the code\n",
+ "4. Have a Critic + UserProxyAgent that reviews the code (add functions for reading git diffs)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Create simple agent structure\n",
+ "[Example Notebook](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_two_users.ipynb)"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Construct Agents"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "gpt4_config = {\n",
+ " \"seed\": 42, # change the seed for different trials\n",
+ " \"temperature\": 0,\n",
+ " \"config_list\": config_list_gpt4,\n",
+ " \"request_timeout\": 120,\n",
+ "}\n",
+ "user_proxy = autogen.UserProxyAgent(\n",
+ " name=\"Admin\",\n",
+ " system_message=\"A human admin. Interact with the planner to discuss the plan. Plan execution needs to be approved by this admin.\",\n",
+ " code_execution_config=False,\n",
+ ")\n",
+ "engineer = autogen.UserProxyAgent(\n",
+ " name=\"Engineer\",\n",
+ " llm_config=engineer_config,\n",
+ " human_input_mode=\"NEVER\",\n",
+ " system_message='''Engineer. You follow an approved plan. You write python/shell code to solve tasks. Wrap the code in a code block that specifies the script type. The user can't modify your code. So do not suggest incomplete code which requires others to modify. Don't use a code block if it's not intended to be executed by the executor.\n",
+ "Don't include multiple code blocks in one response. Do not ask others to copy and paste the result. Check the execution result returned by the executor.\n",
+ "If the result indicates there is an error, fix the error and output the code again. Suggest the full code instead of partial code or code changes. 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.\n",
+ "''',\n",
+ ")\n",
+ "engineer.register_function(\n",
+ " function_map={\n",
+ " \"create_file\": create_file,\n",
+ " \"append_to_file\": append_to_file,\n",
+ " \"completed_all_code_updates\": completed_all_code_updates,\n",
+ " })\n",
+ "\n",
+ "#\"Repository Manager\" or \"Source Code Librarian\"\n",
+ "source_code_librarian = autogen.UserProxyAgent(\n",
+ " name=\"Architect\",\n",
+ " llm_config=source_code_librarian_config,\n",
+ " human_input_mode=\"NEVER\",\n",
+ " system_message=\"\"\"Architect. You browse the codebase to understand the larger structure, dependencies, metadata, and code logic. \n",
+ "Based on your browsing, you answer questions about the code base and create code architecture plans that fit the codebase's existing patterns.\n",
+ "You don't write code. You tell the Engineer what to code.\n",
+ " \"\"\"\n",
+ ")\n",
+ "source_code_librarian.register_function(\n",
+ " function_map={\n",
+ " \"semantic_search\": semantic_search,\n",
+ " \"view_function_code\": view_function_code,\n",
+ " \"view_file_functions_and_classes\": view_file_functions_and_classes,\n",
+ " \"create_plan_to_complete_user_task\": create_plan_to_complete_user_task,\n",
+ " })\n",
+ "planner = autogen.AssistantAgent(\n",
+ " name=\"Coordinator\",\n",
+ " system_message='''Coordinator. First, ask for the architect's input and then suggest a plan. \n",
+ "The plan may involve an engineer who can write code and an architect who searches the existing codebase.\n",
+ "Explain the plan first. Be clear about which step is performed by an engineer, and which is performed by an architect.\n",
+ "Revise the plan based on findings from the architect and engineer if necessary.\n",
+ "''',\n",
+ " llm_config=gpt4_config,\n",
+ ")\n",
+ "\n",
+ "# TODO replace with test writer\n",
+ "executor = autogen.UserProxyAgent(\n",
+ " name=\"Executor\",\n",
+ " system_message=\"Executor. Execute the code written by the engineer and report the result.\",\n",
+ " human_input_mode=\"NEVER\",\n",
+ " code_execution_config={\"work_dir\": \"coding\"},\n",
+ ")\n",
+ "critic = autogen.AssistantAgent(\n",
+ " name=\"Critic\",\n",
+ " system_message=\"Critic. Double check plan, claims, code from other agents and provide feedback. Check whether the plan includes adding verifiable info such as source URL.\",\n",
+ " llm_config=gpt4_config,\n",
+ ")\n",
+ "# groupchat = autogen.GroupChat(agents=[user_proxy, engineer, source_code_librarian, planner, executor, critic], messages=[], max_round=50)\n",
+ "groupchat = autogen.GroupChat(agents=[user_proxy, engineer, source_code_librarian, executor, critic], messages=[], max_round=50)\n",
+ "manager = autogen.GroupChatManager(groupchat=groupchat, llm_config=gpt4_config)\n",
+ "\n",
+ "manager.register_function(\n",
+ " function_map={\n",
+ " \"semantic_search\": semantic_search,\n",
+ " \"view_function_code\": view_function_code,\n",
+ " \"view_file_functions_and_classes\": view_file_functions_and_classes,\n",
+ " \"create_plan_to_complete_user_task\": create_plan_to_complete_user_task,\n",
+ " \"create_file\": create_file,\n",
+ " \"append_to_file\": append_to_file,\n",
+ " \"completed_all_code_updates\": completed_all_code_updates,\n",
+ " })"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Start Chat"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\u001b[33mAdmin\u001b[0m (to chat_manager):\n",
+ "\n",
+ "\n",
+ "Add a code file handler for Elixir.\n",
+ "\n",
+ "\n",
+ "--------------------------------------------------------------------------------\n",
+ "\u001b[33mEngineer\u001b[0m (to chat_manager):\n",
+ "\n",
+ "\u001b[32m***** Suggested function Call: append_to_file *****\u001b[0m\n",
+ "Arguments: \n",
+ "{\n",
+ " \"file_path\": \"handlers.json\",\n",
+ " \"content\": \",\\n \\\"elixir\\\": \\\"elixir_handler.py\\\"\"\n",
+ "}\n",
+ "\u001b[32m***************************************************\u001b[0m\n",
+ "\n",
+ "--------------------------------------------------------------------------------\n",
+ "\u001b[33mArchitect\u001b[0m (to chat_manager):\n",
+ "\n",
+ "\u001b[32m***** Response from calling function \"append_to_file\" *****\u001b[0m\n",
+ "Error: Function append_to_file not found.\n",
+ "\u001b[32m***********************************************************\u001b[0m\n",
+ "\n",
+ "--------------------------------------------------------------------------------\n",
+ "\u001b[33mArchitect\u001b[0m (to chat_manager):\n",
+ "\n",
+ "\u001b[32m***** Suggested function Call: view_function_code *****\u001b[0m\n",
+ "Arguments: \n",
+ "{\n",
+ " \"function_name\": \"append_to_file\"\n",
+ "}\n",
+ "\u001b[32m*******************************************************\u001b[0m\n",
+ "\n",
+ "--------------------------------------------------------------------------------\n",
+ "\u001b[33mEngineer\u001b[0m (to chat_manager):\n",
+ "\n",
+ "\u001b[32m***** Response from calling function \"view_function_code\" *****\u001b[0m\n",
+ "Error: Function view_function_code not found.\n",
+ "\u001b[32m***************************************************************\u001b[0m\n",
+ "\n",
+ "--------------------------------------------------------------------------------\n"
+ ]
+ }
+ ],
+ "source": [
+ "user_proxy.initiate_chat(\n",
+ " manager,\n",
+ " message=\"\"\"\n",
+ "Add a code file handler for Elixir.\n",
+ "\"\"\",\n",
+ ")"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Create Group Chat without Critic for Comparison"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "groupchat_nocritic = autogen.GroupChat(agents=[user_proxy, engineer, scientist, planner, executor], messages=[], max_round=50)\n",
+ "for agent in groupchat.agents:\n",
+ " agent.reset()\n",
+ "manager_nocritic = autogen.GroupChatManager(groupchat=groupchat_nocritic, llm_config=gpt4_config)\n",
+ "user_proxy.initiate_chat(\n",
+ " manager_nocritic,\n",
+ " message=\"\"\"\n",
+ "find papers on LLM applications from arxiv in the last week, create a markdown table of different domains.\n",
+ "\"\"\",\n",
+ ")"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "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.11.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/expt/autogen-tools.ipynb b/expt/autogen-tools.ipynb
new file mode 100644
index 0000000..4fd2dfd
--- /dev/null
+++ b/expt/autogen-tools.ipynb
@@ -0,0 +1,445 @@
+{
+ "cells": [
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "ae1f50ec",
+ "metadata": {},
+ "source": [
+ ""
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "9a71fa36",
+ "metadata": {},
+ "source": [
+ "# Auto Generated Agent Chat: Task Solving with Provided Tools as Functions\n",
+ "\n",
+ "AutoGen offers conversable agents powered by LLM, tool or human, which can be used to perform tasks collectively via automated chat. This framwork allows tool use and human participance through multi-agent conversation. Please find documentation about this feature [here](https://microsoft.github.io/autogen/docs/Use-Cases/agent_chat).\n",
+ "\n",
+ "In this notebook, we demonstrate how to use `AssistantAgent` and `UserProxyAgent` to make function calls with the new feature of OpenAI models (in model version 0613). A specified prompt and function configs need to be passed to `AssistantAgent` to initialize the agent. The corresponding functions need to be passed to `UserProxyAgent`, which will be responsible for executing any function calls made by `AssistantAgent`. Besides this requirement of matching descriptions with functions, we recommend checking the system message in the `AssistantAgent` to make sure the instructions align with the function call descriptions.\n",
+ "\n",
+ "## Requirements\n",
+ "\n",
+ "AutoGen requires `Python>=3.8`. To run this notebook example, please install the [mathchat] option since we will import functions from `MathUserProxyAgent`:\n",
+ "```bash\n",
+ "pip install \"pyautogen[mathchat]\"\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "2b803c17",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# %pip install \"pyautogen[mathchat]~=0.1.0\""
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "5ebd2397",
+ "metadata": {},
+ "source": [
+ "## Set your API Endpoint\n",
+ "\n",
+ "The [`config_list_from_models`](https://microsoft.github.io/autogen/docs/reference/oai/openai_utils#config_list_from_models) function tries to create a list of configurations using Azure OpenAI endpoints and OpenAI endpoints for the provided list of models. It assumes the api keys and api bases are stored in the corresponding environment variables or local txt files:\n",
+ "\n",
+ "- OpenAI API key: os.environ[\"OPENAI_API_KEY\"] or `openai_api_key_file=\"key_openai.txt\"`.\n",
+ "- Azure OpenAI API key: os.environ[\"AZURE_OPENAI_API_KEY\"] or `aoai_api_key_file=\"key_aoai.txt\"`. Multiple keys can be stored, one per line.\n",
+ "- Azure OpenAI API base: os.environ[\"AZURE_OPENAI_API_BASE\"] or `aoai_api_base_file=\"base_aoai.txt\"`. Multiple bases can be stored, one per line.\n",
+ "\n",
+ "It's OK to have only the OpenAI API key, or only the Azure OpenAI API key + base.\n",
+ "If you open this notebook in google colab, you can upload your files by click the file icon on the left panel and then choose \"upload file\" icon.\n",
+ "\n",
+ "The following code excludes Azure OpenAI endpoints from the config list because some endpoints don't support functions yet. Remove the `exclude` argument if they do."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "dca301a4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import autogen\n",
+ "\n",
+ "config_list = autogen.config_list_from_models(model_list=[\"gpt-4\", \"gpt-3.5-turbo\", \"gpt-3.5-turbo-16k\"], exclude=\"aoai\")"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "92fde41f",
+ "metadata": {},
+ "source": [
+ "The config list looks like the following:\n",
+ "```python\n",
+ "config_list = [\n",
+ " {\n",
+ " 'model': 'gpt-4',\n",
+ " 'api_key': '',\n",
+ " }, # OpenAI API endpoint for gpt-4\n",
+ " {\n",
+ " 'model': 'gpt-3.5-turbo',\n",
+ " 'api_key': '',\n",
+ " }, # OpenAI API endpoint for gpt-3.5-turbo\n",
+ " {\n",
+ " 'model': 'gpt-3.5-turbo-16k',\n",
+ " 'api_key': '',\n",
+ " }, # OpenAI API endpoint for gpt-3.5-turbo-16k\n",
+ "]\n",
+ "```\n"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "2b9526e7",
+ "metadata": {},
+ "source": [
+ "## Making Function Calls\n",
+ "\n",
+ "In this example, we demonstrate function call execution with `AssistantAgent` and `UserProxyAgent`. With the default system prompt of `AssistantAgent`, we allow the LLM assistant to perform tasks with code, and the `UserProxyAgent` would extract code blocks from the LLM response and execute them. With the new \"function_call\" feature, we define functions and specify the description of the function in the OpenAI config for the `AssistantAgent`. Then we register the functions in `UserProxyAgent`.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "9fb85afb",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\u001b[33muser_proxy\u001b[0m (to chatbot):\n",
+ "\n",
+ "Draw two agents chatting with each other with an example dialog. Don't add plt.show().\n",
+ "\n",
+ "--------------------------------------------------------------------------------\n",
+ "\u001b[33mchatbot\u001b[0m (to user_proxy):\n",
+ "\n",
+ "\u001b[32m***** Suggested function Call: python *****\u001b[0m\n",
+ "Arguments: \n",
+ "{\n",
+ " \"cell\": \"\n",
+ "import matplotlib.pyplot as plt\n",
+ "import matplotlib.patches as mpatches\n",
+ "\n",
+ "# Define basic parameters\n",
+ "face_color = '#FFDDC1'\n",
+ "plt.figure(figsize=(10, 2))\n",
+ "\n",
+ "# Agent 1\n",
+ "agent1 = mpatches.FancyBboxPatch((0.02, 0.4), 0.2, 0.6, boxstyle=mpatches.BoxStyle(\\\"Round\\\", pad=0.02))\n",
+ "plt.gca().add_artist(agent1)\n",
+ "plt.gca().text(0.12, 0.7, 'Agent 1', ha='center', va='center', fontsize=12, color='blue')\n",
+ "\n",
+ "# Agent 2\n",
+ "agent2 = mpatches.FancyBboxPatch((0.45, 0.4), 0.2, 0.6, boxstyle=mpatches.BoxStyle(\\\"Round\\\", pad=0.02))\n",
+ "plt.gca().add_artist(agent2)\n",
+ "plt.gca().text(0.55, 0.7, 'Agent 2', ha='center', va='center', fontsize=12, color='red')\n",
+ "\n",
+ "# Dialog\n",
+ "plt.gca().text(0.12, 0.35, '\\\"Hello, how are you?\\\"', ha='center', va='center', fontsize=10)\n",
+ "plt.gca().text(0.55, 0.15, '\\\"I\\'m fine, thank you!\\\"', ha='center', va='center', fontsize=10)\n",
+ "\n",
+ "# Descriptions\n",
+ "plt.gca().text(0.12, 0.15, 'Greeting', ha='center', va='center', fontsize=10)\n",
+ "plt.gca().text(0.55, 0.35, 'Response', ha='center', va='center', fontsize=10)\n",
+ "\n",
+ "plt.axis('off')\n",
+ "\"\n",
+ "}\n",
+ "\u001b[32m*******************************************\u001b[0m\n",
+ "\n",
+ "--------------------------------------------------------------------------------\n",
+ "\u001b[33muser_proxy\u001b[0m (to chatbot):\n",
+ "\n",
+ "\u001b[32m***** Response from calling function \"python\" *****\u001b[0m\n",
+ "Error: Invalid \\escape: line 1 column 785 (char 784)\n",
+ " You argument should follow json format.\n",
+ "\u001b[32m***************************************************\u001b[0m\n",
+ "\n",
+ "--------------------------------------------------------------------------------\n",
+ "\u001b[33mchatbot\u001b[0m (to user_proxy):\n",
+ "\n",
+ "\u001b[32m***** Suggested function Call: python *****\u001b[0m\n",
+ "Arguments: \n",
+ "{\n",
+ " \"cell\": \"import matplotlib.pyplot as plt\\nimport matplotlib.patches as mpatches\\n\\n# Define basic parameters\\nface_color = '#FFDDC1'\\nplt.figure(figsize=(10, 2))\\n\\n# Agent 1\\nagent1 = mpatches.FancyBboxPatch((0.02, 0.4), 0.2, 0.6, boxstyle=mpatches.BoxStyle('Round', pad=0.02))\\nplt.gca().add_artist(agent1)\\nplt.gca().text(0.12, 0.7, 'Agent 1', ha='center', va='center', fontsize=12, color='blue')\\n\\n# Agent 2\\nagent2 = mpatches.FancyBboxPatch((0.45, 0.4), 0.2, 0.6, boxstyle=mpatches.BoxStyle('Round', pad=0.02))\\nplt.gca().add_artist(agent2)\\nplt.gca().text(0.55, 0.7, 'Agent 2', ha='center', va='center', fontsize=12, color='red')\\n\\n# Dialog\\nplt.gca().text(0.12, 0.35, '\\\"Hello, how are you?\\\"', ha='center', va='center', fontsize=10)\\nplt.gca().text(0.55, 0.15, '\\\"I\\\\'m fine, thank you!\\\"', ha='center', va='center', fontsize=10)\\n\\n# Descriptions\\nplt.gca().text(0.12, 0.15, 'Greeting', ha='center', va='center', fontsize=10)\\nplt.gca().text(0.55, 0.35, 'Response', ha='center', va='center', fontsize=10)\\n\\nplt.axis('off')\"\n",
+ "}\n",
+ "\u001b[32m*******************************************\u001b[0m\n",
+ "\n",
+ "--------------------------------------------------------------------------------\n",
+ "\u001b[35m\n",
+ ">>>>>>>> EXECUTING FUNCTION python...\u001b[0m\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "(0.0, 1.0, 0.0, 1.0)"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "data": {
+ "image/png": "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iIhIcgwaREREREQkOQYNIiIiIiKSHIMGERERERFJjkGDiIiIiIgkx6BBRERERESSY9AgIiIiIiLJMWgQEREREZHkGDSIiIiIiEhyDBpERERERCQ5Bg0iIiIiIpIcgwYREREREUmOQYOIiIiIiCTHoEFERERERJJj0CAiIiIiIskxaBARERERkeQYNJ4BMpkMP/74Y12XQUTPuDVr1sDV1RVyuRzLli3DnDlz0Lp167ouCwBw4cIFyGQyxMfHP/Z9ccwlIjIMg8YDunLlCiZNmgRPT0+YmZnBwcEB/v7+WLVqFUpLSx/rvu92Us/JycGrr776WPdNRHVrxIgRmDNnDoCaX3QvXLgAoPYv2DeXpVZUVIQPPvgAM2bMQFZWFsaOHYtp06bh119/lXxf9zNixAgEBQX97ft9GtwegqKjo+Hu7g5A/98QEdHfwaiuC3ianD9/Hv7+/rCxscGCBQvQsmVLmJqaIjExEWvWrIGzszMGDBhQa7uqqioYGxs/trocHR0fW99ERABw8eJFVFVVoW/fvnByctKtt7CwqMOqiIjoScYrGg9g/PjxMDIywh9//IE333wTPj4+8PDwwMCBAxEZGYn+/fsDqPlEadWqVRgwYABUKhW++OILAMDOnTvRtm1bmJmZwcPDA3PnzkV1dbWu/4KCAowZMwb29vawsrJCt27dkJCQAABYt24d5s6di4SEBMhkMshkMqxbt063v5ufYN38NHP79u3o2rUrlEolfH19cfToUb1j+fe//w1XV1colUq89tprCAsLg42NzeN9A4nob7du3TrY2Njg559/hre3N5RKJd544w2UlpYiIiIC7u7usLW1xYcffgiNRnPXPlq2bAkA8PDw0F1Ruf0q680rDV999RWcnJxQr149TJgwAVVVVbo2FRUVmDZtGpydnaFSqfDSSy8hOjra4OOZM2cOIiIisHPnTt1YeOv258+fv+vYl5eXh6FDh8LZ2RlKpRItW7bEpk2b9PoPCAjAhx9+iI8//hh2dnZwdHS871WA0NBQODk54dSpU7Veu3DhAuRyOf744w+99cuWLYObmxu0Wi0A4ODBg3jxxRdhamoKJycnzJw5U+/84O7ujmXLlun10bp1a16hIKInGoOGgfLy8rB3715MmDABKpXqjm1uvV1hzpw5eO2115CYmIhRo0bh8OHDCA4OxqRJk5CcnIzV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sLe3r/VYRkP5+/vjX//6F8LCwuDr64tffvkFU6ZMgZmZ2UP1R0RkqPDwcAQHB2Pq1Knw9vZGUFAQTpw4gYYNG+ra3Pr47jt599134e3tDT8/P9jb2yMmJsagfc+ePRtt27ZFr169EBAQAEdHx0f+4r833ngDEREReOedd7B9+/Z7th09ejQqKysxatQovfXOzs6IiorC8ePH4evri/feew+jR4/WfRAFALNmzUKXLl3Qr18/9O3bF0FBQXrzboCaW6nS0tJQXFz8SMdERCQVmeB1VkLNiTs1NRWHDx+u61KI6DmWkZEBLy8vJCcno0mTJnVdjqTmzZuHH3744Y7ft0FE9CziHI3n1FdffYXAwECoVCrs3r0bERER+Oabb+q6LCJ6zkVFRWHs2LHPVMhQq9W4cOECvv76a343BhE9V3hF4zn15ptvIjo6GsXFxfDw8MDEiRPx3nvv1XVZRETPnBEjRmDTpk0ICgrCxo0b7zmfj4joWcKgQUREREREkuNkcCIiIiIikhyDBhERERERSY5Bg4iIiIiIJMegQUREREREkmPQICIiIiIiyTFoEBERERGR5Bg0iIiIiIhIcgwaREREREQkOQYNIiIiIiKSHIMGERERERFJjkGDiIiIiIgkx6BBRERERESSY9AgIiIiIiLJMWgQEREREZHkGDSIiIiIiEhyDBpERERERCQ5Bg0iIiIiIpIcgwYREREREUmOQYOIiIiIiCTHoEFERERERJJj0CAiIiIiIskxaBARERERkeQYNIiIiIiISHIMGkREREREJDkGDSIiIiIiktz/A0/x9tng3v+UAAAAAElFTkSuQmCC",
+ "text/plain": [
+ "