Skip to content

Commit 31d2d37

Browse files
authored
[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
1 parent 11d9336 commit 31d2d37

File tree

12 files changed

+235
-52
lines changed

12 files changed

+235
-52
lines changed

samples/apps/cap/README.md

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# Composable Actor Platform (CAP) for AutoGen
22

3-
## I just want to run the demo!
3+
## I just want to run the remote AutoGen agents!
44
*Python Instructions (Windows, Linux, MacOS):*
55

66
0) cd py
77
1) pip install -r autogencap/requirements.txt
88
2) python ./demo/App.py
9+
3) Choose (5) and follow instructions to run standalone Agents
10+
4) Choose other options for other demos
911

1012
*Demo Notes:*
1113
1) Options involving AutoGen require OAI_CONFIG_LIST.
@@ -15,14 +17,15 @@
1517

1618
*Demo Reference:*
1719
```
18-
Select the Composable Actor Platform (CAP) demo app to run:
19-
(enter anything else to quit)
20-
1. Hello World Actor
21-
2. Complex Actor Graph
22-
3. AutoGen Pair
23-
4. AutoGen GroupChat
24-
5. AutoGen Agents in different processes
25-
Enter your choice (1-5):
20+
Select the Composable Actor Platform (CAP) demo app to run:
21+
(enter anything else to quit)
22+
1. Hello World
23+
2. Complex Agent (e.g. Name or Quit)
24+
3. AutoGen Pair
25+
4. AutoGen GroupChat
26+
5. AutoGen Agents in different processes
27+
6. List Actors in CAP (Registry)
28+
Enter your choice (1-6):
2629
```
2730

2831
## What is Composable Actor Platform (CAP)?

samples/apps/cap/py/README.md

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Composable Actor Platform (CAP) for AutoGen
2+
3+
## I just want to run the remote AutoGen agents!
4+
*Python Instructions (Windows, Linux, MacOS):*
5+
6+
pip install autogencap
7+
8+
1) AutoGen require OAI_CONFIG_LIST.
9+
AutoGen python requirements: 3.8 <= python <= 3.11
10+
11+
```
12+
13+
## What is Composable Actor Platform (CAP)?
14+
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.
15+
16+
Actors can register themselves with CAP, find other agents, construct arbitrary graphs, send and receive messages independently and many, many, many other things.
17+
```python
18+
# CAP Platform
19+
network = LocalActorNetwork()
20+
# Register an agent
21+
network.register(GreeterAgent())
22+
# Tell agents to connect to other agents
23+
network.connect()
24+
# Get a channel to the agent
25+
greeter_link = network.lookup_agent("Greeter")
26+
# Send a message to the agent
27+
greeter_link.send_txt_msg("Hello World!")
28+
# Cleanup
29+
greeter_link.close()
30+
network.disconnect()
31+
```
32+
### Check out other demos in the `py/demo` directory. We show the following: ###
33+
1) Hello World shown above
34+
2) Many CAP Actors interacting with each other
35+
3) A pair of interacting AutoGen Agents wrapped in CAP Actors
36+
4) CAP wrapped AutoGen Agents in a group chat
37+
5) Two AutoGen Agents running in different processes and communicating through CAP
38+
6) List all registered agents in CAP
39+
7) AutoGen integration to list all registered agents

samples/apps/cap/py/autogencap/ActorConnector.py

+14-10
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@ def _connect_pub_socket(self):
2929
evt: Dict[str, Any] = {}
3030
mon_evt = recv_monitor_message(monitor)
3131
evt.update(mon_evt)
32-
if evt["event"] == zmq.EVENT_MONITOR_STOPPED or evt["event"] == zmq.EVENT_HANDSHAKE_SUCCEEDED:
33-
Debug("ActorSender", "Handshake received (Or Monitor stopped)")
32+
if evt["event"] == zmq.EVENT_HANDSHAKE_SUCCEEDED:
33+
Debug("ActorSender", "Handshake received")
34+
break
35+
elif evt["event"] == zmq.EVENT_MONITOR_STOPPED:
36+
Debug("ActorSender", "Monitor stopped")
3437
break
3538
self._pub_socket.disable_monitor()
3639
monitor.close()
@@ -117,32 +120,33 @@ def send_txt_msg(self, msg):
117120
def send_bin_msg(self, msg_type: str, msg):
118121
self._sender.send_bin_msg(msg_type, msg)
119122

120-
def binary_request(self, msg_type: str, msg, retry=5):
123+
def binary_request(self, msg_type: str, msg, num_attempts=5):
121124
original_timeout: int = 0
122-
if retry == -1:
125+
if num_attempts == -1:
123126
original_timeout = self._resp_socket.getsockopt(zmq.RCVTIMEO)
124127
self._resp_socket.setsockopt(zmq.RCVTIMEO, 1000)
125128

126129
try:
127130
self._sender.send_bin_request_msg(msg_type, msg, self._resp_topic)
128-
while retry == -1 or retry > 0:
131+
while num_attempts == -1 or num_attempts > 0:
129132
try:
130133
topic, resp_msg_type, _, resp = self._resp_socket.recv_multipart()
131134
return topic, resp_msg_type, resp
132135
except zmq.Again:
133136
Debug(
134-
"ActorConnector", f"{self._topic}: No response received. retry_count={retry}, max_retry={retry}"
137+
"ActorConnector",
138+
f"{self._topic}: No response received. retry_count={num_attempts}, max_retry={num_attempts}",
135139
)
136140
time.sleep(0.01)
137-
if retry != -1:
138-
retry -= 1
141+
if num_attempts != -1:
142+
num_attempts -= 1
139143
finally:
140-
if retry == -1:
144+
if num_attempts == -1:
141145
self._resp_socket.setsockopt(zmq.RCVTIMEO, original_timeout)
142146

143147
Error("ActorConnector", f"{self._topic}: No response received. Giving up.")
144148
return None, None, None
145149

146150
def close(self):
147-
self._sender.close()
151+
self._pub_socket.close()
148152
self._resp_socket.close()

samples/apps/cap/py/autogencap/DebugLog.py

+40-24
Original file line numberDiff line numberDiff line change
@@ -15,42 +15,58 @@
1515
LEVEL_NAMES = ["DBG", "INF", "WRN", "ERR"]
1616
LEVEL_COLOR = ["dark_grey", "green", "yellow", "red"]
1717

18-
console_lock = threading.Lock()
19-
20-
21-
def Log(level, context, msg):
22-
# Check if the current level meets the threshold
23-
if level >= Config.LOG_LEVEL: # Use the LOG_LEVEL from the Config module
24-
# Check if the context is in the list of ignored contexts
25-
if context in Config.IGNORED_LOG_CONTEXTS:
26-
return
27-
with console_lock:
28-
timestamp = colored(datetime.datetime.now().strftime("%m/%d/%y %H:%M:%S"), "dark_grey")
29-
# Translate level number to name and color
30-
level_name = colored(LEVEL_NAMES[level], LEVEL_COLOR[level])
31-
# Left justify the context and color it blue
32-
context = colored(context.ljust(14), "blue")
33-
# Left justify the threadid and color it blue
34-
thread_id = colored(str(threading.get_ident()).ljust(5), "blue")
35-
# color the msg based on the level
36-
msg = colored(msg, LEVEL_COLOR[level])
37-
print(f"{thread_id} {timestamp} {level_name}: [{context}] {msg}")
18+
19+
class BaseLogger:
20+
def __init__(self):
21+
self._lock = threading.Lock()
22+
23+
def Log(self, level, context, msg):
24+
# Check if the current level meets the threshold
25+
if level >= Config.LOG_LEVEL: # Use the LOG_LEVEL from the Config module
26+
# Check if the context is in the list of ignored contexts
27+
if context in Config.IGNORED_LOG_CONTEXTS:
28+
return
29+
with self._lock:
30+
self.WriteLog(level, context, msg)
31+
32+
def WriteLog(self, level, context, msg):
33+
raise NotImplementedError("Subclasses must implement this method")
34+
35+
36+
class ConsoleLogger(BaseLogger):
37+
def __init__(self):
38+
super().__init__()
39+
40+
def WriteLog(self, level, context, msg):
41+
timestamp = colored(datetime.datetime.now().strftime("%m/%d/%y %H:%M:%S"), "pink")
42+
# Translate level number to name and color
43+
level_name = colored(LEVEL_NAMES[level], LEVEL_COLOR[level])
44+
# Left justify the context and color it blue
45+
context = colored(context.ljust(14), "blue")
46+
# Left justify the threadid and color it blue
47+
thread_id = colored(str(threading.get_ident()).ljust(5), "blue")
48+
# color the msg based on the level
49+
msg = colored(msg, LEVEL_COLOR[level])
50+
print(f"{thread_id} {timestamp} {level_name}: [{context}] {msg}")
51+
52+
53+
LOGGER = ConsoleLogger()
3854

3955

4056
def Debug(context, message):
41-
Log(DEBUG, context, message)
57+
LOGGER.Log(DEBUG, context, message)
4258

4359

4460
def Info(context, message):
45-
Log(INFO, context, message)
61+
LOGGER.Log(INFO, context, message)
4662

4763

4864
def Warn(context, message):
49-
Log(WARN, context, message)
65+
LOGGER.Log(WARN, context, message)
5066

5167

5268
def Error(context, message):
53-
Log(ERROR, context, message)
69+
LOGGER.Log(ERROR, context, message)
5470

5571

5672
def shorten(msg, num_parts=5, max_len=100):

samples/apps/cap/py/autogencap/ag_adapter/AutoGenConnector.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from typing import Dict, Optional, Union
23

34
from autogen import Agent
@@ -37,7 +38,7 @@ def send_gen_reply_req(self):
3738
# Setting retry to -1 to keep trying until a response is received
3839
# This normal AutoGen behavior but does not handle the case when an AutoGen agent
3940
# is not running. In that case, the connector will keep trying indefinitely.
40-
_, _, resp = self._can_channel.binary_request(type(msg).__name__, serialized_msg, retry=-1)
41+
_, _, resp = self._can_channel.binary_request(type(msg).__name__, serialized_msg, num_attempts=-1)
4142
gen_reply_resp = GenReplyResp()
4243
gen_reply_resp.ParseFromString(resp)
4344
return gen_reply_resp.data
@@ -55,7 +56,8 @@ def send_receive_req(
5556
msg = ReceiveReq()
5657
if isinstance(message, dict):
5758
for key, value in message.items():
58-
msg.data_map.data[key] = value
59+
json_serialized_value = json.dumps(value)
60+
msg.data_map.data[key] = json_serialized_value
5961
elif isinstance(message, str):
6062
msg.data = message
6163
msg.sender = sender.name

samples/apps/cap/py/autogencap/ag_adapter/CAP2AG.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from enum import Enum
23
from typing import Optional
34

@@ -72,7 +73,11 @@ def _call_agent_receive(self, receive_params: ReceiveReq):
7273
save_name = self._ag2can_other_agent.name
7374
self._ag2can_other_agent.set_name(receive_params.sender)
7475
if receive_params.HasField("data_map"):
75-
data = dict(receive_params.data_map.data)
76+
json_data = dict(receive_params.data_map.data)
77+
data = {}
78+
for key, json_value in json_data.items():
79+
value = json.loads(json_value)
80+
data[key] = value
7681
else:
7782
data = receive_params.data
7883
self._the_ag_agent.receive(data, self._ag2can_other_agent, request_reply, silent)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import time
2+
3+
from autogen import ConversableAgent
4+
5+
from ..DebugLog import Info, Warn
6+
from .CAP2AG import CAP2AG
7+
8+
9+
class Agent:
10+
def __init__(self, agent: ConversableAgent, counter_party_name="user_proxy", init_chat=False):
11+
self._agent = agent
12+
self._the_other_name = counter_party_name
13+
self._agent_adptr = CAP2AG(
14+
ag_agent=self._agent, the_other_name=self._the_other_name, init_chat=init_chat, self_recursive=True
15+
)
16+
17+
def register(self, network):
18+
Info("Agent", f"Running Standalone {self._agent.name}")
19+
network.register(self._agent_adptr)
20+
21+
def running(self):
22+
return self._agent_adptr.run

samples/apps/cap/py/demo/App.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def main():
4545
print("3. AutoGen Pair")
4646
print("4. AutoGen GroupChat")
4747
print("5. AutoGen Agents in different processes")
48-
print("6. List Actors in CAP")
48+
print("6. List Actors in CAP (Registry)")
4949
choice = input("Enter your choice (1-6): ")
5050

5151
if choice == "1":

samples/apps/cap/py/demo/RemoteAGDemo.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
def remote_ag_demo():
66
print("Remote Agent Demo")
77
instructions = """
8-
In this demo, Broker, Assistant, and UserProxy are running in separate processes.
9-
demo/standalone/UserProxy.py will initiate a conversation by sending UserProxy a message.
8+
In this demo, Assistant, and UserProxy are running in separate processes.
9+
demo/standalone/user_proxy.py will initiate a conversation by sending UserProxy Agent a message.
1010
1111
Please do the following:
12-
1) Start Broker (python demo/standalone/Broker.py)
13-
2) Start Assistant (python demo/standalone/Assistant.py)
14-
3) Start UserProxy (python demo/standalone/UserProxy.py)
12+
1) Start Assistant (python demo/standalone/assistant.py)
13+
2) Start UserProxy (python demo/standalone/user_proxy.py)
1514
"""
1615
print(instructions)
1716
input("Press Enter to return to demo menu...")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import time
2+
3+
import _paths
4+
from autogencap.ag_adapter.agent import Agent
5+
from autogencap.Config import IGNORED_LOG_CONTEXTS
6+
from autogencap.LocalActorNetwork import LocalActorNetwork
7+
8+
from autogen import UserProxyAgent
9+
10+
# Filter out some Log message contexts
11+
IGNORED_LOG_CONTEXTS.extend(["BROKER"])
12+
13+
14+
def main():
15+
# Standard AutoGen
16+
user_proxy = UserProxyAgent(
17+
"user_proxy",
18+
code_execution_config={"work_dir": "coding"},
19+
is_termination_msg=lambda x: "TERMINATE" in x.get("content"),
20+
)
21+
22+
# Wrap AutoGen Agent in CAP
23+
cap_user_proxy = Agent(user_proxy, counter_party_name="assistant", init_chat=True)
24+
# Create the message bus
25+
network = LocalActorNetwork()
26+
# Add the user_proxy to the message bus
27+
cap_user_proxy.register(network)
28+
# Start message processing
29+
network.connect()
30+
31+
# Wait for the user_proxy to finish
32+
interact_with_user(network, cap_user_proxy)
33+
# Cleanup
34+
network.disconnect()
35+
36+
37+
# Starts the Broker and the Assistant. The UserProxy is started separately.
38+
def interact_with_user(network, cap_assistant):
39+
user_proxy_conn = network.lookup_actor("user_proxy")
40+
example = "Plot a chart of MSFT daily closing prices for last 1 Month."
41+
print(f"Example: {example}")
42+
try:
43+
user_input = input("Please enter your command: ")
44+
if user_input == "":
45+
user_input = example
46+
print(f"Sending: {user_input}")
47+
user_proxy_conn.send_txt_msg(user_input)
48+
49+
# Hang around for a while
50+
while cap_assistant.running():
51+
time.sleep(0.5)
52+
except KeyboardInterrupt:
53+
print("Interrupted by user, shutting down.")
54+
55+
56+
if __name__ == "__main__":
57+
main()

0 commit comments

Comments
 (0)