From 255bf22596f1202b6c73b7de534743c66a5fb5d0 Mon Sep 17 00:00:00 2001
From: Yiran Wu <32823396+kevin666aa@users.noreply.github.com>
Date: Sat, 11 Nov 2023 14:57:14 -0500
Subject: [PATCH] Fix test error of compressible agent (#631)
* fix bug in test
* update workflow
* update
* deepcopy to copy
---
.github/workflows/build.yml | 2 +-
.github/workflows/openai.yml | 2 +-
.../agentchat/contrib/compressible_agent.py | 2 +-
notebook/agentchat_compression.ipynb | 2582 ++++++++---------
.../contrib/test_compressible_agent.py | 20 +-
5 files changed, 1304 insertions(+), 1304 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5bc831913ae..3cdb6293b27 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -55,7 +55,7 @@ jobs:
run: |
pip install -e .[mathchat,test]
pip uninstall -y openai
- coverage run -a -m pytest test
+ coverage run -a -m pytest test --ignore=test/agentchat/contrib
coverage xml
- name: Upload coverage to Codecov
if: matrix.python-version == '3.10'
diff --git a/.github/workflows/openai.yml b/.github/workflows/openai.yml
index 6f966337bed..b9184fd5268 100644
--- a/.github/workflows/openai.yml
+++ b/.github/workflows/openai.yml
@@ -54,7 +54,7 @@ jobs:
AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }}
OAI_CONFIG_LIST: ${{ secrets.OAI_CONFIG_LIST }}
run: |
- coverage run -a -m pytest test
+ coverage run -a -m pytest test --ignore=test/agentchat/contrib
coverage xml
- name: Coverage and check notebook outputs
if: matrix.python-version != '3.9'
diff --git a/autogen/agentchat/contrib/compressible_agent.py b/autogen/agentchat/contrib/compressible_agent.py
index 15e641c0244..f1de41512e9 100644
--- a/autogen/agentchat/contrib/compressible_agent.py
+++ b/autogen/agentchat/contrib/compressible_agent.py
@@ -139,7 +139,7 @@ def _set_compress_config(self, compress_config: Optional[Dict] = False):
if compress_config.get("mode", "TERMINATE") not in allowed_modes:
raise ValueError(f"Invalid compression mode. Allowed values are: {', '.join(allowed_modes)}")
- self.compress_config = self.DEFAULT_COMPRESS_CONFIG
+ self.compress_config = self.DEFAULT_COMPRESS_CONFIG.copy()
self.compress_config.update(compress_config)
if not isinstance(self.compress_config["leave_last_n"], int) or self.compress_config["leave_last_n"] < 0:
diff --git a/notebook/agentchat_compression.ipynb b/notebook/agentchat_compression.ipynb
index 40dc8a92497..295d1691856 100644
--- a/notebook/agentchat_compression.ipynb
+++ b/notebook/agentchat_compression.ipynb
@@ -1,1295 +1,1295 @@
{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- ""
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Auto Generated Agent Chat: Conversations with Chat History Compression Enabled (Experimental)\n",
- "\n",
- "AutoGen offers conversable agents powered by LLM, tools, or humans, which can be used to perform tasks collectively via automated chat. This framework 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 enable compression of history messages using the `CompressibleAgent`. While this agent retains all the default functionalities of the `AssistantAgent`, it also provides the added feature of compression when activated through the `compress_config` setting.\n",
- "\n",
- "Different compression modes are supported:\n",
- "1. `compress_config=False` (Default): `CompressibleAgent` is equivalent to `AssistantAgent`.\n",
- "2. `compress_config=True` or `compress_config={\"mode\": \"TERMINATE\"}`: no compression will be performed. However, we will count token usage before sending requests to the OpenAI model. The conversation will be terminated directly if the total token usage exceeds the maximum token usage allowed by the model (to avoid the token limit error from OpenAI API).\n",
- "3. `compress_config={\"mode\": \"COMPRESS\", \"trigger_count\": }, \"leave_last_n\": `: compression is enabled.\n",
- " ```python\n",
- " # default compress_config\n",
- " compress_config = {\n",
- " \"mode\": \"COMPRESS\",\n",
- " \"compress_function\": None,\n",
- " \"trigger_count\": 0.7, # default to 0.7, or your pre-set number\n",
- " \"broadcast\": True, # the compressed with be broadcast to sender. This will not be used in groupchat.\n",
- "\n",
- " # the following settings are for this mode only\n",
- " \"leave_last_n\": 2, # leave the last n messages in the history to avoid compression\n",
- " \"verbose\": False, # if True, print out the content to be compressed and the compressed content\n",
- " }\n",
- " ```\n",
- " Currently, our compression logic is as follows:\n",
- " 1. We will always leave the first user message (as well as system prompts) and compress the rest of the history messages.\n",
- " 2. You can choose to not compress the last n messages in the history with \"leave_last_n\".\n",
- " 2. The summary is performed on a per-message basis, with the role of the messages (See compressed content in the example below).\n",
- "\n",
- "4. `compress_config={\"mode\": \"CUSTOMIZED\", \"compress_function\": }`: the `compress_function` function will be called on trigger count. The function should accept a list of messages as input and return a tuple of (is_success: bool, compressed_messages: List[Dict]). The whole message history (except system prompt) will be passed.\n",
- "\n",
- "\n",
- "By adjusting `trigger_count`, you can decide when to compress the history messages based on existing tokens. If this is a float number between 0 and 1, it is interpreted as a ratio of max tokens allowed by the model. For example, the AssistantAgent uses gpt-4 with max tokens 8192, the trigger_count = 0.7 * 8192 = 5734.4 -> 5734. Do not set `trigger_count` to the max tokens allowed by the model, since the same LLM is employed for compression and it needs tokens to generate the compressed content. \n",
- "\n",
- "\n",
- "\n",
- "## Limitations\n",
- "- For now, the compression feature **is not well-supported for groupchat**. If you initialize a `CompressibleAgent` in a groupchat with compression, the compressed cannot be broadcast to all other agents in the groupchat. If you use this feature in groupchat, extra cost will be incurred since compression will be performed on at per-agent basis.\n",
- "- We do not support async compression for now.\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": [
- "# %pip install pyautogen~=0.1.0"
- ]
- },
- {
- "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.\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import autogen\n",
- "config_list = autogen.config_list_from_json(\n",
- " \"OAI_CONFIG_LIST\",\n",
- " filter_dict={\n",
- " \"model\": [\"gpt-4\", \"gpt-4-0314\", \"gpt4\", \"gpt-4-32k\", \"gpt-4-32k-0314\", \"gpt-4-32k-v0314\"],\n",
- " },\n",
- ")"
- ]
- },
- {
- "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',\n",
- " 'api_key': '',\n",
- " },\n",
- " {\n",
- " 'model': 'gpt-4',\n",
- " 'api_key': '',\n",
- " 'base_url': '',\n",
- " 'api_type': 'azure',\n",
- " 'api_version': '2023-06-01-preview',\n",
- " },\n",
- " {\n",
- " 'model': 'gpt-4-32k',\n",
- " 'api_key': '',\n",
- " 'base_url': '',\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": [
- "## Example 1\n",
- "This example is from [agentchat_MathChat.ipynb](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_MathChat.ipynb). Compression with code execution.\n",
- "\n",
- "Note: we set `trigger_count=600`, and `leave_last_n=2`. In this example, we set a low trigger_count to demonstrate the compression feature. \n",
- "The token count after compression is still bigger than trigger count, mainly because the trigger count is low an the first and last 2 messages are not compressed. Thus, the compression is performed at each turn. In practice, you want to adjust the trigger_count to a bigger number and properly set the `leave_last_n` to avoid compression at each turn. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "\u001b[33mmathproxyagent\u001b[0m (to assistant):\n",
- "\n",
- "Let's use Python to solve a math problem.\n",
- "\n",
- "Query requirements:\n",
- "You should always use the 'print' function for the output and use fractions/radical forms instead of decimals.\n",
- "You can use packages like sympy to help you.\n",
- "You must follow the formats below to write your code:\n",
- "```python\n",
- "# your code\n",
- "```\n",
- "\n",
- "First state the key idea to solve the problem. You may choose from three ways to solve the problem:\n",
- "Case 1: If the problem can be solved with Python code directly, please write a program to solve it. You can enumerate all possible arrangements if needed.\n",
- "Case 2: If the problem is mostly reasoning, you can solve it by yourself directly.\n",
- "Case 3: If the problem cannot be handled in the above two ways, please follow this process:\n",
- "1. Solve the problem step by step (do not over-divide the steps).\n",
- "2. Take out any queries that can be asked through Python (for example, any calculations or equations that can be calculated).\n",
- "3. Wait for me to give the results.\n",
- "4. Continue if you think the result is correct. If the result is invalid or unexpected, please correct your query or reasoning.\n",
- "\n",
- "After all the queries are run and you get the answer, put the answer in \\boxed{}.\n",
- "\n",
- "Problem:\n",
- "Find all $x$ that satisfy the inequality $(2x+10)(x+3)<(3x+9)(x+8)$. Express your answer in interval notation.\n",
- "\n",
- "--------------------------------------------------------------------------------\n",
- "\u001b[33massistant\u001b[0m (to mathproxyagent):\n",
- "\n",
- "First, we need to consider both sides of the inequality as two separate equations. Then subtract one from the other to set this result equal to zero. This would allow us to find the critical points, i.e., the places where the inequality changes its nature (from less to more or vice versa). Then we find the intervals.\n",
- "\n",
- "Here's how you can solve this problem in Python:\n",
- "\n",
- "```python\n",
- "from sympy import *\n",
- "from sympy.abc import x\n",
- "\n",
- "# define the equation\n",
- "equation = (2*x+10)*(x+3) - (3*x+9)*(x+8)\n",
- "\n",
- "# set the equation equal to zero to find the critical points\n",
- "critical_points = solve(equation, x)\n",
- "\n",
- "# Sort the critical_points\n",
- "critical_points = sorted(critical_points)\n",
- "\n",
- "# define a function to test the intervals\n",
- "def test_intervals(interval):\n",
- " test_num = sum(interval)/2 # get the mid point of the interval\n",
- " return equation.subs(x, test_num)\n",
- "\n",
- "# define the intervals based on the critical points\n",
- "intervals = [(-oo, critical_points[0]), (critical_points[0], critical_points[1]), (critical_points[1], oo)]\n",
- "\n",
- "solution = []\n",
- "\n",
- "# loop through the intervals, if the result is less than zero, it means it's a valid interval\n",
- "for i in intervals:\n",
- " if test_intervals(i) < 0:\n",
- " solution.append(i)\n",
- "\n",
- "# print the solution in interval notation\n",
- "for interval in solution:\n",
- " print(interval)\n",
- "```\n",
- "\n",
- "Replace oo with infinity when interpreting the result. Also, keep in mind that in interval notation, parentheses denote that the endpoint is not included in the set, and brackets denote that the end point is included in the set. Thus, (a, b) means \"greater than a and less than b\", [a, b] means \"greater than or equal to a and less than or equal to b\".\n",
- "\n",
- "--------------------------------------------------------------------------------\n",
- "\u001b[33mmathproxyagent\u001b[0m (to assistant):\n",
- "\n",
- "Error: Traceback (most recent call last):\n",
- " File \"\", line 25, in \n",
- " if test_intervals(i) < 0:\n",
- " return func(self, other)\n",
- " return StrictLessThan(self, other)\n",
- " raise TypeError(\"Invalid NaN comparison\")\n",
- "TypeError: Invalid NaN comparison\n",
- "\n",
- "--------------------------------------------------------------------------------\n"
- ]
- },
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "Warning: Compression skipped at trigger count threshold. The first msg and last 2 msgs will not be compressed. current msg count: 3. Consider raising trigger_count.\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "\u001b[33massistant\u001b[0m (to mathproxyagent):\n",
- "\n",
- "I apologize for the oversight. It seems like the function is returning NaN error. Because we are dealing with real numbers, NaN (Not a Number) error could arise from operations that don't return a defined real number. \n",
- "\n",
- "However, in this case, we it seems there's a problem with the comparison of symbolic terms with zero in the function test_intervals.\n",
- "\n",
- "Let's correct this approach. The error arises from trying to compare a symbolic expression to zero directly. Instead, we can create a numeric function from our symbolic expression, and use this function to test the intervals. \n",
- "\n",
- "Here is the revised code:\n",
- "\n",
- "```python\n",
- "from sympy import *\n",
- "from sympy.abc import x\n",
- "\n",
- "# define the equation\n",
- "equation = expand((2*x+10)*(x+3) - (3*x+9)*(x+8))\n",
- "\n",
- "# set the equation equal to zero to find the critical points\n",
- "critical_points = solve(equation, x)\n",
- "\n",
- "# Convert the points to standard python floats, so that they can be sorted numerically\n",
- "critical_points = [N(i) for i in critical_points]\n",
- "\n",
- "# Sort the critical_points\n",
- "critical_points = sorted(critical_points)\n",
- "\n",
- "# define a function to test the intervals\n",
- "def test_intervals(interval):\n",
- " test_num = N(sum(interval)/2) # get the mid point of the interval\n",
- " return equation.subs(x, test_num)\n",
- "\n",
- "# define the intervals based on the critical points\n",
- "intervals = [(-oo, critical_points[0]), (critical_points[0], critical_points[1]), (critical_points[1], oo)]\n",
- "\n",
- "# Now, let's create a numeric function from this symbolic equation\n",
- "numeric_equation = lambdify(x, equation)\n",
- "\n",
- "solution = []\n",
- "\n",
- "# loop through the intervals, first find the mid point of each interval, \n",
- "# then if numeric result for this mid point is less than zero, it means it's a valid interval\n",
- "for interval in intervals:\n",
- " mid_point = sum(interval) / 2 \n",
- " if numeric_equation(mid_point) < 0:\n",
- " solution.append(interval)\n",
- "\n",
- "# print the solution in interval notation\n",
- "for interval in solution:\n",
- " print(interval)\n",
- "```\n",
- "\n",
- "Remember to replace `oo` with infinity while interpreting the output.\n",
- "\n",
- "--------------------------------------------------------------------------------\n",
- "\u001b[33mmathproxyagent\u001b[0m (to assistant):\n",
- "\n",
- "Error: Traceback (most recent call last):\n",
- " File \"\", line 36, in \n",
- " if numeric_equation(mid_point) < 0:\n",
- " return func(self, other)\n",
- " return StrictLessThan(self, other)\n",
- " raise TypeError(\"Invalid NaN comparison\")\n",
- "TypeError: Invalid NaN comparison\n",
- "\n",
- "--------------------------------------------------------------------------------\n",
- "\u001b[35m******************************Start compressing the following content:******************************\u001b[0m\n",
- "To be compressed:\n",
- "##ASSISTANT## First, we need to consider both sides of the inequality as two separate equations. Then subtract one from the other to set this result equal to zero. This would allow us to find the critical points, i.e., the places where the inequality changes its nature (from less to more or vice versa). Then we find the intervals.\n",
- "\n",
- "Here's how you can solve this problem in Python:\n",
- "\n",
- "```python\n",
- "from sympy import *\n",
- "from sympy.abc import x\n",
- "\n",
- "# define the equation\n",
- "equation = (2*x+10)*(x+3) - (3*x+9)*(x+8)\n",
- "\n",
- "# set the equation equal to zero to find the critical points\n",
- "critical_points = solve(equation, x)\n",
- "\n",
- "# Sort the critical_points\n",
- "critical_points = sorted(critical_points)\n",
- "\n",
- "# define a function to test the intervals\n",
- "def test_intervals(interval):\n",
- " test_num = sum(interval)/2 # get the mid point of the interval\n",
- " return equation.subs(x, test_num)\n",
- "\n",
- "# define the intervals based on the critical points\n",
- "intervals = [(-oo, critical_points[0]), (critical_points[0], critical_points[1]), (critical_points[1], oo)]\n",
- "\n",
- "solution = []\n",
- "\n",
- "# loop through the intervals, if the result is less than zero, it means it's a valid interval\n",
- "for i in intervals:\n",
- " if test_intervals(i) < 0:\n",
- " solution.append(i)\n",
- "\n",
- "# print the solution in interval notation\n",
- "for interval in solution:\n",
- " print(interval)\n",
- "```\n",
- "\n",
- "Replace oo with infinity when interpreting the result. Also, keep in mind that in interval notation, parentheses denote that the endpoint is not included in the set, and brackets denote that the end point is included in the set. Thus, (a, b) means \"greater than a and less than b\", [a, b] means \"greater than or equal to a and less than or equal to b\".\n",
- "##USER## Error: Traceback (most recent call last):\n",
- " File \"\", line 25, in \n",
- " if test_intervals(i) < 0:\n",
- " return func(self, other)\n",
- " return StrictLessThan(self, other)\n",
- " raise TypeError(\"Invalid NaN comparison\")\n",
- "TypeError: Invalid NaN comparison\n",
- "\n",
- "\u001b[35m******************************Content after compressing:******************************\u001b[0m\n",
- "##ASSISTANT##\n",
- "The assistant provides instructions to solve an inequality, which involves considering separate equations for the inequality components, finding the differing point (set to zero), identifying critical points (where nature of inequality changes), and determining the intervals. \n",
- "\n",
- "They also share Python code for the process, using sympy library. Steps include defining the equation, finding critical points by setting equation to zero, sorting these points, defining a function to test the intervals, determining intervals based on critical points, and looping through intervals to identify valid solutions. The assistant defines 'oo' as infinity and clarifies the use of parentheses and brackets to denote exclusion or inclusion of endpoints in interval notation.\n",
- "\n",
- "##CODE##\n",
- "Python script to solve the given inequality and display the solution using interval notation.\n",
- "\n",
- "##USER##\n",
- "User encounters 'TypeError: Invalid NaN comparison', indicating there's an error when comparing a test interval to zero in the Python script shared by the assistant.\n",
- " \u001b[35m\n",
- "********************************************************************************\u001b[0m\n",
- "\u001b[35mToken Count (including 13 tokens from system msg and function descriptions). Before compression : 1294 | After: 1045\u001b[0m\n",
- "--------------------------------------------------------------------------------\n",
- "\u001b[33massistant\u001b[0m (to mathproxyagent):\n",
- "\n",
- "I apologize for the confusion. There's an error with the calculation of mid_point when the interval is from negative infinity to a real number or a real number to infinity. In these circumstances, the calculation will return a NaN which cannot be used in comparison. We could handle these issues specifically.\n",
- "\n",
- "Let's refine the script:\n",
- "\n",
- "```python\n",
- "from sympy import *\n",
- "from sympy.abc import x\n",
- "\n",
- "# define the equation\n",
- "equation = expand((2*x+10)*(x+3) - (3*x+9)*(x+8))\n",
- "\n",
- "# set the equation equal to zero to find the critical points\n",
- "critical_points = solve(equation, x)\n",
- "\n",
- "# Convert the points to standard python floats, so that they can be sorted numerically\n",
- "critical_points = [N(i) for i in critical_points]\n",
- "\n",
- "# Sort the critical_points\n",
- "critical_points = sorted(critical_points)\n",
- "\n",
- "# Now, let's create a numeric function from this symbolic equation\n",
- "numeric_equation = lambdify(x, equation)\n",
- "\n",
- "solution = []\n",
- "\n",
- "# Define the intervals based on the critical points\n",
- "intervals = [(-oo, critical_points[0]), (critical_points[0], critical_points[1]), (critical_points[1], oo)]\n",
- "\n",
- "# Loop through the intervals and compute the mid point of each interval\n",
- "# If the mid point is NaN (occurs when dealing with infinity), then specific values are used for testing\n",
- "for interval in intervals:\n",
- " if interval[0] == -oo:\n",
- " test_value = interval[1] - 1\n",
- " elif interval[1] == oo:\n",
- " test_value = interval[0] + 1\n",
- " else:\n",
- " test_value = sum(interval) / 2\n",
- "\n",
- " # If numeric result for test_value is less than zero, it's a valid interval\n",
- " if numeric_equation(test_value) < 0:\n",
- " solution.append(interval)\n",
- "\n",
- "# Print out the solution intervals\n",
- "for interval in solution:\n",
- " print(interval)\n",
- "```\n",
- "\n",
- "The script now handles the special cases when dealing with infinity and should return the valid intervals.\n",
- "\n",
- "--------------------------------------------------------------------------------\n",
- "\u001b[33mmathproxyagent\u001b[0m (to assistant):\n",
- "\n",
- "(-oo, -14.0000000000000)\n",
- "(-3.00000000000000, oo)\n",
- "\n",
- "--------------------------------------------------------------------------------\n",
- "\u001b[35m******************************Start compressing the following content:******************************\u001b[0m\n",
- "To be compressed:\n",
- "##ASSISTANT##\n",
- "The assistant provides instructions to solve an inequality, which involves considering separate equations for the inequality components, finding the differing point (set to zero), identifying critical points (where nature of inequality changes), and determining the intervals. \n",
- "\n",
- "They also share Python code for the process, using sympy library. Steps include defining the equation, finding critical points by setting equation to zero, sorting these points, defining a function to test the intervals, determining intervals based on critical points, and looping through intervals to identify valid solutions. The assistant defines 'oo' as infinity and clarifies the use of parentheses and brackets to denote exclusion or inclusion of endpoints in interval notation.\n",
- "\n",
- "##CODE##\n",
- "Python script to solve the given inequality and display the solution using interval notation.\n",
- "\n",
- "##USER##\n",
- "User encounters 'TypeError: Invalid NaN comparison', indicating there's an error when comparing a test interval to zero in the Python script shared by the assistant.\n",
- "\n",
- "##ASSISTANT## I apologize for the oversight. It seems like the function is returning NaN error. Because we are dealing with real numbers, NaN (Not a Number) error could arise from operations that don't return a defined real number. \n",
- "\n",
- "However, in this case, we it seems there's a problem with the comparison of symbolic terms with zero in the function test_intervals.\n",
- "\n",
- "Let's correct this approach. The error arises from trying to compare a symbolic expression to zero directly. Instead, we can create a numeric function from our symbolic expression, and use this function to test the intervals. \n",
- "\n",
- "Here is the revised code:\n",
- "\n",
- "```python\n",
- "from sympy import *\n",
- "from sympy.abc import x\n",
- "\n",
- "# define the equation\n",
- "equation = expand((2*x+10)*(x+3) - (3*x+9)*(x+8))\n",
- "\n",
- "# set the equation equal to zero to find the critical points\n",
- "critical_points = solve(equation, x)\n",
- "\n",
- "# Convert the points to standard python floats, so that they can be sorted numerically\n",
- "critical_points = [N(i) for i in critical_points]\n",
- "\n",
- "# Sort the critical_points\n",
- "critical_points = sorted(critical_points)\n",
- "\n",
- "# define a function to test the intervals\n",
- "def test_intervals(interval):\n",
- " test_num = N(sum(interval)/2) # get the mid point of the interval\n",
- " return equation.subs(x, test_num)\n",
- "\n",
- "# define the intervals based on the critical points\n",
- "intervals = [(-oo, critical_points[0]), (critical_points[0], critical_points[1]), (critical_points[1], oo)]\n",
- "\n",
- "# Now, let's create a numeric function from this symbolic equation\n",
- "numeric_equation = lambdify(x, equation)\n",
- "\n",
- "solution = []\n",
- "\n",
- "# loop through the intervals, first find the mid point of each interval, \n",
- "# then if numeric result for this mid point is less than zero, it means it's a valid interval\n",
- "for interval in intervals:\n",
- " mid_point = sum(interval) / 2 \n",
- " if numeric_equation(mid_point) < 0:\n",
- " solution.append(interval)\n",
- "\n",
- "# print the solution in interval notation\n",
- "for interval in solution:\n",
- " print(interval)\n",
- "```\n",
- "\n",
- "Remember to replace `oo` with infinity while interpreting the output.\n",
- "##USER## Error: Traceback (most recent call last):\n",
- " File \"\", line 36, in \n",
- " if numeric_equation(mid_point) < 0:\n",
- " return func(self, other)\n",
- " return StrictLessThan(self, other)\n",
- " raise TypeError(\"Invalid NaN comparison\")\n",
- "TypeError: Invalid NaN comparison\n",
- "\n",
- "\u001b[35m******************************Content after compressing:******************************\u001b[0m\n",
- "##ASSISTANT##\n",
- "The assistant provides steps to solve an inequality, including considering separate equations, finding the differing point (set to zero), identifying critical points, and determining intervals. They provide Python code using sympy for the process, defining 'oo' as infinity and explaining usage of parentheses and brackets in interval notation. \n",
- "\n",
- "##CODE##\n",
- "The assistant provides Python code for solving an inequality and displaying the solution in interval notation.\n",
- "\n",
- "##USER##\n",
- "The user reports encountering a 'TypeError: Invalid NaN comparison' when comparing a test interval to zero in the script provided by the assistant.\n",
- "\n",
- "##ASSISTANT##\n",
- "The assistant apologizes for the oversight and suggests the issue might lie in the comparison of symbolic terms with zero. The assistant then provides revised code, explaining it creates a numeric function from the symbolic expression to test the intervals. It's highlighted to replace 'oo' with infinity when interpreting the output.\n",
- "\n",
- "##CODE##\n",
- "Revised Python code is given by the assistant to solve a TypeError that occurred in the initial code.\n",
- "\n",
- "##USER##\n",
- "The user reports another TypeError ('Invalid NaN comparison') when executing the revised code provided by the assistant. \u001b[35m\n",
- "********************************************************************************\u001b[0m\n",
- "\u001b[35mToken Count (including 13 tokens from system msg and function descriptions). Before compression : 1494 | After: 1013\u001b[0m\n",
- "--------------------------------------------------------------------------------\n",
- "\u001b[33massistant\u001b[0m (to mathproxyagent):\n",
- "\n",
- "Great! You have got the correct answer.\n",
- "\n",
- "So, for $x$ in the interval $(-\\infty, -14]$ and $(-3, +\\infty)$, the inequality $(2x+10)(x+3)<(3x+9)(x+8)$ is satisfied. \n",
- "\n",
- "In interval notation, this result can be written as $(-\\infty, -14] \\cup (-3, +\\infty)$.\n",
- "\n",
- "The final answer is:\n",
- "\n",
- "\\boxed{(-\\infty, -14] \\cup (-3, +\\infty)}\n",
- "\n",
- "--------------------------------------------------------------------------------\n"
- ]
- }
- ],
- "source": [
- "from autogen.agentchat.contrib.math_user_proxy_agent import MathUserProxyAgent\n",
- "from autogen.agentchat.contrib.compressible_agent import CompressibleAgent\n",
- "\n",
- "# 1. replace AssistantAgent with CompressibleAgent\n",
- "assistant = CompressibleAgent(\n",
- " name=\"assistant\", \n",
- " system_message=\"You are a helpful assistant.\",\n",
- " llm_config={\n",
- " \"timeout\": 600,\n",
- " \"seed\": 42,\n",
- " \"config_list\": config_list,\n",
- " },\n",
- " compress_config={\n",
- " \"mode\": \"COMPRESS\",\n",
- " \"trigger_count\": 600, # set this to a large number for less frequent compression\n",
- " \"verbose\": True, # to allow printing of compression information: contex before and after compression\n",
- " \"leave_last_n\": 2,\n",
- " }\n",
- ")\n",
- "\n",
- "# 2. create the MathUserProxyAgent instance named \"mathproxyagent\"\n",
- "mathproxyagent = MathUserProxyAgent(\n",
- " name=\"mathproxyagent\", \n",
- " human_input_mode=\"NEVER\",\n",
- " code_execution_config={\"use_docker\": False},\n",
- " max_consecutive_auto_reply=5,\n",
- ")\n",
- "math_problem = \"Find all $x$ that satisfy the inequality $(2x+10)(x+3)<(3x+9)(x+8)$. Express your answer in interval notation.\"\n",
- "mathproxyagent.initiate_chat(assistant, problem=math_problem)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Example 2\n",
- "This example is from [agentchat_function_call.ipynb](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_function_call.ipynb). Compression with function calls. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "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",
- "\n",
- " # Define agent texts\n",
- " agent1_texts = ['Hello there!', 'Nice to meet you.', 'How can I assist you?']\n",
- " agent2_texts = ['Hey!', 'Nice meeting you too.', 'Could you help me solve a problem?']\n",
- "\n",
- " # Define agent y positions\n",
- " agent1_y = [3, 2, 1]\n",
- " agent2_y = [3, 2, 1]\n",
- "\n",
- " # Create figure and axis\n",
- " fig, ax = plt.subplots()\n",
- "\n",
- " # Plot Agent 1 texts\n",
- " for i, text in enumerate(agent1_texts):\n",
- " ax.text(0, agent1_y[i], text, fontsize=12, ha='right')\n",
- "\n",
- " # Plot Agent 2 texts\n",
- " for i, text in enumerate(agent2_texts):\n",
- " ax.text(1, agent2_y[i], text, fontsize=12, ha='left')\n",
- "\n",
- " # Set plot attributes\n",
- " ax.set_xlim(-0.5, 1.5)\n",
- " ax.set_ylim(0, 4)\n",
- " ax.set_xticks([0, 1])\n",
- " ax.set_xticklabels(['Agent 1', 'Agent 2'])\n",
- " ax.set_yticks([])\n",
- " ax.spines['top'].set_visible(False)\n",
- " ax.spines['right'].set_visible(False)\n",
- " ax.spines['bottom'].set_visible(False)\"\n",
- "}\n",
- "\u001b[32m*******************************************\u001b[0m\n",
- "\n",
- "--------------------------------------------------------------------------------\n",
- "\u001b[35m\n",
- ">>>>>>>> EXECUTING FUNCTION python...\u001b[0m\n"
- ]
- },
- {
- "ename": "IndentationError",
- "evalue": "unexpected indent (1440792568.py, line 4)",
- "output_type": "error",
- "traceback": [
- "\u001b[0;36m Cell \u001b[0;32mIn[3], line 4\u001b[0;36m\u001b[0m\n\u001b[0;31m agent1_texts = ['Hello there!', 'Nice to meet you.', 'How can I assist you?']\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mIndentationError\u001b[0m\u001b[0;31m:\u001b[0m unexpected indent\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "\u001b[33muser_proxy\u001b[0m (to chatbot):\n",
- "\n",
- "\u001b[32m***** Response from calling function \"python\" *****\u001b[0m\n",
- "None\n",
- "unexpected indent (1440792568.py, line 4)\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\": \n",
- " \"import matplotlib.pyplot as plt\n",
- "\n",
- "# Define agent texts\n",
- "agent1_texts = ['Hello there!', 'Nice to meet you.', 'How can I assist you?']\n",
- "agent2_texts = ['Hey!', 'Nice meeting you too.', 'Could you help me solve a problem?']\n",
- "\n",
- "# Define agent y positions\n",
- "agent1_y = [3, 2, 1]\n",
- "agent2_y = [3, 2, 1]\n",
- "\n",
- "# Create figure and axis\n",
- "fig, ax = plt.subplots()\n",
- "\n",
- "# Plot Agent 1 texts\n",
- "for i, text in enumerate(agent1_texts):\n",
- " ax.text(0, agent1_y[i], text, fontsize=12, ha='right')\n",
- "\n",
- "# Plot Agent 2 texts\n",
- "for i, text in enumerate(agent2_texts):\n",
- " ax.text(1, agent2_y[i], text, fontsize=12, ha='left')\n",
- "\n",
- "# Set plot attributes\n",
- "ax.set_xlim(-0.5, 1.5)\n",
- "ax.set_ylim(0, 4)\n",
- "ax.set_xticks([0, 1])\n",
- "ax.set_xticklabels(['Agent 1', 'Agent 2'])\n",
- "ax.set_yticks([])\n",
- "ax.spines['top'].set_visible(False)\n",
- "ax.spines['right'].set_visible(False)\n",
- "ax.spines['bottom'].set_visible(False)\"\n",
- "}\n",
- "\u001b[32m*******************************************\u001b[0m\n",
- "\n",
- "--------------------------------------------------------------------------------\n",
- "\u001b[35m\n",
- ">>>>>>>> EXECUTING FUNCTION python...\u001b[0m\n"
- ]
- },
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- "