Skip to content

Commit 8f6590e

Browse files
authored
Feature: Composable Actor Platform for AutoGen (#1655)
* Core CAP components + Autogen adapter + Demo * Cleanup Readme * C# folder * Cleanup readme * summary_method bug fix * CAN -> CAP * pre-commit fixes * pre-commit fixes * modification of sys path should ignore E402 * fix pre-commit check issues * Updated docs * Clean up docs * more refactoring * better packaging refactor * Refactoring for package changes * Run demo app without autogencap installed or in the path * Remove debug related sleep() * removed CAP in some class names * Investigate a logging framework that supports color in windows * added type hints * remove circular dependency * fixed pre-commit issues * pre-commit ruff issues * removed circular definition * pre-commit fixes * Fix pre-commit issues * pre-commit fixes * updated for _prepare_chat signature changes * Better instructions for demo and some minor refactoring * Added details that explain CAP * Reformat Readme * More ReadMe Formatting * Readme edits * Agent -> Actor * Broker can startup on it's own * Remote AutoGen Agents * Updated docs * 1) StandaloneBroker in demo 2) Removed Autogen only demo options * 1) Agent -> Actor refactor 2) init broker as early * rename user_proxy -> user_proxy_conn * Add DirectorySvc * Standalone demo refactor * Get ActorInfo from DirectorySvc when searching for Actor * Broker cleanup * Proper cleanup and remove debug sleep() * Run one directory service only. * fix paths to run demo apps from command line * Handle keyboard interrupt * Wait for Broker and Directory to start up * Move Terminate AGActor * Accept input from the user in UserProxy * Move sleeps close to operations that bind or connect * Comments * Created an encapsulated CAP Pair for AutoGen pair communication * pre-commit checks * fix pre-commit * Pair should not make assumptions about who is first and who is second * Use task passed into InitiateChat * Standalone directory svc * Fix broken LFS files * Long running DirectorySvc * DirectorySvc does not have a status * Exit DirectorySvc Loop * Debugging Remoting * Reduce frequency of status messages * Debugging remote Actor * roll back git-lfs updates * rollback git-lfs changes * Debug network connectivity * pre-commit fixes * Create a group chat interface familiar to AutoGen GroupChat users * pre-commit fixes
1 parent a120f0e commit 8f6590e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2006
-0
lines changed

samples/apps/cap/README.md

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Composable Actor Platform (CAP) for AutoGen
2+
3+
## I just want to run the demo!
4+
*Python Instructions (Windows, Linux, MacOS):*
5+
6+
0) cd py
7+
1) pip install -r autogencap/requirements.txt
8+
2) python ./demo/App.py
9+
10+
*Demo Notes:*
11+
1) Options involving AutoGen require OAI_CONFIG_LIST.
12+
AutoGen python requirements: 3.8 <= python <= 3.11
13+
2) For option 2, type something in and see who receives the message. Quit to quit.
14+
3) To view any option that displays a chart (such as option 4), you will need to disable Docker code execution. You can do this by setting the environment variable `AUTOGEN_USE_DOCKER` to `False`.
15+
16+
*Demo Reference:*
17+
```
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):
26+
```
27+
28+
## What is Composable Actor Platform (CAP)?
29+
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.
30+
31+
Actors can register themselves with CAP, find other agents, construct arbitrary graphs, send and receive messages independently and many, many, many other things.
32+
```python
33+
# CAP Platform
34+
network = LocalActorNetwork()
35+
# Register an agent
36+
network.register(GreeterAgent())
37+
# Tell agents to connect to other agents
38+
network.connect()
39+
# Get a channel to the agent
40+
greeter_link = network.lookup_agent("Greeter")
41+
# Send a message to the agent
42+
greeter_link.send_txt_msg("Hello World!")
43+
# Cleanup
44+
greeter_link.close()
45+
network.disconnect()
46+
```
47+
### Check out other demos in the `py/demo` directory. We show the following: ###
48+
1) Hello World shown above
49+
2) Many CAP Actors interacting with each other
50+
3) A pair of interacting AutoGen Agents wrapped in CAP Actors
51+
4) CAP wrapped AutoGen Agents in a group chat
52+
53+
### Coming soon. Stay tuned! ###
54+
1) Two AutoGen Agents running in different processes and communicating through CAP

samples/apps/cap/TODO.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
- ~~Pretty print debug_logs~~
2+
- ~~colors~~
3+
- ~~messages to oai should be condensed~~
4+
- ~~remove orchestrator in scenario 4 and have the two actors talk to each other~~
5+
- ~~pass a complex multi-part message~~
6+
- ~~protobuf for messages~~
7+
- ~~make changes to autogen to enable scenario 3 to work with CAN~~
8+
- ~~make groupchat work~~
9+
- ~~actors instead of agents~~
10+
- clean up for PR into autogen
11+
- ~~Create folder structure under Autogen examples~~
12+
- ~~CAN -> CAP (Composable Actor Protocol)~~
13+
- CAP actor lookup should use zmq
14+
- Add min C# actors & reorganize
15+
- Hybrid GroupChat with C# ProductManager
16+
- C++ Msg Layer
17+
- Rust Msg Layer
18+
- Node Msg Layer
19+
- Java Msg Layer
20+
- Investigate a standard logging framework that supports color in windows
21+
- structlog?

samples/apps/cap/c#/Readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Coming soon...

samples/apps/cap/c++/Readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Coming soon...

samples/apps/cap/node/Readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Coming soon...
+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import zmq
2+
import threading
3+
import traceback
4+
import time
5+
from .DebugLog import Debug, Info
6+
from .Config import xpub_url
7+
8+
9+
class Actor:
10+
def __init__(self, agent_name: str, description: str):
11+
self.actor_name: str = agent_name
12+
self.agent_description: str = description
13+
self.run = False
14+
15+
def connect_network(self, network):
16+
Debug(self.actor_name, f"is connecting to {network}")
17+
Debug(self.actor_name, "connected")
18+
19+
def _process_txt_msg(self, msg: str, msg_type: str, topic: str, sender: str) -> bool:
20+
Info(self.actor_name, f"InBox: {msg}")
21+
return True
22+
23+
def _process_bin_msg(self, msg: bytes, msg_type: str, topic: str, sender: str) -> bool:
24+
Info(self.actor_name, f"Msg: topic=[{topic}], msg_type=[{msg_type}]")
25+
return True
26+
27+
def _recv_thread(self):
28+
Debug(self.actor_name, "recv thread started")
29+
self._socket: zmq.Socket = self._context.socket(zmq.SUB)
30+
self._socket.setsockopt(zmq.RCVTIMEO, 500)
31+
self._socket.connect(xpub_url)
32+
str_topic = f"{self.actor_name}"
33+
Debug(self.actor_name, f"subscribe to: {str_topic}")
34+
self._socket.setsockopt_string(zmq.SUBSCRIBE, f"{str_topic}")
35+
try:
36+
while self.run:
37+
try:
38+
topic, msg_type, sender_topic, msg = self._socket.recv_multipart()
39+
topic = topic.decode("utf-8") # Convert bytes to string
40+
msg_type = msg_type.decode("utf-8") # Convert bytes to string
41+
sender_topic = sender_topic.decode("utf-8") # Convert bytes to string
42+
except zmq.Again:
43+
continue # No message received, continue to next iteration
44+
except Exception:
45+
continue
46+
if msg_type == "text":
47+
msg = msg.decode("utf-8") # Convert bytes to string
48+
if not self._process_txt_msg(msg, msg_type, topic, sender_topic):
49+
msg = "quit"
50+
if msg.lower() == "quit":
51+
break
52+
else:
53+
if not self._process_bin_msg(msg, msg_type, topic, sender_topic):
54+
break
55+
except Exception as e:
56+
Debug(self.actor_name, f"recv thread encountered an error: {e}")
57+
traceback.print_exc()
58+
finally:
59+
self.run = False
60+
Debug(self.actor_name, "recv thread ended")
61+
62+
def start(self, context: zmq.Context):
63+
self._context = context
64+
self.run: bool = True
65+
self._thread = threading.Thread(target=self._recv_thread)
66+
self._thread.start()
67+
time.sleep(0.01)
68+
69+
def disconnect_network(self, network):
70+
Debug(self.actor_name, f"is disconnecting from {network}")
71+
Debug(self.actor_name, "disconnected")
72+
self.stop()
73+
74+
def stop(self):
75+
self.run = False
76+
self._thread.join()
77+
self._socket.setsockopt(zmq.LINGER, 0)
78+
self._socket.close()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Agent_Sender takes a zmq context, Topic and creates a
2+
# socket that can publish to that topic. It exposes this functionality
3+
# using send_msg method
4+
import zmq
5+
import time
6+
import uuid
7+
from .DebugLog import Debug, Error
8+
from .Config import xsub_url, xpub_url
9+
10+
11+
class ActorConnector:
12+
def __init__(self, context, topic):
13+
self._pub_socket = context.socket(zmq.PUB)
14+
self._pub_socket.setsockopt(zmq.LINGER, 0)
15+
self._pub_socket.connect(xsub_url)
16+
17+
self._resp_socket = context.socket(zmq.SUB)
18+
self._resp_socket.setsockopt(zmq.LINGER, 0)
19+
self._resp_socket.setsockopt(zmq.RCVTIMEO, 10000)
20+
self._resp_socket.connect(xpub_url)
21+
self._resp_topic = str(uuid.uuid4())
22+
Debug("AgentConnector", f"subscribe to: {self._resp_topic}")
23+
self._resp_socket.setsockopt_string(zmq.SUBSCRIBE, f"{self._resp_topic}")
24+
self._topic = topic
25+
time.sleep(0.01) # Let the network do things.
26+
27+
def send_txt_msg(self, msg):
28+
self._pub_socket.send_multipart(
29+
[self._topic.encode("utf8"), "text".encode("utf8"), self._resp_topic.encode("utf8"), msg.encode("utf8")]
30+
)
31+
32+
def send_bin_msg(self, msg_type: str, msg):
33+
self._pub_socket.send_multipart(
34+
[self._topic.encode("utf8"), msg_type.encode("utf8"), self._resp_topic.encode("utf8"), msg]
35+
)
36+
37+
def binary_request(self, msg_type: str, msg, retry=5):
38+
time.sleep(0.5) # Let the network do things.
39+
self._pub_socket.send_multipart(
40+
[self._topic.encode("utf8"), msg_type.encode("utf8"), self._resp_topic.encode("utf8"), msg]
41+
)
42+
time.sleep(0.5) # Let the network do things.
43+
for i in range(retry + 1):
44+
try:
45+
self._resp_socket.setsockopt(zmq.RCVTIMEO, 10000)
46+
resp_topic, resp_msg_type, resp_sender_topic, resp = self._resp_socket.recv_multipart()
47+
return resp_topic, resp_msg_type, resp_sender_topic, resp
48+
except zmq.Again:
49+
Debug("ActorConnector", f"binary_request: No response received. retry_count={i}, max_retry={retry}")
50+
time.sleep(0.01) # Wait a bit before retrying
51+
continue
52+
Error("ActorConnector", "binary_request: No response received. Giving up.")
53+
return None, None, None, None
54+
55+
def close(self):
56+
self._pub_socket.close()
57+
self._resp_socket.close()
+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import time
2+
import zmq
3+
import threading
4+
from autogencap.DebugLog import Debug, Info, Warn
5+
from autogencap.Config import xsub_url, xpub_url
6+
7+
8+
class Broker:
9+
def __init__(self, context: zmq.Context = zmq.Context()):
10+
self._context: zmq.Context = context
11+
self._run: bool = False
12+
self._xpub: zmq.Socket = None
13+
self._xsub: zmq.Socket = None
14+
15+
def start(self) -> bool:
16+
try:
17+
# XPUB setup
18+
self._xpub = self._context.socket(zmq.XPUB)
19+
self._xpub.setsockopt(zmq.LINGER, 0)
20+
self._xpub.bind(xpub_url)
21+
22+
# XSUB setup
23+
self._xsub = self._context.socket(zmq.XSUB)
24+
self._xsub.setsockopt(zmq.LINGER, 0)
25+
self._xsub.bind(xsub_url)
26+
27+
except zmq.ZMQError as e:
28+
Debug("BROKER", f"Unable to start. Check details: {e}")
29+
# If binding fails, close the sockets and return False
30+
if self._xpub:
31+
self._xpub.close()
32+
if self._xsub:
33+
self._xsub.close()
34+
return False
35+
36+
self._run = True
37+
self._broker_thread: threading.Thread = threading.Thread(target=self.thread_fn)
38+
self._broker_thread.start()
39+
time.sleep(0.01)
40+
return True
41+
42+
def stop(self):
43+
# Error("BROKER_ERR", "fix cleanup self._context.term()")
44+
Debug("BROKER", "stopped")
45+
self._run = False
46+
self._broker_thread.join()
47+
if self._xpub:
48+
self._xpub.close()
49+
if self._xsub:
50+
self._xsub.close()
51+
# self._context.term()
52+
53+
def thread_fn(self):
54+
try:
55+
# Poll sockets for events
56+
self._poller: zmq.Poller = zmq.Poller()
57+
self._poller.register(self._xpub, zmq.POLLIN)
58+
self._poller.register(self._xsub, zmq.POLLIN)
59+
60+
# Receive msgs, forward and process
61+
while self._run:
62+
events = dict(self._poller.poll(500))
63+
if self._xpub in events:
64+
message = self._xpub.recv_multipart()
65+
Debug("BROKER", f"subscription message: {message[0]}")
66+
self._xsub.send_multipart(message)
67+
68+
if self._xsub in events:
69+
message = self._xsub.recv_multipart()
70+
Debug("BROKER", f"publishing message: {message}")
71+
self._xpub.send_multipart(message)
72+
73+
except Exception as e:
74+
Debug("BROKER", f"thread encountered an error: {e}")
75+
finally:
76+
self._run = False
77+
Debug("BROKER", "thread ended")
78+
return
79+
80+
81+
# Run a standalone broker that all other Actors can connect to.
82+
# This can also run inproc with the other actors.
83+
def main():
84+
broker = Broker()
85+
Info("BROKER", "Starting.")
86+
if broker.start():
87+
Info("BROKER", "Running.")
88+
else:
89+
Warn("BROKER", "Failed to start.")
90+
return
91+
92+
status_interval = 300 # seconds
93+
last_time = time.time()
94+
95+
# Broker is running in a separate thread. Here we are watching the
96+
# broker's status and printing status every few seconds. This is
97+
# a good place to print other statistics captured as the broker runs.
98+
# -- Exits when the user presses Ctrl+C --
99+
while broker._run:
100+
# print a message every n seconds
101+
current_time = time.time()
102+
elapsed_time = current_time - last_time
103+
if elapsed_time > status_interval:
104+
Info("BROKER", "Running.")
105+
last_time = current_time
106+
try:
107+
time.sleep(0.5)
108+
except KeyboardInterrupt:
109+
Info("BROKER", "KeyboardInterrupt. Stopping the broker.")
110+
broker.stop()
111+
112+
113+
if __name__ == "__main__":
114+
main()
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Set the current log level
2+
LOG_LEVEL = 0
3+
IGNORED_LOG_CONTEXTS = []
4+
xpub_url: str = "tcp://127.0.0.1:5555"
5+
xsub_url: str = "tcp://127.0.0.1:5556"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Termination_Topic: str = "Termination"
2+
Directory_Svc_Topic: str = "Directory_Svc"

0 commit comments

Comments
 (0)