mirror of https://github.com/microsoft/autogen.git
[CAP] Improved AutoGen Agents support & Pip Install (#2711)
* 1) Removed most framework sleeps 2) refactored connection code * pre-commit fixes * pre-commit * ignore protobuf files in pre-commit checks * Fix duplicate actor registration * refactor change * Nicer printing of Actors * 1) Report recv_multipart errors 4) Always send 4 parts * AutoGen generate_reply expects to wait indefinitely for an answer. CAP can wait a certain amount and give up. In order to reconcile the two, AutoGenConnector is set to wait indefinitely. * pre-commit formatting fixes * pre-commit format changes * don't check autogenerated proto py files * Iterating on CAP interface for AutoGen * User proxy must initiate chat * autogencap pypi package * added dependencies * serialize/deserialize dictionary elements to json when dealing with ReceiveReq * 1) Removed most framework sleeps 2) refactored connection code * Nicer printing of Actors * AutoGen generate_reply expects to wait indefinitely for an answer. CAP can wait a certain amount and give up. In order to reconcile the two, AutoGenConnector is set to wait indefinitely. * pre-commit formatting fixes * pre-commit format changes * Iterating on CAP interface for AutoGen * User proxy must initiate chat * autogencap pypi package * added dependencies * serialize/deserialize dictionary elements to json when dealing with ReceiveReq * pre-commit check fixes * fix pre-commit issues * Better encapsulation of logging * pre-commit fix * pip package update
This commit is contained in:
parent
11d9336e3d
commit
31d2d37d88
|
@ -1,11 +1,13 @@
|
|||
# Composable Actor Platform (CAP) for AutoGen
|
||||
|
||||
## I just want to run the demo!
|
||||
## I just want to run the remote AutoGen agents!
|
||||
*Python Instructions (Windows, Linux, MacOS):*
|
||||
|
||||
0) cd py
|
||||
1) pip install -r autogencap/requirements.txt
|
||||
2) python ./demo/App.py
|
||||
3) Choose (5) and follow instructions to run standalone Agents
|
||||
4) Choose other options for other demos
|
||||
|
||||
*Demo Notes:*
|
||||
1) Options involving AutoGen require OAI_CONFIG_LIST.
|
||||
|
@ -15,14 +17,15 @@
|
|||
|
||||
*Demo Reference:*
|
||||
```
|
||||
Select the Composable Actor Platform (CAP) demo app to run:
|
||||
(enter anything else to quit)
|
||||
1. Hello World Actor
|
||||
2. Complex Actor Graph
|
||||
3. AutoGen Pair
|
||||
4. AutoGen GroupChat
|
||||
5. AutoGen Agents in different processes
|
||||
Enter your choice (1-5):
|
||||
Select the Composable Actor Platform (CAP) demo app to run:
|
||||
(enter anything else to quit)
|
||||
1. Hello World
|
||||
2. Complex Agent (e.g. Name or Quit)
|
||||
3. AutoGen Pair
|
||||
4. AutoGen GroupChat
|
||||
5. AutoGen Agents in different processes
|
||||
6. List Actors in CAP (Registry)
|
||||
Enter your choice (1-6):
|
||||
```
|
||||
|
||||
## What is Composable Actor Platform (CAP)?
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
# Composable Actor Platform (CAP) for AutoGen
|
||||
|
||||
## I just want to run the remote AutoGen agents!
|
||||
*Python Instructions (Windows, Linux, MacOS):*
|
||||
|
||||
pip install autogencap
|
||||
|
||||
1) AutoGen require OAI_CONFIG_LIST.
|
||||
AutoGen python requirements: 3.8 <= python <= 3.11
|
||||
|
||||
```
|
||||
|
||||
## What is Composable Actor Platform (CAP)?
|
||||
AutoGen is about Agents and Agent Orchestration. CAP extends AutoGen to allows Agents to communicate via a message bus. CAP, therefore, deals with the space between these components. CAP is a message based, actor platform that allows actors to be composed into arbitrary graphs.
|
||||
|
||||
Actors can register themselves with CAP, find other agents, construct arbitrary graphs, send and receive messages independently and many, many, many other things.
|
||||
```python
|
||||
# CAP Platform
|
||||
network = LocalActorNetwork()
|
||||
# Register an agent
|
||||
network.register(GreeterAgent())
|
||||
# Tell agents to connect to other agents
|
||||
network.connect()
|
||||
# Get a channel to the agent
|
||||
greeter_link = network.lookup_agent("Greeter")
|
||||
# Send a message to the agent
|
||||
greeter_link.send_txt_msg("Hello World!")
|
||||
# Cleanup
|
||||
greeter_link.close()
|
||||
network.disconnect()
|
||||
```
|
||||
### Check out other demos in the `py/demo` directory. We show the following: ###
|
||||
1) Hello World shown above
|
||||
2) Many CAP Actors interacting with each other
|
||||
3) A pair of interacting AutoGen Agents wrapped in CAP Actors
|
||||
4) CAP wrapped AutoGen Agents in a group chat
|
||||
5) Two AutoGen Agents running in different processes and communicating through CAP
|
||||
6) List all registered agents in CAP
|
||||
7) AutoGen integration to list all registered agents
|
|
@ -29,8 +29,11 @@ class ActorSender:
|
|||
evt: Dict[str, Any] = {}
|
||||
mon_evt = recv_monitor_message(monitor)
|
||||
evt.update(mon_evt)
|
||||
if evt["event"] == zmq.EVENT_MONITOR_STOPPED or evt["event"] == zmq.EVENT_HANDSHAKE_SUCCEEDED:
|
||||
Debug("ActorSender", "Handshake received (Or Monitor stopped)")
|
||||
if evt["event"] == zmq.EVENT_HANDSHAKE_SUCCEEDED:
|
||||
Debug("ActorSender", "Handshake received")
|
||||
break
|
||||
elif evt["event"] == zmq.EVENT_MONITOR_STOPPED:
|
||||
Debug("ActorSender", "Monitor stopped")
|
||||
break
|
||||
self._pub_socket.disable_monitor()
|
||||
monitor.close()
|
||||
|
@ -117,32 +120,33 @@ class ActorConnector:
|
|||
def send_bin_msg(self, msg_type: str, msg):
|
||||
self._sender.send_bin_msg(msg_type, msg)
|
||||
|
||||
def binary_request(self, msg_type: str, msg, retry=5):
|
||||
def binary_request(self, msg_type: str, msg, num_attempts=5):
|
||||
original_timeout: int = 0
|
||||
if retry == -1:
|
||||
if num_attempts == -1:
|
||||
original_timeout = self._resp_socket.getsockopt(zmq.RCVTIMEO)
|
||||
self._resp_socket.setsockopt(zmq.RCVTIMEO, 1000)
|
||||
|
||||
try:
|
||||
self._sender.send_bin_request_msg(msg_type, msg, self._resp_topic)
|
||||
while retry == -1 or retry > 0:
|
||||
while num_attempts == -1 or num_attempts > 0:
|
||||
try:
|
||||
topic, resp_msg_type, _, resp = self._resp_socket.recv_multipart()
|
||||
return topic, resp_msg_type, resp
|
||||
except zmq.Again:
|
||||
Debug(
|
||||
"ActorConnector", f"{self._topic}: No response received. retry_count={retry}, max_retry={retry}"
|
||||
"ActorConnector",
|
||||
f"{self._topic}: No response received. retry_count={num_attempts}, max_retry={num_attempts}",
|
||||
)
|
||||
time.sleep(0.01)
|
||||
if retry != -1:
|
||||
retry -= 1
|
||||
if num_attempts != -1:
|
||||
num_attempts -= 1
|
||||
finally:
|
||||
if retry == -1:
|
||||
if num_attempts == -1:
|
||||
self._resp_socket.setsockopt(zmq.RCVTIMEO, original_timeout)
|
||||
|
||||
Error("ActorConnector", f"{self._topic}: No response received. Giving up.")
|
||||
return None, None, None
|
||||
|
||||
def close(self):
|
||||
self._sender.close()
|
||||
self._pub_socket.close()
|
||||
self._resp_socket.close()
|
||||
|
|
|
@ -15,42 +15,58 @@ ERROR = 3
|
|||
LEVEL_NAMES = ["DBG", "INF", "WRN", "ERR"]
|
||||
LEVEL_COLOR = ["dark_grey", "green", "yellow", "red"]
|
||||
|
||||
console_lock = threading.Lock()
|
||||
|
||||
class BaseLogger:
|
||||
def __init__(self):
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def Log(self, level, context, msg):
|
||||
# Check if the current level meets the threshold
|
||||
if level >= Config.LOG_LEVEL: # Use the LOG_LEVEL from the Config module
|
||||
# Check if the context is in the list of ignored contexts
|
||||
if context in Config.IGNORED_LOG_CONTEXTS:
|
||||
return
|
||||
with self._lock:
|
||||
self.WriteLog(level, context, msg)
|
||||
|
||||
def WriteLog(self, level, context, msg):
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
|
||||
def Log(level, context, msg):
|
||||
# Check if the current level meets the threshold
|
||||
if level >= Config.LOG_LEVEL: # Use the LOG_LEVEL from the Config module
|
||||
# Check if the context is in the list of ignored contexts
|
||||
if context in Config.IGNORED_LOG_CONTEXTS:
|
||||
return
|
||||
with console_lock:
|
||||
timestamp = colored(datetime.datetime.now().strftime("%m/%d/%y %H:%M:%S"), "dark_grey")
|
||||
# Translate level number to name and color
|
||||
level_name = colored(LEVEL_NAMES[level], LEVEL_COLOR[level])
|
||||
# Left justify the context and color it blue
|
||||
context = colored(context.ljust(14), "blue")
|
||||
# Left justify the threadid and color it blue
|
||||
thread_id = colored(str(threading.get_ident()).ljust(5), "blue")
|
||||
# color the msg based on the level
|
||||
msg = colored(msg, LEVEL_COLOR[level])
|
||||
print(f"{thread_id} {timestamp} {level_name}: [{context}] {msg}")
|
||||
class ConsoleLogger(BaseLogger):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def WriteLog(self, level, context, msg):
|
||||
timestamp = colored(datetime.datetime.now().strftime("%m/%d/%y %H:%M:%S"), "pink")
|
||||
# Translate level number to name and color
|
||||
level_name = colored(LEVEL_NAMES[level], LEVEL_COLOR[level])
|
||||
# Left justify the context and color it blue
|
||||
context = colored(context.ljust(14), "blue")
|
||||
# Left justify the threadid and color it blue
|
||||
thread_id = colored(str(threading.get_ident()).ljust(5), "blue")
|
||||
# color the msg based on the level
|
||||
msg = colored(msg, LEVEL_COLOR[level])
|
||||
print(f"{thread_id} {timestamp} {level_name}: [{context}] {msg}")
|
||||
|
||||
|
||||
LOGGER = ConsoleLogger()
|
||||
|
||||
|
||||
def Debug(context, message):
|
||||
Log(DEBUG, context, message)
|
||||
LOGGER.Log(DEBUG, context, message)
|
||||
|
||||
|
||||
def Info(context, message):
|
||||
Log(INFO, context, message)
|
||||
LOGGER.Log(INFO, context, message)
|
||||
|
||||
|
||||
def Warn(context, message):
|
||||
Log(WARN, context, message)
|
||||
LOGGER.Log(WARN, context, message)
|
||||
|
||||
|
||||
def Error(context, message):
|
||||
Log(ERROR, context, message)
|
||||
LOGGER.Log(ERROR, context, message)
|
||||
|
||||
|
||||
def shorten(msg, num_parts=5, max_len=100):
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
from typing import Dict, Optional, Union
|
||||
|
||||
from autogen import Agent
|
||||
|
@ -37,7 +38,7 @@ class AutoGenConnector:
|
|||
# Setting retry to -1 to keep trying until a response is received
|
||||
# This normal AutoGen behavior but does not handle the case when an AutoGen agent
|
||||
# is not running. In that case, the connector will keep trying indefinitely.
|
||||
_, _, resp = self._can_channel.binary_request(type(msg).__name__, serialized_msg, retry=-1)
|
||||
_, _, resp = self._can_channel.binary_request(type(msg).__name__, serialized_msg, num_attempts=-1)
|
||||
gen_reply_resp = GenReplyResp()
|
||||
gen_reply_resp.ParseFromString(resp)
|
||||
return gen_reply_resp.data
|
||||
|
@ -55,7 +56,8 @@ class AutoGenConnector:
|
|||
msg = ReceiveReq()
|
||||
if isinstance(message, dict):
|
||||
for key, value in message.items():
|
||||
msg.data_map.data[key] = value
|
||||
json_serialized_value = json.dumps(value)
|
||||
msg.data_map.data[key] = json_serialized_value
|
||||
elif isinstance(message, str):
|
||||
msg.data = message
|
||||
msg.sender = sender.name
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
|
@ -72,7 +73,11 @@ class CAP2AG(AGActor):
|
|||
save_name = self._ag2can_other_agent.name
|
||||
self._ag2can_other_agent.set_name(receive_params.sender)
|
||||
if receive_params.HasField("data_map"):
|
||||
data = dict(receive_params.data_map.data)
|
||||
json_data = dict(receive_params.data_map.data)
|
||||
data = {}
|
||||
for key, json_value in json_data.items():
|
||||
value = json.loads(json_value)
|
||||
data[key] = value
|
||||
else:
|
||||
data = receive_params.data
|
||||
self._the_ag_agent.receive(data, self._ag2can_other_agent, request_reply, silent)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import time
|
||||
|
||||
from autogen import ConversableAgent
|
||||
|
||||
from ..DebugLog import Info, Warn
|
||||
from .CAP2AG import CAP2AG
|
||||
|
||||
|
||||
class Agent:
|
||||
def __init__(self, agent: ConversableAgent, counter_party_name="user_proxy", init_chat=False):
|
||||
self._agent = agent
|
||||
self._the_other_name = counter_party_name
|
||||
self._agent_adptr = CAP2AG(
|
||||
ag_agent=self._agent, the_other_name=self._the_other_name, init_chat=init_chat, self_recursive=True
|
||||
)
|
||||
|
||||
def register(self, network):
|
||||
Info("Agent", f"Running Standalone {self._agent.name}")
|
||||
network.register(self._agent_adptr)
|
||||
|
||||
def running(self):
|
||||
return self._agent_adptr.run
|
|
@ -45,7 +45,7 @@ def main():
|
|||
print("3. AutoGen Pair")
|
||||
print("4. AutoGen GroupChat")
|
||||
print("5. AutoGen Agents in different processes")
|
||||
print("6. List Actors in CAP")
|
||||
print("6. List Actors in CAP (Registry)")
|
||||
choice = input("Enter your choice (1-6): ")
|
||||
|
||||
if choice == "1":
|
||||
|
|
|
@ -5,13 +5,12 @@
|
|||
def remote_ag_demo():
|
||||
print("Remote Agent Demo")
|
||||
instructions = """
|
||||
In this demo, Broker, Assistant, and UserProxy are running in separate processes.
|
||||
demo/standalone/UserProxy.py will initiate a conversation by sending UserProxy a message.
|
||||
In this demo, Assistant, and UserProxy are running in separate processes.
|
||||
demo/standalone/user_proxy.py will initiate a conversation by sending UserProxy Agent a message.
|
||||
|
||||
Please do the following:
|
||||
1) Start Broker (python demo/standalone/Broker.py)
|
||||
2) Start Assistant (python demo/standalone/Assistant.py)
|
||||
3) Start UserProxy (python demo/standalone/UserProxy.py)
|
||||
1) Start Assistant (python demo/standalone/assistant.py)
|
||||
2) Start UserProxy (python demo/standalone/user_proxy.py)
|
||||
"""
|
||||
print(instructions)
|
||||
input("Press Enter to return to demo menu...")
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import time
|
||||
|
||||
import _paths
|
||||
from autogencap.ag_adapter.agent import Agent
|
||||
from autogencap.Config import IGNORED_LOG_CONTEXTS
|
||||
from autogencap.LocalActorNetwork import LocalActorNetwork
|
||||
|
||||
from autogen import UserProxyAgent
|
||||
|
||||
# Filter out some Log message contexts
|
||||
IGNORED_LOG_CONTEXTS.extend(["BROKER"])
|
||||
|
||||
|
||||
def main():
|
||||
# Standard AutoGen
|
||||
user_proxy = UserProxyAgent(
|
||||
"user_proxy",
|
||||
code_execution_config={"work_dir": "coding"},
|
||||
is_termination_msg=lambda x: "TERMINATE" in x.get("content"),
|
||||
)
|
||||
|
||||
# Wrap AutoGen Agent in CAP
|
||||
cap_user_proxy = Agent(user_proxy, counter_party_name="assistant", init_chat=True)
|
||||
# Create the message bus
|
||||
network = LocalActorNetwork()
|
||||
# Add the user_proxy to the message bus
|
||||
cap_user_proxy.register(network)
|
||||
# Start message processing
|
||||
network.connect()
|
||||
|
||||
# Wait for the user_proxy to finish
|
||||
interact_with_user(network, cap_user_proxy)
|
||||
# Cleanup
|
||||
network.disconnect()
|
||||
|
||||
|
||||
# Starts the Broker and the Assistant. The UserProxy is started separately.
|
||||
def interact_with_user(network, cap_assistant):
|
||||
user_proxy_conn = network.lookup_actor("user_proxy")
|
||||
example = "Plot a chart of MSFT daily closing prices for last 1 Month."
|
||||
print(f"Example: {example}")
|
||||
try:
|
||||
user_input = input("Please enter your command: ")
|
||||
if user_input == "":
|
||||
user_input = example
|
||||
print(f"Sending: {user_input}")
|
||||
user_proxy_conn.send_txt_msg(user_input)
|
||||
|
||||
# Hang around for a while
|
||||
while cap_assistant.running():
|
||||
time.sleep(0.5)
|
||||
except KeyboardInterrupt:
|
||||
print("Interrupted by user, shutting down.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,36 @@
|
|||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "autogencap_rajan.jedi"
|
||||
version = "0.0.7"
|
||||
authors = [
|
||||
{ name="Rajan Chari", email="rajan.jedi@gmail.com" },
|
||||
]
|
||||
dependencies = [
|
||||
"pyzmq >= 25.1.2",
|
||||
"protobuf >= 4.25.3",
|
||||
"termcolor >= 2.4.0",
|
||||
"pyautogen >= 0.2.23",
|
||||
]
|
||||
description = "CAP w/ autogen bindings"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/microsoft/autogen"
|
||||
"Bug Tracker" = "https://github.com/microsoft/autogen/issues"
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
packages = ["autogencap"]
|
||||
only-packages = true
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["autogencap"]
|
||||
only-packages = true
|
Loading…
Reference in New Issue