Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code executors #1405

Merged
merged 72 commits into from
Feb 10, 2024
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
be75414
code executor
ekzhu Jan 24, 2024
2ea759b
Merge branch 'main' into coding
ekzhu Jan 24, 2024
36fb1be
test
ekzhu Jan 25, 2024
93d0fc6
revert to main conversable agent
ekzhu Jan 25, 2024
71d3cfa
prepare for pr
ekzhu Jan 25, 2024
af3c7ae
Merge branch 'main' into coding
ekzhu Jan 25, 2024
fc3d70a
kernel
ekzhu Jan 26, 2024
be4b34c
run open ai tests only when it's out of draft status
ekzhu Jan 26, 2024
d42a086
update workflow file
ekzhu Jan 26, 2024
5e069c0
revert workflow changes
ekzhu Jan 26, 2024
907bc8a
ipython executor
ekzhu Jan 27, 2024
4a68901
check kernel installed; fix tests
ekzhu Jan 27, 2024
c2eac94
fix tests
ekzhu Jan 27, 2024
3e96f01
fix tests
ekzhu Jan 27, 2024
57ee0b6
update system prompt
ekzhu Jan 27, 2024
b74d369
Update notebook, more tests
ekzhu Jan 27, 2024
c4dc779
notebook
ekzhu Jan 27, 2024
4e222c0
raise instead of return None
ekzhu Jan 27, 2024
c20de12
Merge branch 'main' into coding
ekzhu Jan 27, 2024
fa84009
allow user provided code executor.
ekzhu Jan 29, 2024
0441eb1
fixing types
davorrunje Jan 30, 2024
8fa7341
Merge remote-tracking branch 'origin/main' into coding
davorrunje Jan 30, 2024
00f08d6
Merge remote-tracking branch 'origin/main' into coding-review
davorrunje Jan 30, 2024
7c1b559
wip
davorrunje Jan 31, 2024
7181cf2
Merge remote-tracking branch 'origin/main' into coding-review
davorrunje Jan 31, 2024
ebd242b
refactoring
davorrunje Jan 31, 2024
85802d8
polishing
davorrunje Jan 31, 2024
6bf887e
Merge branch 'coding-review' into coding
davorrunje Jan 31, 2024
702bdd3
fixed failing tests
davorrunje Feb 1, 2024
778caf0
Merge branch 'coding-review' into coding
davorrunje Feb 1, 2024
a87a7f1
resolved merge conflict
davorrunje Feb 1, 2024
0e38de5
fixing failing test
davorrunje Feb 1, 2024
9ec323d
wip
ekzhu Feb 1, 2024
ca0d890
Merge branch 'coding' of github.com:ekzhu/autogen into coding
ekzhu Feb 1, 2024
0e65e86
local command line executor and embedded ipython executor
ekzhu Feb 1, 2024
363708a
revert notebook
ekzhu Feb 1, 2024
4346e51
Merge branch 'main' into coding
ekzhu Feb 1, 2024
f8b129d
fix format
ekzhu Feb 1, 2024
439afb5
fix merged error
ekzhu Feb 1, 2024
d8b3061
fix lmm test
ekzhu Feb 1, 2024
03e9d59
fix lmm test
ekzhu Feb 1, 2024
db64958
Merge branch 'main' into coding
ekzhu Feb 2, 2024
7d7cb2a
move warning
ekzhu Feb 2, 2024
11bfa93
name and description should be part of the agent protocol, reset is n…
ekzhu Feb 2, 2024
09eb58f
version for dependency
ekzhu Feb 2, 2024
b2bc99d
Update autogen/agentchat/conversable_agent.py
ekzhu Feb 3, 2024
eb55228
Merge branch 'main' into coding
ekzhu Feb 3, 2024
1de5c1e
ordering of protocol
ekzhu Feb 3, 2024
a9f8ca4
description
ekzhu Feb 3, 2024
6835d54
fix tests
ekzhu Feb 3, 2024
2f0701e
Merge branch 'main' into coding
ekzhu Feb 5, 2024
b3e5747
make ipython executor dependency optional
ekzhu Feb 5, 2024
371eb28
update document optional dependencies
ekzhu Feb 5, 2024
07c5195
Merge branch 'main' into coding
AaronWard Feb 6, 2024
8a29a62
Merge branch 'main' into coding
ekzhu Feb 6, 2024
34cc504
Remove exclude from Agent protocol
ekzhu Feb 6, 2024
ac41ba1
Make ConversableAgent consistent with Agent
ekzhu Feb 6, 2024
fc684c1
fix tests
ekzhu Feb 6, 2024
8eb6603
add doc string
ekzhu Feb 6, 2024
7b65133
add doc string
ekzhu Feb 6, 2024
95b4f7f
fix notebook
ekzhu Feb 6, 2024
8ab5b53
merge
ekzhu Feb 7, 2024
ba1c6d6
Merge branch 'main' into coding
sonichi Feb 7, 2024
b640c17
Merge branch 'main' into coding
ekzhu Feb 7, 2024
01cd408
Merge branch 'main' into coding
AaronWard Feb 8, 2024
fa748e0
fix interface
ekzhu Feb 8, 2024
0ddb245
Merge branch 'main' into coding
ekzhu Feb 9, 2024
547a481
merge and update agents
ekzhu Feb 9, 2024
cc601cc
disable config usage in reply function
ekzhu Feb 9, 2024
313b12f
description field setter
ekzhu Feb 9, 2024
088ce19
customize system message update
ekzhu Feb 9, 2024
2c4ae6f
update doc
ekzhu Feb 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ on:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
permissions: {}
# actions: read
permissions:
{}
# actions: read
# checks: read
# contents: read
# deployments: read
Expand All @@ -44,6 +45,7 @@ jobs:
pip install -e .
python -c "import autogen"
pip install pytest mock
python -m ipykernel install --user --name python3
- name: Set AUTOGEN_USE_DOCKER based on OS
shell: bash
run: |
Expand Down
96 changes: 62 additions & 34 deletions autogen/agentchat/agent.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,62 @@
from typing import Dict, List, Optional, Union
from typing import Any, Callable, Dict, List, Optional, Protocol, Union, runtime_checkable


class Agent:
@runtime_checkable
class Agent(Protocol):
"""(In preview) An abstract class for AI agent.
ekzhu marked this conversation as resolved.
Show resolved Hide resolved

An agent can communicate with other agents and perform actions.
Different agents can differ in what actions they perform in the `receive` method.
"""

def __init__(
self,
name: str,
):
"""
Args:
name (str): name of the agent.
"""
# a dictionary of conversations, default value is list
self._name = name

@property
def name(self):
"""Get the name of the agent."""
return self._name
def name(self) -> str:
"""The name of the agent."""
...

def send(self, message: Union[Dict, str], recipient: "Agent", request_reply: Optional[bool] = None):
"""(Abstract method) Send a message to another agent."""
@property
def description(self) -> str:
"""The description of the agent. Used for the agent's introduction in
a group chat setting."""
...

async def a_send(self, message: Union[Dict, str], recipient: "Agent", request_reply: Optional[bool] = None):
"""(Abstract async method) Send a message to another agent."""
def send(
self,
message: Union[Dict[str, Any], str],
recipient: "Agent",
request_reply: Optional[bool] = None,
) -> None:
"""Send a message to another agent."""
...

def receive(self, message: Union[Dict, str], sender: "Agent", request_reply: Optional[bool] = None):
"""(Abstract method) Receive a message from another agent."""
async def a_send(
self,
message: Union[Dict[str, Any], str],
recipient: "Agent",
ekzhu marked this conversation as resolved.
Show resolved Hide resolved
request_reply: Optional[bool] = None,
) -> None:
"""(Async) Send a message to another agent."""
...

async def a_receive(self, message: Union[Dict, str], sender: "Agent", request_reply: Optional[bool] = None):
"""(Abstract async method) Receive a message from another agent."""
def receive(
self, message: Union[Dict[str, Any], str], sender: "Agent", request_reply: Optional[bool] = None
) -> None:
"""Receive a message from another agent."""

def reset(self):
"""(Abstract method) Reset the agent."""
async def a_receive(
self, message: Union[Dict[str, Any], str], sender: "Agent", request_reply: Optional[bool] = None
) -> None:
"""(Async) Receive a message from another agent."""
...

def generate_reply(
self,
messages: Optional[List[Dict]] = None,
messages: Optional[List[Dict[str, Any]]] = None,
sender: Optional["Agent"] = None,
**kwargs,
) -> Union[str, Dict, None]:
"""(Abstract method) Generate a reply based on the received messages.
exclude: Optional[List[Callable[..., Any]]] = None,
**kwargs: Any,
ekzhu marked this conversation as resolved.
Show resolved Hide resolved
) -> Union[str, Dict[str, Any], None]:
"""Generate a reply based on the received messages.

Args:
messages (list[dict]): a list of messages received.
Expand All @@ -56,15 +67,32 @@ def generate_reply(

async def a_generate_reply(
self,
messages: Optional[List[Dict]] = None,
messages: Optional[List[Dict[str, Any]]] = None,
sender: Optional["Agent"] = None,
**kwargs,
) -> Union[str, Dict, None]:
"""(Abstract async method) Generate a reply based on the received messages.
exclude: Optional[List[Callable[..., Any]]] = None,
**kwargs: Any,
ekzhu marked this conversation as resolved.
Show resolved Hide resolved
) -> Union[str, Dict[str, Any], None]:
"""(Async) Generate a reply based on the received messages.

Args:
messages (list[dict]): a list of messages received.
sender: sender of an Agent instance.
Returns:
str or dict or None: the generated reply. If None, no reply is generated.
"""


@runtime_checkable
class LLMAgent(Agent, Protocol):
"""(In preview) An abstract class for LLM agent."""

@property
def system_message(self) -> str:
"""(Abstract method) Return the system message."""
BeibinLi marked this conversation as resolved.
Show resolved Hide resolved

def update_system_message(self, system_message: str) -> None:
"""(Abstract method) Update the system message.

Args:
system_message (str): system message for the ChatCompletion inference.
"""
146 changes: 114 additions & 32 deletions autogen/agentchat/conversable_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from typing import Any, Awaitable, Callable, Dict, List, Literal, Optional, Tuple, Type, TypeVar, Union
import warnings

from ..coding.base import CodeExecutor
from ..coding.factory import CodeExecutorFactory

from .. import OpenAIWrapper
from ..cache.cache import Cache
from ..code_utils import (
Expand All @@ -24,7 +27,7 @@


from ..function_utils import get_function_schema, load_basemodels_if_needed, serialize_to_str
from .agent import Agent
from .agent import Agent, LLMAgent
from .._pydantic import model_dump

try:
Expand All @@ -42,7 +45,7 @@ def colored(x, *args, **kwargs):
F = TypeVar("F", bound=Callable[..., Any])


ekzhu marked this conversation as resolved.
Show resolved Hide resolved
class ConversableAgent(Agent):
class ConversableAgent: # implements LLAgent protocol
ekzhu marked this conversation as resolved.
Show resolved Hide resolved
"""(In preview) A class for generic conversable agents which can be configured as assistant or user proxy.

After receiving each message, the agent will send a reply to the sender unless the msg is a termination msg.
Expand Down Expand Up @@ -117,7 +120,7 @@ def __init__(
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)
"""
super().__init__(name)
self._name = name
# a dictionary of conversations, default value is list
self._oai_messages = defaultdict(list)
self._oai_system_message = [{"content": system_message, "role": "system"}]
Expand All @@ -140,23 +143,6 @@ def __init__(
# Initialize standalone client cache object.
self.client_cache = None

if code_execution_config is None:
warnings.warn(
"Using None to signal a default code_execution_config is deprecated. "
"Use {} to use default or False to disable code execution.",
stacklevel=2,
)

self._code_execution_config: Union[Dict, Literal[False]] = (
{} if code_execution_config is None else code_execution_config
)

if isinstance(self._code_execution_config, dict):
use_docker = self._code_execution_config.get("use_docker", None)
use_docker = decide_use_docker(use_docker)
check_can_use_docker_or_throw(use_docker)
self._code_execution_config["use_docker"] = use_docker

self.human_input_mode = human_input_mode
self._max_consecutive_auto_reply = (
max_consecutive_auto_reply if max_consecutive_auto_reply is not None else self.MAX_CONSECUTIVE_AUTO_REPLY
Expand All @@ -174,7 +160,36 @@ def __init__(
self.reply_at_receive = defaultdict(bool)
self.register_reply([Agent, None], ConversableAgent.generate_oai_reply)
self.register_reply([Agent, None], ConversableAgent.a_generate_oai_reply, ignore_async_in_sync_chat=True)
self.register_reply([Agent, None], ConversableAgent.generate_code_execution_reply)

# Setting up code execution.
# Do not register code execution reply if code execution is disabled.
if code_execution_config is not False:
# If code_execution_config is None, set it to an empty dict.
if code_execution_config is None:
warnings.warn(
BeibinLi marked this conversation as resolved.
Show resolved Hide resolved
"Using None to signal a default code_execution_config is deprecated. "
"Use {} to use default or False to disable code execution.",
stacklevel=2,
)
code_execution_config = {}
if not isinstance(code_execution_config, dict):
raise ValueError("code_execution_config must be a dict or False.")

# We have got a valid code_execution_config.
self._code_execution_config = code_execution_config

if self._code_execution_config.get("executor") is not None:
# Use the new code executor.
self._code_executor = CodeExecutorFactory.create(self._code_execution_config)
self.register_reply([Agent, None], ConversableAgent._generate_code_execution_reply_using_executor)
else:
# Legacy code execution using code_utils.
BeibinLi marked this conversation as resolved.
Show resolved Hide resolved
use_docker = self._code_execution_config.get("use_docker", None)
use_docker = decide_use_docker(use_docker)
check_can_use_docker_or_throw(use_docker)
self._code_execution_config["use_docker"] = use_docker
self.register_reply([Agent, None], ConversableAgent.generate_code_execution_reply)

self.register_reply([Agent, None], ConversableAgent.generate_tool_calls_reply)
self.register_reply([Agent, None], ConversableAgent.a_generate_tool_calls_reply, ignore_async_in_sync_chat=True)
self.register_reply([Agent, None], ConversableAgent.generate_function_call_reply)
Expand All @@ -190,6 +205,21 @@ def __init__(
# New hookable methods should be added to this list as required to support new agent capabilities.
self.hook_lists = {self.process_last_message: []} # This is currently the only hookable method.

@property
def name(self) -> str:
"""Get the name of the agent."""
return self._name

@property
def code_executor(self) -> CodeExecutor:
"""The code executor used by this agent. Raise if code execution is disabled."""
if not hasattr(self, "_code_executor"):
raise ValueError(
"No code executor as code execution is disabled. "
"To enable code execution, set code_execution_config."
)
return self._code_executor

def register_reply(
self,
trigger: Union[Type[Agent], str, Agent, Callable[[Agent], bool], List],
Expand Down Expand Up @@ -262,15 +292,15 @@ def reply_func(
self._ignore_async_func_in_sync_chat_list.append(reply_func)

@property
def system_message(self) -> Union[str, List]:
def system_message(self) -> str:
"""Return the system message."""
return self._oai_system_message[0]["content"]

def update_system_message(self, system_message: Union[str, List]):
def update_system_message(self, system_message: str) -> None:
"""Update the system message.

Args:
system_message (str or List): system message for the ChatCompletion inference.
system_message (str): system message for the ChatCompletion inference.
"""
self._oai_system_message[0]["content"] = system_message

Expand Down Expand Up @@ -849,13 +879,63 @@ async def a_generate_oai_reply(
None, functools.partial(self.generate_oai_reply, messages=messages, sender=sender, config=config)
)

def _generate_code_execution_reply_using_executor(
sonichi marked this conversation as resolved.
Show resolved Hide resolved
self,
messages: Optional[List[Dict]] = None,
sender: Optional[Agent] = None,
config: Optional[Union[Dict, Literal[False]]] = None,
):
"""Generate a reply using code executor."""
code_execution_config = config if config is not None else self._code_execution_config
sonichi marked this conversation as resolved.
Show resolved Hide resolved
if code_execution_config is False:
return False, None
if messages is None:
messages = self._oai_messages[sender]
last_n_messages = code_execution_config.get("last_n_messages", "auto")

if not (isinstance(last_n_messages, (int, float)) and last_n_messages >= 0) and last_n_messages != "auto":
raise ValueError("last_n_messages must be either a non-negative integer, or the string 'auto'.")

num_messages_to_scan = last_n_messages
if last_n_messages == "auto":
# Find when the agent last spoke
num_messages_to_scan = 0
for message in reversed(messages):
if "role" not in message:
break
elif message["role"] != "user":
break
else:
num_messages_to_scan += 1
num_messages_to_scan = min(len(messages), num_messages_to_scan)
messages_to_scan = messages[-num_messages_to_scan:]

# iterate through the last n messages in reverse
# if code blocks are found, execute the code blocks and return the output
# if no code blocks are found, continue
for message in reversed(messages_to_scan):
if not message["content"]:
continue
code_blocks = self._code_executor.code_extractor.extract_code_blocks(message["content"])
if len(code_blocks) == 0:
continue
# found code blocks, execute code.
code_result = self._code_executor.execute_code_blocks(code_blocks)
exitcode2str = "execution succeeded" if code_result.exit_code == 0 else "execution failed"
return True, f"exitcode: {code_result.exit_code} ({exitcode2str})\nCode output: {code_result.output}"
ekzhu marked this conversation as resolved.
Show resolved Hide resolved

return False, None

def generate_code_execution_reply(
self,
messages: Optional[List[Dict]] = None,
sender: Optional[Agent] = None,
config: Optional[Union[Dict, Literal[False]]] = None,
):
"""Generate a reply using code execution."""
"""(Deprecated) Generate a reply using code execution.

NOTE: this function uses the legacy code utils and will be removed in the future.
"""
code_execution_config = config if config is not None else self._code_execution_config
if code_execution_config is False:
return False, None
Expand Down Expand Up @@ -1277,9 +1357,10 @@ async def a_check_termination_and_human_reply(

def generate_reply(
self,
messages: Optional[List[Dict]] = None,
sender: Optional[Agent] = None,
exclude: Optional[List[Callable]] = None,
messages: Optional[List[Dict[str, Any]]] = None,
sender: Optional["Agent"] = None,
exclude: Optional[List[Callable[..., Any]]] = None,
**kwargs: Any,
) -> Union[str, Dict, None]:
"""Reply based on the conversation history and the sender.

Expand Down Expand Up @@ -1333,10 +1414,11 @@ def generate_reply(

async def a_generate_reply(
self,
messages: Optional[List[Dict]] = None,
sender: Optional[Agent] = None,
exclude: Optional[List[Callable]] = None,
) -> Union[str, Dict, None]:
messages: Optional[List[Dict[str, Any]]] = None,
sender: Optional["Agent"] = None,
exclude: Optional[List[Callable[..., Any]]] = None,
**kwargs: Any,
) -> Union[str, Dict[str, Any], None]:
"""(async) Reply based on the conversation history and the sender.

Either messages or sender must be provided.
Expand Down
Loading
Loading