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
This commit is contained in:
Rajan 2024-03-13 00:48:52 -04:00 committed by GitHub
parent a120f0ed2b
commit 8f6590e231
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 2006 additions and 0 deletions

View File

@ -0,0 +1,54 @@
# Composable Actor Platform (CAP) for AutoGen
## I just want to run the demo!
*Python Instructions (Windows, Linux, MacOS):*
0) cd py
1) pip install -r autogencap/requirements.txt
2) python ./demo/App.py
*Demo Notes:*
1) Options involving AutoGen require OAI_CONFIG_LIST.
AutoGen python requirements: 3.8 <= python <= 3.11
2) For option 2, type something in and see who receives the message. Quit to quit.
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`.
*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):
```
## 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
### Coming soon. Stay tuned! ###
1) Two AutoGen Agents running in different processes and communicating through CAP

21
samples/apps/cap/TODO.md Normal file
View File

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

View File

@ -0,0 +1 @@
Coming soon...

View File

@ -0,0 +1 @@
Coming soon...

View File

@ -0,0 +1 @@
Coming soon...

View File

@ -0,0 +1,78 @@
import zmq
import threading
import traceback
import time
from .DebugLog import Debug, Info
from .Config import xpub_url
class Actor:
def __init__(self, agent_name: str, description: str):
self.actor_name: str = agent_name
self.agent_description: str = description
self.run = False
def connect_network(self, network):
Debug(self.actor_name, f"is connecting to {network}")
Debug(self.actor_name, "connected")
def _process_txt_msg(self, msg: str, msg_type: str, topic: str, sender: str) -> bool:
Info(self.actor_name, f"InBox: {msg}")
return True
def _process_bin_msg(self, msg: bytes, msg_type: str, topic: str, sender: str) -> bool:
Info(self.actor_name, f"Msg: topic=[{topic}], msg_type=[{msg_type}]")
return True
def _recv_thread(self):
Debug(self.actor_name, "recv thread started")
self._socket: zmq.Socket = self._context.socket(zmq.SUB)
self._socket.setsockopt(zmq.RCVTIMEO, 500)
self._socket.connect(xpub_url)
str_topic = f"{self.actor_name}"
Debug(self.actor_name, f"subscribe to: {str_topic}")
self._socket.setsockopt_string(zmq.SUBSCRIBE, f"{str_topic}")
try:
while self.run:
try:
topic, msg_type, sender_topic, msg = self._socket.recv_multipart()
topic = topic.decode("utf-8") # Convert bytes to string
msg_type = msg_type.decode("utf-8") # Convert bytes to string
sender_topic = sender_topic.decode("utf-8") # Convert bytes to string
except zmq.Again:
continue # No message received, continue to next iteration
except Exception:
continue
if msg_type == "text":
msg = msg.decode("utf-8") # Convert bytes to string
if not self._process_txt_msg(msg, msg_type, topic, sender_topic):
msg = "quit"
if msg.lower() == "quit":
break
else:
if not self._process_bin_msg(msg, msg_type, topic, sender_topic):
break
except Exception as e:
Debug(self.actor_name, f"recv thread encountered an error: {e}")
traceback.print_exc()
finally:
self.run = False
Debug(self.actor_name, "recv thread ended")
def start(self, context: zmq.Context):
self._context = context
self.run: bool = True
self._thread = threading.Thread(target=self._recv_thread)
self._thread.start()
time.sleep(0.01)
def disconnect_network(self, network):
Debug(self.actor_name, f"is disconnecting from {network}")
Debug(self.actor_name, "disconnected")
self.stop()
def stop(self):
self.run = False
self._thread.join()
self._socket.setsockopt(zmq.LINGER, 0)
self._socket.close()

View File

@ -0,0 +1,57 @@
# Agent_Sender takes a zmq context, Topic and creates a
# socket that can publish to that topic. It exposes this functionality
# using send_msg method
import zmq
import time
import uuid
from .DebugLog import Debug, Error
from .Config import xsub_url, xpub_url
class ActorConnector:
def __init__(self, context, topic):
self._pub_socket = context.socket(zmq.PUB)
self._pub_socket.setsockopt(zmq.LINGER, 0)
self._pub_socket.connect(xsub_url)
self._resp_socket = context.socket(zmq.SUB)
self._resp_socket.setsockopt(zmq.LINGER, 0)
self._resp_socket.setsockopt(zmq.RCVTIMEO, 10000)
self._resp_socket.connect(xpub_url)
self._resp_topic = str(uuid.uuid4())
Debug("AgentConnector", f"subscribe to: {self._resp_topic}")
self._resp_socket.setsockopt_string(zmq.SUBSCRIBE, f"{self._resp_topic}")
self._topic = topic
time.sleep(0.01) # Let the network do things.
def send_txt_msg(self, msg):
self._pub_socket.send_multipart(
[self._topic.encode("utf8"), "text".encode("utf8"), self._resp_topic.encode("utf8"), msg.encode("utf8")]
)
def send_bin_msg(self, msg_type: str, msg):
self._pub_socket.send_multipart(
[self._topic.encode("utf8"), msg_type.encode("utf8"), self._resp_topic.encode("utf8"), msg]
)
def binary_request(self, msg_type: str, msg, retry=5):
time.sleep(0.5) # Let the network do things.
self._pub_socket.send_multipart(
[self._topic.encode("utf8"), msg_type.encode("utf8"), self._resp_topic.encode("utf8"), msg]
)
time.sleep(0.5) # Let the network do things.
for i in range(retry + 1):
try:
self._resp_socket.setsockopt(zmq.RCVTIMEO, 10000)
resp_topic, resp_msg_type, resp_sender_topic, resp = self._resp_socket.recv_multipart()
return resp_topic, resp_msg_type, resp_sender_topic, resp
except zmq.Again:
Debug("ActorConnector", f"binary_request: No response received. retry_count={i}, max_retry={retry}")
time.sleep(0.01) # Wait a bit before retrying
continue
Error("ActorConnector", "binary_request: No response received. Giving up.")
return None, None, None, None
def close(self):
self._pub_socket.close()
self._resp_socket.close()

View File

@ -0,0 +1,114 @@
import time
import zmq
import threading
from autogencap.DebugLog import Debug, Info, Warn
from autogencap.Config import xsub_url, xpub_url
class Broker:
def __init__(self, context: zmq.Context = zmq.Context()):
self._context: zmq.Context = context
self._run: bool = False
self._xpub: zmq.Socket = None
self._xsub: zmq.Socket = None
def start(self) -> bool:
try:
# XPUB setup
self._xpub = self._context.socket(zmq.XPUB)
self._xpub.setsockopt(zmq.LINGER, 0)
self._xpub.bind(xpub_url)
# XSUB setup
self._xsub = self._context.socket(zmq.XSUB)
self._xsub.setsockopt(zmq.LINGER, 0)
self._xsub.bind(xsub_url)
except zmq.ZMQError as e:
Debug("BROKER", f"Unable to start. Check details: {e}")
# If binding fails, close the sockets and return False
if self._xpub:
self._xpub.close()
if self._xsub:
self._xsub.close()
return False
self._run = True
self._broker_thread: threading.Thread = threading.Thread(target=self.thread_fn)
self._broker_thread.start()
time.sleep(0.01)
return True
def stop(self):
# Error("BROKER_ERR", "fix cleanup self._context.term()")
Debug("BROKER", "stopped")
self._run = False
self._broker_thread.join()
if self._xpub:
self._xpub.close()
if self._xsub:
self._xsub.close()
# self._context.term()
def thread_fn(self):
try:
# Poll sockets for events
self._poller: zmq.Poller = zmq.Poller()
self._poller.register(self._xpub, zmq.POLLIN)
self._poller.register(self._xsub, zmq.POLLIN)
# Receive msgs, forward and process
while self._run:
events = dict(self._poller.poll(500))
if self._xpub in events:
message = self._xpub.recv_multipart()
Debug("BROKER", f"subscription message: {message[0]}")
self._xsub.send_multipart(message)
if self._xsub in events:
message = self._xsub.recv_multipart()
Debug("BROKER", f"publishing message: {message}")
self._xpub.send_multipart(message)
except Exception as e:
Debug("BROKER", f"thread encountered an error: {e}")
finally:
self._run = False
Debug("BROKER", "thread ended")
return
# Run a standalone broker that all other Actors can connect to.
# This can also run inproc with the other actors.
def main():
broker = Broker()
Info("BROKER", "Starting.")
if broker.start():
Info("BROKER", "Running.")
else:
Warn("BROKER", "Failed to start.")
return
status_interval = 300 # seconds
last_time = time.time()
# Broker is running in a separate thread. Here we are watching the
# broker's status and printing status every few seconds. This is
# a good place to print other statistics captured as the broker runs.
# -- Exits when the user presses Ctrl+C --
while broker._run:
# print a message every n seconds
current_time = time.time()
elapsed_time = current_time - last_time
if elapsed_time > status_interval:
Info("BROKER", "Running.")
last_time = current_time
try:
time.sleep(0.5)
except KeyboardInterrupt:
Info("BROKER", "KeyboardInterrupt. Stopping the broker.")
broker.stop()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,5 @@
# Set the current log level
LOG_LEVEL = 0
IGNORED_LOG_CONTEXTS = []
xpub_url: str = "tcp://127.0.0.1:5555"
xsub_url: str = "tcp://127.0.0.1:5556"

View File

@ -0,0 +1,2 @@
Termination_Topic: str = "Termination"
Directory_Svc_Topic: str = "Directory_Svc"

View File

@ -0,0 +1,73 @@
import threading
import datetime
from autogencap.Config import LOG_LEVEL, IGNORED_LOG_CONTEXTS
from termcolor import colored
# Define log levels as constants
DEBUG = 0
INFO = 1
WARN = 2
ERROR = 3
# Map log levels to their names
LEVEL_NAMES = ["DBG", "INF", "WRN", "ERR"]
LEVEL_COLOR = ["dark_grey", "green", "yellow", "red"]
console_lock = threading.Lock()
def Log(level, context, msg):
# Check if the current level meets the threshold
if level >= LOG_LEVEL: # Use the LOG_LEVEL from the Config module
# Check if the context is in the list of ignored contexts
if context in 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])
# Center justify the context and color it blue
context = colored(context.ljust(14), "blue")
# color the msg based on the level
msg = colored(msg, LEVEL_COLOR[level])
print(f"{threading.get_ident()} {timestamp} {level_name}: [{context}] {msg}")
def Debug(context, message):
Log(DEBUG, context, message)
def Info(context, message):
Log(INFO, context, message)
def Warn(context, message):
Log(WARN, context, message)
def Error(context, message):
Log(ERROR, context, message)
def shorten(msg, num_parts=5, max_len=100):
# Remove new lines
msg = msg.replace("\n", " ")
# If the message is shorter than or equal to max_len characters, return it as is
if len(msg) <= max_len:
return msg
# Calculate the length of each part
part_len = max_len // num_parts
# Create a list to store the parts
parts = []
# Get the parts from the message
for i in range(num_parts):
start = i * part_len
end = start + part_len
parts.append(msg[start:end])
# Join the parts with '...' and return the result
return "...".join(parts)

View File

@ -0,0 +1,210 @@
from autogencap.Constants import Directory_Svc_Topic
from autogencap.Config import xpub_url, xsub_url
from autogencap.DebugLog import Debug, Info, Error
from autogencap.ActorConnector import ActorConnector
from autogencap.Actor import Actor
from autogencap.proto.CAP_pb2 import ActorRegistration, ActorInfo, ActorLookup, ActorLookupResponse, Ping, Pong
import zmq
import threading
import time
# TODO (Future DirectorySv PR) use actor description, network_id, other properties to make directory
# service more generic and powerful
class DirectoryActor(Actor):
def __init__(self, topic: str, name: str):
super().__init__(topic, name)
self._registered_actors = {}
self._network_prefix = ""
def _process_bin_msg(self, msg: bytes, msg_type: str, topic: str, sender: str) -> bool:
if msg_type == ActorRegistration.__name__:
self._actor_registration_msg_handler(topic, msg_type, msg)
elif msg_type == ActorLookup.__name__:
self._actor_lookup_msg_handler(topic, msg_type, msg, sender)
elif msg_type == Ping.__name__:
self._ping_msg_handler(topic, msg_type, msg, sender)
else:
Error("DirectorySvc", f"Unknown message type: {msg_type}")
return True
def _ping_msg_handler(self, topic: str, msg_type: str, msg: bytes, sender_topic: str):
Info("DirectorySvc", f"Ping received: {sender_topic}")
pong = Pong()
serialized_msg = pong.SerializeToString()
sender_connection = ActorConnector(self._context, sender_topic)
sender_connection.send_bin_msg(Pong.__name__, serialized_msg)
def _actor_registration_msg_handler(self, topic: str, msg_type: str, msg: bytes):
actor_reg = ActorRegistration()
actor_reg.ParseFromString(msg)
Info("DirectorySvc", f"Actor registration: {actor_reg.actor_info.name}")
name = actor_reg.actor_info.name
# TODO (Future DirectorySv PR) network_id should be namespace prefixed to support multiple networks
actor_reg.actor_info.name + self._network_prefix
if name in self._registered_actors:
Error("DirectorySvc", f"Actor already registered: {name}")
return
self._registered_actors[name] = actor_reg.actor_info
def _actor_lookup_msg_handler(self, topic: str, msg_type: str, msg: bytes, sender_topic: str):
actor_lookup = ActorLookup()
actor_lookup.ParseFromString(msg)
Debug("DirectorySvc", f"Actor lookup: {actor_lookup.actor_info.name}")
actor: ActorInfo = None
if actor_lookup.actor_info.name in self._registered_actors:
Info("DirectorySvc", f"Actor found: {actor_lookup.actor_info.name}")
actor = self._registered_actors[actor_lookup.actor_info.name]
else:
Error("DirectorySvc", f"Actor not found: {actor_lookup.actor_info.name}")
actor_lookup_resp = ActorLookupResponse()
if actor is not None:
actor_lookup_resp.actor.info_coll.extend([actor])
actor_lookup_resp.found = True
else:
actor_lookup_resp.found = False
sender_connection = ActorConnector(self._context, sender_topic)
serialized_msg = actor_lookup_resp.SerializeToString()
sender_connection.send_bin_msg(ActorLookupResponse.__name__, serialized_msg)
class DirectorySvc:
def __init__(self, context: zmq.Context = zmq.Context()):
self._context: zmq.Context = context
self._directory_connector: ActorConnector = None
self._directory_actor: DirectoryActor = None
def _no_other_directory(self) -> bool:
ping = Ping()
serialized_msg = ping.SerializeToString()
_, _, _, resp = self._directory_connector.binary_request(Ping.__name__, serialized_msg, retry=0)
if resp is None:
return True
return False
def start(self):
self._directory_connector = ActorConnector(self._context, Directory_Svc_Topic)
if self._no_other_directory():
self._directory_actor = DirectoryActor(Directory_Svc_Topic, "Directory Service")
self._directory_actor.start(self._context)
Info("DirectorySvc", "Directory service started.")
else:
Info("DirectorySvc", "Another directory service is running. This instance will not start.")
def stop(self):
if self._directory_actor:
self._directory_actor.stop()
if self._directory_connector:
self._directory_connector.close()
def register_actor(self, actor_info: ActorInfo):
# Send a message to the directory service
# to register the actor
actor_reg = ActorRegistration()
actor_reg.actor_info.CopyFrom(actor_info)
serialized_msg = actor_reg.SerializeToString()
self._directory_connector.send_bin_msg(ActorRegistration.__name__, serialized_msg)
def register_actor_by_name(self, actor_name: str):
actor_info = ActorInfo(name=actor_name)
self.register_actor(actor_info)
def lookup_actor_by_name(self, actor_name: str) -> ActorInfo:
actor_info = ActorInfo(name=actor_name)
actor_lookup = ActorLookup(actor_info=actor_info)
serialized_msg = actor_lookup.SerializeToString()
_, _, _, resp = self._directory_connector.binary_request(ActorLookup.__name__, serialized_msg)
actor_lookup_resp = ActorLookupResponse()
actor_lookup_resp.ParseFromString(resp)
if actor_lookup_resp.found:
if len(actor_lookup_resp.actor.info_coll) > 0:
return actor_lookup_resp.actor.info_coll[0]
return None
# Standalone min proxy for a standalone directory service
class MinProxy:
def __init__(self, context: zmq.Context):
self._context: zmq.Context = context
self._xpub: zmq.Socket = None
self._xsub: zmq.Socket = None
def start(self):
# Start the proxy thread
proxy_thread = threading.Thread(target=self.proxy_thread_fn)
proxy_thread.start()
time.sleep(0.01)
def stop(self):
self._xsub.setsockopt(zmq.LINGER, 0)
self._xpub.setsockopt(zmq.LINGER, 0)
self._xpub.close()
self._xsub.close()
time.sleep(0.01)
def proxy_thread_fn(self):
self._xpub: zmq.Socket = self._context.socket(zmq.XPUB)
self._xsub: zmq.Socket = self._context.socket(zmq.XSUB)
try:
self._xpub.bind(xpub_url)
self._xsub.bind(xsub_url)
zmq.proxy(self._xpub, self._xsub)
except zmq.ContextTerminated:
self._xpub.close()
self._xsub.close()
except Exception as e:
Error("proxy_thread_fn", f"proxy_thread_fn encountered an error: {e}")
self._xpub.setsockopt(zmq.LINGER, 0)
self._xsub.setsockopt(zmq.LINGER, 0)
self._xpub.close()
self._xsub.close()
finally:
Info("proxy_thread_fn", "proxy_thread_fn terminated.")
# Run a standalone directory service
def main():
context: zmq.Context = zmq.Context()
# Start simple broker (will exit if real broker is running)
proxy: MinProxy = MinProxy(context)
proxy.start()
# Start the directory service
directory_svc = DirectorySvc(context)
directory_svc.start()
# # How do you register an actor?
# directory_svc.register_actor_by_name("my_actor")
#
# # How do you look up an actor?
# actor: ActorInfo = directory_svc.lookup_actor_by_name("my_actor")
# if actor is not None:
# Info("main", f"Found actor: {actor.name}")
# DirectorySvc is running in a separate thread. Here we are watching the
# status and printing status every few seconds. This is
# a good place to print other statistics captured as the broker runs.
# -- Exits when the user presses Ctrl+C --
status_interval = 300 # seconds
last_time = time.time()
while True:
# print a message every n seconds
current_time = time.time()
elapsed_time = current_time - last_time
if elapsed_time > status_interval:
Info("DirectorySvc", "Running.")
last_time = current_time
try:
time.sleep(0.5)
except KeyboardInterrupt:
Info("DirectorySvc", "KeyboardInterrupt. Stopping the DirectorySvc.")
break
directory_svc.stop()
proxy.stop()
context.term()
Info("main", "Done.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,71 @@
import zmq
from .DebugLog import Debug, Warn
from .ActorConnector import ActorConnector
from .Broker import Broker
from .DirectorySvc import DirectorySvc
from .Constants import Termination_Topic
from .Actor import Actor
from .proto.CAP_pb2 import ActorInfo
import time
# TODO: remove time import
class LocalActorNetwork:
def __init__(self, name: str = "Local Actor Network", start_broker: bool = True):
self.local_actors = {}
self.name: str = name
self._context: zmq.Context = zmq.Context()
self._start_broker: bool = start_broker
self._broker: Broker = None
self._directory_svc: DirectorySvc = None
def __str__(self):
return f"{self.name}"
def _init_runtime(self):
if self._start_broker and self._broker is None:
self._broker = Broker(self._context)
if not self._broker.start():
self._start_broker = False # Don't try to start the broker again
self._broker = None
if self._directory_svc is None:
self._directory_svc = DirectorySvc(self._context)
self._directory_svc.start()
def register(self, actor: Actor):
self._init_runtime()
# Get actor's name and description and add to a dictionary so
# that we can look up the actor by name
self._directory_svc.register_actor_by_name(actor.actor_name)
self.local_actors[actor.actor_name] = actor
actor.start(self._context)
Debug("Local_Actor_Network", f"{actor.actor_name} registered in the network.")
def connect(self):
self._init_runtime()
for actor in self.local_actors.values():
actor.connect_network(self)
def disconnect(self):
for actor in self.local_actors.values():
actor.disconnect_network(self)
if self._directory_svc:
self._directory_svc.stop()
if self._broker:
self._broker.stop()
def actor_connector_by_topic(self, topic: str) -> ActorConnector:
return ActorConnector(self._context, topic)
def lookup_actor(self, name: str) -> ActorConnector:
actor_info: ActorInfo = self._directory_svc.lookup_actor_by_name(name)
if actor_info is None:
Warn("Local_Actor_Network", f"{name}, not found in the network.")
return None
Debug("Local_Actor_Network", f"[{name}] found in the network.")
return self.actor_connector_by_topic(name)
def lookup_termination(self) -> ActorConnector:
termination_topic: str = Termination_Topic
return self.actor_connector_by_topic(termination_topic)

View File

@ -0,0 +1,81 @@
import time
from typing import Callable, Dict, List, Optional, Union
from autogen import Agent, ConversableAgent
from .AutoGenConnector import AutoGenConnector
from ..LocalActorNetwork import LocalActorNetwork
class AG2CAP(ConversableAgent):
"""
A conversable agent proxy that sends messages to CAN when called
"""
def __init__(
self,
network: LocalActorNetwork,
agent_name: str,
agent_description: Optional[str] = None,
):
super().__init__(name=agent_name, description=agent_description, llm_config=False)
self._agent_connector: AutoGenConnector = None
self._network: LocalActorNetwork = network
self._recv_called = False
def reset_receive_called(self):
self._recv_called = False
def was_receive_called(self):
return self._recv_called
def set_name(self, name: str):
"""
Set the name of the agent.
Why? because we need it to look like different agents
"""
self._name = name
def _check_connection(self):
if self._agent_connector is None:
self._agent_connector = AutoGenConnector(self._network.lookup_actor(self.name))
self._terminate_connector = AutoGenConnector(self._network.lookup_termination())
def receive(
self,
message: Union[Dict, str],
sender: Agent,
request_reply: Optional[bool] = None,
silent: Optional[bool] = False,
):
"""
Receive a message from the AutoGen system.
"""
self._recv_called = True
self._check_connection()
self._agent_connector.send_receive_req(message, sender, request_reply, silent)
def generate_reply(
self,
messages: Optional[List[Dict]] = None,
sender: Optional[Agent] = None,
exclude: Optional[List[Callable]] = None,
) -> Union[str, Dict, None]:
"""
Generate a reply message for the AutoGen system.
"""
self._check_connection()
return self._agent_connector.send_gen_reply_req()
def _prepare_chat(
self,
recipient: ConversableAgent,
clear_history: bool,
prepare_recipient: bool = True,
reply_at_receive: bool = True,
) -> None:
self._check_connection()
self._agent_connector.send_prep_chat(recipient, clear_history, prepare_recipient)
def send_terminate(self, recipient: ConversableAgent) -> None:
self._check_connection()
self._agent_connector.send_terminate(recipient)
self._terminate_connector.send_terminate(self)

View File

@ -0,0 +1,12 @@
import zmq
from autogencap.Actor import Actor
from autogencap.Constants import Termination_Topic
from autogencap.DebugLog import Debug
class AGActor(Actor):
def start(self, context: zmq.Context):
super().start(context)
str_topic = Termination_Topic
Debug(self.actor_name, f"subscribe to: {str_topic}")
self._socket.setsockopt_string(zmq.SUBSCRIBE, f"{str_topic}")

View File

@ -0,0 +1,84 @@
from typing import Dict, Optional, Union
from autogen import Agent
from ..ActorConnector import ActorConnector
from ..proto.Autogen_pb2 import GenReplyReq, GenReplyResp, PrepChat, ReceiveReq, Terminate
class AutoGenConnector:
"""
A specialized ActorConnector class for sending and receiving Autogen messages
to/from the CAP system.
"""
def __init__(self, cap_sender: ActorConnector):
self._can_channel: ActorConnector = cap_sender
def close(self):
"""
Close the connector.
"""
self._can_channel.close()
def _send_msg(self, msg):
"""
Send a message to CAP.
"""
serialized_msg = msg.SerializeToString()
self._can_channel.send_bin_msg(type(msg).__name__, serialized_msg)
def send_gen_reply_req(self):
"""
Send a GenReplyReq message to CAP and receive the response.
"""
msg = GenReplyReq()
serialized_msg = msg.SerializeToString()
_, _, _, resp = self._can_channel.binary_request(type(msg).__name__, serialized_msg)
gen_reply_resp = GenReplyResp()
gen_reply_resp.ParseFromString(resp)
return gen_reply_resp.data
def send_receive_req(
self,
message: Union[Dict, str],
sender: Agent,
request_reply: Optional[bool] = None,
silent: Optional[bool] = False,
):
"""
Send a ReceiveReq message to CAP.
"""
msg = ReceiveReq()
if isinstance(message, dict):
for key, value in message.items():
msg.data_map.data[key] = value
elif isinstance(message, str):
msg.data = message
msg.sender = sender.name
if request_reply is not None:
msg.request_reply = request_reply
if silent is not None:
msg.silent = silent
self._send_msg(msg)
def send_terminate(self, sender: Agent):
"""
Send a Terminate message to CAP.
"""
msg = Terminate()
msg.sender = sender.name
self._send_msg(msg)
def send_prep_chat(self, recipient: "Agent", clear_history: bool, prepare_recipient: bool = True) -> None:
"""
Send a PrepChat message to CAP.
Args:
recipient (Agent): _description_
clear_history (bool): _description_
prepare_recipient (bool, optional): _description_. Defaults to True.
"""
msg = PrepChat()
msg.recipient = recipient.name
msg.clear_history = clear_history
msg.prepare_recipient = prepare_recipient
self._send_msg(msg)

View File

@ -0,0 +1,155 @@
from enum import Enum
from typing import Optional
from ..DebugLog import Debug, Error, Info, Warn, shorten
from ..LocalActorNetwork import LocalActorNetwork
from ..proto.Autogen_pb2 import GenReplyReq, GenReplyResp, PrepChat, ReceiveReq, Terminate
from .AGActor import AGActor
from .AG2CAP import AG2CAP
from autogen import ConversableAgent
class CAP2AG(AGActor):
"""
A CAN actor that acts as an adapter for the AutoGen system.
"""
States = Enum("States", ["INIT", "CONVERSING"])
def __init__(self, ag_agent: ConversableAgent, the_other_name: str, init_chat: bool, self_recursive: bool = True):
super().__init__(ag_agent.name, ag_agent.description)
self._the_ag_agent: ConversableAgent = ag_agent
self._ag2can_other_agent: AG2CAP = None
self._other_agent_name: str = the_other_name
self._init_chat: bool = init_chat
self.STATE = self.States.INIT
self._can2ag_name: str = self.actor_name + ".can2ag"
self._self_recursive: bool = self_recursive
self._network: LocalActorNetwork = None
self._connectors = {}
def connect_network(self, network: LocalActorNetwork):
"""
Connect to the AutoGen system.
"""
self._network = network
self._ag2can_other_agent = AG2CAP(self._network, self._other_agent_name)
Debug(self._can2ag_name, "connected to {network}")
def disconnect_network(self, network: LocalActorNetwork):
"""
Disconnect from the AutoGen system.
"""
super().disconnect_network(network)
# self._the_other.close()
Debug(self.actor_name, "disconnected")
def _process_txt_msg(self, msg: str, msg_type: str, topic: str, sender: str):
"""
Process a text message received from the AutoGen system.
"""
Info(self._can2ag_name, f"proc_txt_msg: [{topic}], [{msg_type}], {shorten(msg)}")
if self.STATE == self.States.INIT:
self.STATE = self.States.CONVERSING
if self._init_chat:
self._the_ag_agent.initiate_chat(self._ag2can_other_agent, message=msg, summary_method=None)
else:
self._the_ag_agent.receive(msg, self._ag2can_other_agent, True)
else:
self._the_ag_agent.receive(msg, self._ag2can_other_agent, True)
return True
def _call_agent_receive(self, receive_params: ReceiveReq):
request_reply: Optional[bool] = None
silent: Optional[bool] = False
if receive_params.HasField("request_reply"):
request_reply = receive_params.request_reply
if receive_params.HasField("silent"):
silent = receive_params.silent
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)
else:
data = receive_params.data
self._the_ag_agent.receive(data, self._ag2can_other_agent, request_reply, silent)
self._ag2can_other_agent.set_name(save_name)
def receive_msgproc(self, msg: bytes):
"""
Process a ReceiveReq message received from the AutoGen system.
"""
receive_params = ReceiveReq()
receive_params.ParseFromString(msg)
self._ag2can_other_agent.reset_receive_called()
if self.STATE == self.States.INIT:
self.STATE = self.States.CONVERSING
if self._init_chat:
self._the_ag_agent.initiate_chat(
self._ag2can_other_agent, message=receive_params.data, summary_method=None
)
else:
self._call_agent_receive(receive_params)
else:
self._call_agent_receive(receive_params)
if not self._ag2can_other_agent.was_receive_called() and self._self_recursive:
Warn(self._can2ag_name, "TERMINATE")
self._ag2can_other_agent.send_terminate(self._the_ag_agent)
return False
return True
def get_actor_connector(self, topic: str):
"""
Get the actor connector for the given topic.
"""
if topic in self._connectors:
return self._connectors[topic]
else:
connector = self._network.actor_connector_by_topic(topic)
self._connectors[topic] = connector
return connector
def generate_reply_msgproc(self, msg: GenReplyReq, sender_topic: str):
"""
Process a GenReplyReq message received from the AutoGen system and generate a reply.
"""
generate_reply_params = GenReplyReq()
generate_reply_params.ParseFromString(msg)
reply = self._the_ag_agent.generate_reply(sender=self._ag2can_other_agent)
connector = self.get_actor_connector(sender_topic)
reply_msg = GenReplyResp()
if reply:
reply_msg.data = reply.encode("utf8")
serialized_msg = reply_msg.SerializeToString()
connector.send_bin_msg(type(reply_msg).__name__, serialized_msg)
return True
def prepchat_msgproc(self, msg, sender_topic):
prep_chat = PrepChat()
prep_chat.ParseFromString(msg)
self._the_ag_agent._prepare_chat(self._ag2can_other_agent, prep_chat.clear_history, prep_chat.prepare_recipient)
return True
def _process_bin_msg(self, msg: bytes, msg_type: str, topic: str, sender: str):
"""
Process a binary message received from the AutoGen system.
"""
Info(self._can2ag_name, f"proc_bin_msg: topic=[{topic}], msg_type=[{msg_type}]")
if msg_type == ReceiveReq.__name__:
return self.receive_msgproc(msg)
elif msg_type == GenReplyReq.__name__:
return self.generate_reply_msgproc(msg, sender)
elif msg_type == PrepChat.__name__:
return self.prepchat_msgproc(msg, sender)
elif msg_type == Terminate.__name__:
Warn(self._can2ag_name, f"TERMINATE received: topic=[{topic}], msg_type=[{msg_type}]")
return False
else:
Error(self._can2ag_name, f"Unhandled message type: topic=[{topic}], msg_type=[{msg_type}]")
return True

View File

@ -0,0 +1,39 @@
from autogen import Agent, AssistantAgent, GroupChat
from autogencap.ag_adapter.AG2CAP import AG2CAP
from autogencap.ag_adapter.CAP2AG import CAP2AG
from autogencap.LocalActorNetwork import LocalActorNetwork
from typing import List
class CAPGroupChat(GroupChat):
def __init__(
self,
agents: List[AssistantAgent],
messages: List[str],
max_round: int,
chat_initiator: str,
network: LocalActorNetwork,
):
self.chat_initiator: str = chat_initiator
self._cap_network: LocalActorNetwork = network
self._cap_proxies: List[CAP2AG] = []
self._ag_proxies: List[AG2CAP] = []
self._ag_agents: List[Agent] = agents
self._init_cap_proxies()
self._init_ag_proxies()
super().__init__(agents=self._ag_proxies, messages=messages, max_round=max_round)
def _init_cap_proxies(self):
for agent in self._ag_agents:
init_chat = agent.name == self.chat_initiator
cap2ag = CAP2AG(ag_agent=agent, the_other_name="chat_manager", init_chat=init_chat, self_recursive=False)
self._cap_network.register(cap2ag)
self._cap_proxies.append(cap2ag)
def _init_ag_proxies(self):
for agent in self._ag_agents:
ag2cap = AG2CAP(self._cap_network, agent_name=agent.name, agent_description=agent.description)
self._ag_proxies.append(ag2cap)
def is_running(self) -> bool:
return all(proxy.run for proxy in self._cap_proxies)

View File

@ -0,0 +1,38 @@
from autogen import GroupChatManager
from autogencap.ActorConnector import ActorConnector
from autogencap.LocalActorNetwork import LocalActorNetwork
from autogencap.ag_adapter.CAP2AG import CAP2AG
from autogencap.ag_adapter.CAPGroupChat import CAPGroupChat
import time
class CAPGroupChatManager:
def __init__(self, groupchat: CAPGroupChat, llm_config: dict, network: LocalActorNetwork):
self._network: LocalActorNetwork = network
self._cap_group_chat: CAPGroupChat = groupchat
self._ag_group_chat_manager: GroupChatManager = GroupChatManager(
groupchat=self._cap_group_chat, llm_config=llm_config
)
self._cap_proxy: CAP2AG = CAP2AG(
ag_agent=self._ag_group_chat_manager,
the_other_name=self._cap_group_chat.chat_initiator,
init_chat=False,
self_recursive=True,
)
self._network.register(self._cap_proxy)
def initiate_chat(self, txt_msg: str) -> None:
self._network.connect()
user_proxy_conn: ActorConnector = self._network.lookup_actor(self._cap_group_chat.chat_initiator)
user_proxy_conn.send_txt_msg(txt_msg)
self._wait_for_user_exit()
def is_running(self) -> bool:
return self._cap_group_chat.is_running()
def _wait_for_user_exit(self) -> None:
try:
while self.is_running():
time.sleep(0.5)
except KeyboardInterrupt:
print("Interrupted by user, shutting down.")

View File

@ -0,0 +1,34 @@
from autogencap.ag_adapter.CAP2AG import CAP2AG
class CAPPair:
def __init__(self, network, first, second):
self._network = network
self._first_ag_agent = first
self._second_ag_agent = second
self._first_adptr = None
self._second_adptr = None
def initiate_chat(self, message: str):
self._first_adptr = CAP2AG(
ag_agent=self._first_ag_agent,
the_other_name=self._second_ag_agent.name,
init_chat=True,
self_recursive=True,
)
self._second_adptr = CAP2AG(
ag_agent=self._second_ag_agent,
the_other_name=self._first_ag_agent.name,
init_chat=False,
self_recursive=True,
)
self._network.register(self._first_adptr)
self._network.register(self._second_adptr)
self._network.connect()
# Send a message to the user_proxy
agent_connection = self._network.lookup_actor(self._first_ag_agent.name)
agent_connection.send_txt_msg(message)
def running(self):
return self._first_adptr.run and self._second_adptr.run

View File

@ -0,0 +1,35 @@
syntax = "proto3";
// Get protoc here https://github.com/protocolbuffers/protobuf/releases
// .\protoc --python_out=. --pyi_out=. Autogen.proto
message DataMap {
map<string, string> data = 1;
}
message ReceiveReq {
oneof Type {
DataMap data_map = 1;
string data = 2;
}
optional string sender = 3;
optional bool request_reply = 4;
optional bool silent = 5;
}
message Terminate {
optional string sender = 1;
}
message GenReplyReq {
}
message GenReplyResp {
optional string data = 1;
}
message PrepChat {
string recipient = 1;
bool clear_history = 2;
bool prepare_recipient = 3;
}

View File

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: Autogen.proto
# Protobuf Python Version: 4.25.2
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
b'\n\rAutogen.proto"X\n\x07\x44\x61taMap\x12 \n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x12.DataMap.DataEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"\xb0\x01\n\nReceiveReq\x12\x1c\n\x08\x64\x61ta_map\x18\x01 \x01(\x0b\x32\x08.DataMapH\x00\x12\x0e\n\x04\x64\x61ta\x18\x02 \x01(\tH\x00\x12\x13\n\x06sender\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x1a\n\rrequest_reply\x18\x04 \x01(\x08H\x02\x88\x01\x01\x12\x13\n\x06silent\x18\x05 \x01(\x08H\x03\x88\x01\x01\x42\x06\n\x04TypeB\t\n\x07_senderB\x10\n\x0e_request_replyB\t\n\x07_silent"+\n\tTerminate\x12\x13\n\x06sender\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_sender"\r\n\x0bGenReplyReq"*\n\x0cGenReplyResp\x12\x11\n\x04\x64\x61ta\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\x07\n\x05_data"O\n\x08PrepChat\x12\x11\n\trecipient\x18\x01 \x01(\t\x12\x15\n\rclear_history\x18\x02 \x01(\x08\x12\x19\n\x11prepare_recipient\x18\x03 \x01(\x08\x62\x06proto3'
)
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "Autogen_pb2", _globals)
if _descriptor._USE_C_DESCRIPTORS is False:
DESCRIPTOR._options = None
_globals["_DATAMAP_DATAENTRY"]._options = None
_globals["_DATAMAP_DATAENTRY"]._serialized_options = b"8\001"
_globals["_DATAMAP"]._serialized_start = 17
_globals["_DATAMAP"]._serialized_end = 105
_globals["_DATAMAP_DATAENTRY"]._serialized_start = 62
_globals["_DATAMAP_DATAENTRY"]._serialized_end = 105
_globals["_RECEIVEREQ"]._serialized_start = 108
_globals["_RECEIVEREQ"]._serialized_end = 284
_globals["_TERMINATE"]._serialized_start = 286
_globals["_TERMINATE"]._serialized_end = 329
_globals["_GENREPLYREQ"]._serialized_start = 331
_globals["_GENREPLYREQ"]._serialized_end = 344
_globals["_GENREPLYRESP"]._serialized_start = 346
_globals["_GENREPLYRESP"]._serialized_end = 388
_globals["_PREPCHAT"]._serialized_start = 390
_globals["_PREPCHAT"]._serialized_end = 469
# @@protoc_insertion_point(module_scope)

View File

@ -0,0 +1,69 @@
from google.protobuf.internal import containers as _containers
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class DataMap(_message.Message):
__slots__ = ("data",)
class DataEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: str
def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ...
DATA_FIELD_NUMBER: _ClassVar[int]
data: _containers.ScalarMap[str, str]
def __init__(self, data: _Optional[_Mapping[str, str]] = ...) -> None: ...
class ReceiveReq(_message.Message):
__slots__ = ("data_map", "data", "sender", "request_reply", "silent")
DATA_MAP_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
SENDER_FIELD_NUMBER: _ClassVar[int]
REQUEST_REPLY_FIELD_NUMBER: _ClassVar[int]
SILENT_FIELD_NUMBER: _ClassVar[int]
data_map: DataMap
data: str
sender: str
request_reply: bool
silent: bool
def __init__(
self,
data_map: _Optional[_Union[DataMap, _Mapping]] = ...,
data: _Optional[str] = ...,
sender: _Optional[str] = ...,
request_reply: bool = ...,
silent: bool = ...,
) -> None: ...
class Terminate(_message.Message):
__slots__ = ("sender",)
SENDER_FIELD_NUMBER: _ClassVar[int]
sender: str
def __init__(self, sender: _Optional[str] = ...) -> None: ...
class GenReplyReq(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...
class GenReplyResp(_message.Message):
__slots__ = ("data",)
DATA_FIELD_NUMBER: _ClassVar[int]
data: str
def __init__(self, data: _Optional[str] = ...) -> None: ...
class PrepChat(_message.Message):
__slots__ = ("recipient", "clear_history", "prepare_recipient")
RECIPIENT_FIELD_NUMBER: _ClassVar[int]
CLEAR_HISTORY_FIELD_NUMBER: _ClassVar[int]
PREPARE_RECIPIENT_FIELD_NUMBER: _ClassVar[int]
recipient: str
clear_history: bool
prepare_recipient: bool
def __init__(
self, recipient: _Optional[str] = ..., clear_history: bool = ..., prepare_recipient: bool = ...
) -> None: ...

View File

@ -0,0 +1,35 @@
syntax = "proto3";
// Get protoc here https://github.com/protocolbuffers/protobuf/releases
// .\protoc --python_out=. --pyi_out=. CAP.proto
message ActorInfo {
string name = 1;
optional string namespace = 2;
optional string description = 3;
}
message ActorRegistration {
ActorInfo actor_info = 1;
}
message ActorLookup {
optional ActorInfo actor_info = 1;
// TODO: May need more structure here for semantic service discovery
// optional string service_descriptor = 2;
}
message ActorInfoCollection {
repeated ActorInfo info_coll = 1;
}
message ActorLookupResponse {
bool found = 1;
optional ActorInfoCollection actor = 2;
}
message Ping {
}
message Pong {
}

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: CAP.proto
# Protobuf Python Version: 4.25.2
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
b'\n\tCAP.proto"i\n\tActorInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x16\n\tnamespace\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x0c\n\n_namespaceB\x0e\n\x0c_description"3\n\x11\x41\x63torRegistration\x12\x1e\n\nactor_info\x18\x01 \x01(\x0b\x32\n.ActorInfo"A\n\x0b\x41\x63torLookup\x12#\n\nactor_info\x18\x01 \x01(\x0b\x32\n.ActorInfoH\x00\x88\x01\x01\x42\r\n\x0b_actor_info"4\n\x13\x41\x63torInfoCollection\x12\x1d\n\tinfo_coll\x18\x01 \x03(\x0b\x32\n.ActorInfo"X\n\x13\x41\x63torLookupResponse\x12\r\n\x05\x66ound\x18\x01 \x01(\x08\x12(\n\x05\x61\x63tor\x18\x02 \x01(\x0b\x32\x14.ActorInfoCollectionH\x00\x88\x01\x01\x42\x08\n\x06_actor"\x06\n\x04Ping"\x06\n\x04Pongb\x06proto3'
)
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "CAP_pb2", _globals)
if _descriptor._USE_C_DESCRIPTORS is False:
DESCRIPTOR._options = None
_globals["_ACTORINFO"]._serialized_start = 13
_globals["_ACTORINFO"]._serialized_end = 118
_globals["_ACTORREGISTRATION"]._serialized_start = 120
_globals["_ACTORREGISTRATION"]._serialized_end = 171
_globals["_ACTORLOOKUP"]._serialized_start = 173
_globals["_ACTORLOOKUP"]._serialized_end = 238
_globals["_ACTORINFOCOLLECTION"]._serialized_start = 240
_globals["_ACTORINFOCOLLECTION"]._serialized_end = 292
_globals["_ACTORLOOKUPRESPONSE"]._serialized_start = 294
_globals["_ACTORLOOKUPRESPONSE"]._serialized_end = 382
_globals["_PING"]._serialized_start = 384
_globals["_PING"]._serialized_end = 390
_globals["_PONG"]._serialized_start = 392
_globals["_PONG"]._serialized_end = 398
# @@protoc_insertion_point(module_scope)

View File

@ -0,0 +1,58 @@
from google.protobuf.internal import containers as _containers
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import (
ClassVar as _ClassVar,
Iterable as _Iterable,
Mapping as _Mapping,
Optional as _Optional,
Union as _Union,
)
DESCRIPTOR: _descriptor.FileDescriptor
class ActorInfo(_message.Message):
__slots__ = ("name", "namespace", "description")
NAME_FIELD_NUMBER: _ClassVar[int]
NAMESPACE_FIELD_NUMBER: _ClassVar[int]
DESCRIPTION_FIELD_NUMBER: _ClassVar[int]
name: str
namespace: str
description: str
def __init__(
self, name: _Optional[str] = ..., namespace: _Optional[str] = ..., description: _Optional[str] = ...
) -> None: ...
class ActorRegistration(_message.Message):
__slots__ = ("actor_info",)
ACTOR_INFO_FIELD_NUMBER: _ClassVar[int]
actor_info: ActorInfo
def __init__(self, actor_info: _Optional[_Union[ActorInfo, _Mapping]] = ...) -> None: ...
class ActorLookup(_message.Message):
__slots__ = ("actor_info",)
ACTOR_INFO_FIELD_NUMBER: _ClassVar[int]
actor_info: ActorInfo
def __init__(self, actor_info: _Optional[_Union[ActorInfo, _Mapping]] = ...) -> None: ...
class ActorInfoCollection(_message.Message):
__slots__ = ("info_coll",)
INFO_COLL_FIELD_NUMBER: _ClassVar[int]
info_coll: _containers.RepeatedCompositeFieldContainer[ActorInfo]
def __init__(self, info_coll: _Optional[_Iterable[_Union[ActorInfo, _Mapping]]] = ...) -> None: ...
class ActorLookupResponse(_message.Message):
__slots__ = ("found", "actor")
FOUND_FIELD_NUMBER: _ClassVar[int]
ACTOR_FIELD_NUMBER: _ClassVar[int]
found: bool
actor: ActorInfoCollection
def __init__(self, found: bool = ..., actor: _Optional[_Union[ActorInfoCollection, _Mapping]] = ...) -> None: ...
class Ping(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...
class Pong(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...

View File

@ -0,0 +1 @@
.\protoc --pyi_out=. --python_out=. CAP.proto Autogen.proto

View File

@ -0,0 +1,4 @@
pyzmq
protobuf
termcolor
pyautogen

View File

@ -0,0 +1,7 @@
from setuptools import setup, find_packages
setup(
name="autogencap",
version="0.1",
packages=find_packages(),
)

View File

@ -0,0 +1,12 @@
from autogen import AssistantAgent, UserProxyAgent, config_list_from_json
def ag_demo():
config_list = config_list_from_json(env_or_file="OAI_CONFIG_LIST")
assistant = AssistantAgent("assistant", llm_config={"config_list": config_list})
user_proxy = UserProxyAgent(
"user_proxy",
code_execution_config={"work_dir": "coding"},
is_termination_msg=lambda x: "TERMINATE" in x.get("content"),
)
user_proxy.initiate_chat(assistant, message="Plot a chart of MSFT daily closing prices for last 1 Month.")

View File

@ -0,0 +1,34 @@
from autogen import AssistantAgent, GroupChat, GroupChatManager, UserProxyAgent, config_list_from_json
def ag_groupchat_demo():
config_list = config_list_from_json(env_or_file="OAI_CONFIG_LIST")
gpt4_config = {
"cache_seed": 72,
"temperature": 0,
"config_list": config_list,
"timeout": 120,
}
user_proxy = UserProxyAgent(
name="User_proxy",
system_message="A human admin.",
code_execution_config={
"last_n_messages": 2,
"work_dir": "groupchat",
"use_docker": False,
},
human_input_mode="TERMINATE",
is_termination_msg=lambda x: "TERMINATE" in x.get("content"),
)
coder = AssistantAgent(name="Coder", llm_config=gpt4_config)
pm = AssistantAgent(
name="Product_manager",
system_message="Creative in software product ideas.",
llm_config=gpt4_config,
)
groupchat = GroupChat(agents=[user_proxy, coder, pm], messages=[], max_round=12)
manager = GroupChatManager(groupchat=groupchat, llm_config=gpt4_config)
user_proxy.initiate_chat(
manager,
message="Find a latest paper about gpt-4 on arxiv and find its potential applications in software.",
)

View File

@ -0,0 +1,66 @@
"""
Demo App
"""
import argparse
import _paths
from autogencap.Config import LOG_LEVEL, IGNORED_LOG_CONTEXTS
import autogencap.DebugLog as DebugLog
from SimpleActorDemo import simple_actor_demo
from AGDemo import ag_demo
from AGGroupChatDemo import ag_groupchat_demo
from CAPAutGenGroupDemo import cap_ag_group_demo
from CAPAutoGenPairDemo import cap_ag_pair_demo
from ComplexActorDemo import complex_actor_demo
from RemoteAGDemo import remote_ag_demo
####################################################################################################
def parse_args():
# Create a parser for the command line arguments
parser = argparse.ArgumentParser(description="Demo App")
parser.add_argument("--log_level", type=int, default=1, help="Set the log level (0-3)")
# Parse the command line arguments
args = parser.parse_args()
# Set the log level
# Print log level string based on names in debug_log.py
print(f"Log level: {DebugLog.LEVEL_NAMES[args.log_level]}")
# IGNORED_LOG_CONTEXTS.extend(["BROKER"])
####################################################################################################
def main():
parse_args()
while True:
print("Select the Composable Actor Platform (CAP) demo app to run:")
print("(enter anything else to quit)")
print("1. Hello World")
print("2. Complex Agent (e.g. Name or Quit)")
print("3. AutoGen Pair")
print("4. AutoGen GroupChat")
print("5. AutoGen Agents in different processes")
choice = input("Enter your choice (1-5): ")
if choice == "1":
simple_actor_demo()
elif choice == "2":
complex_actor_demo()
# elif choice == "3":
# ag_demo()
elif choice == "3":
cap_ag_pair_demo()
# elif choice == "5":
# ag_groupchat_demo()
elif choice == "4":
cap_ag_group_demo()
elif choice == "5":
remote_ag_demo()
else:
print("Quitting...")
break
if __name__ == "__main__":
main()

View File

@ -0,0 +1,189 @@
"""
This file contains the implementation of various agents used in the application.
Each agent represents a different role and knows how to connect to external systems
to retrieve information.
"""
from autogencap.DebugLog import Debug, Info, shorten
from autogencap.LocalActorNetwork import LocalActorNetwork
from autogencap.ActorConnector import ActorConnector
from autogencap.Actor import Actor
class GreeterAgent(Actor):
"""
Prints message to screen
"""
def __init__(
self,
agent_name="Greeter",
description="This is the greeter agent, who knows how to greet people.",
):
super().__init__(agent_name, description)
class FidelityAgent(Actor):
"""
This class represents the fidelity agent, who knows how to connect to fidelity to get account,
portfolio, and order information.
Args:
agent_name (str, optional): The name of the agent. Defaults to "Fidelity".
description (str, optional): A description of the agent. Defaults to "This is the
fidelity agent who knows how to connect to fidelity to get account, portfolio, and
order information."
"""
def __init__(
self,
agent_name="Fidelity",
description=(
"This is the fidelity agent, who knows"
"how to connect to fidelity to get account, portfolio, and order information."
),
):
super().__init__(agent_name, description)
class FinancialPlannerAgent(Actor):
"""
This class represents the financial planner agent, who knows how to connect to a financial
planner and get financial planning information.
Args:
agent_name (str, optional): The name of the agent. Defaults to "Financial Planner".
description (str, optional): A description of the agent. Defaults to "This is the
financial planner agent, who knows how to connect to a financial planner and get
financial planning information."
"""
def __init__(
self,
agent_name="Financial Planner",
description=(
"This is the financial planner"
" agent, who knows how to connect to a financial planner and get financial"
" planning information."
),
):
super().__init__(agent_name, description)
class QuantAgent(Actor):
"""
This class represents the quant agent, who knows how to connect to a quant and get
quant information.
Args:
agent_name (str, optional): The name of the agent. Defaults to "Quant".
description (str, optional): A description of the agent. Defaults to "This is the
quant agent, who knows how to connect to a quant and get quant information."
"""
def __init__(
self,
agent_name="Quant",
description="This is the quant agent, who knows " "how to connect to a quant and get quant information.",
):
super().__init__(agent_name, description)
class RiskManager(Actor):
"""
This class represents a risk manager, who will analyze portfolio risk.
Args:
description (str, optional): A description of the agent. Defaults to "This is the user
interface agent, who knows how to connect to a user interface and get
user interface information."
"""
cls_agent_name = "Risk Manager"
def __init__(
self,
description=(
"This is the user interface agent, who knows how to connect"
" to a user interface and get user interface information."
),
):
super().__init__(RiskManager.cls_agent_name, description)
class PersonalAssistant(Actor):
"""
This class represents the personal assistant, who knows how to connect to the other agents and
get information from them.
Args:
agent_name (str, optional): The name of the agent. Defaults to "PersonalAssistant".
description (str, optional): A description of the agent. Defaults to "This is the personal assistant,
who knows how to connect to the other agents and get information from them."
"""
cls_agent_name = "PersonalAssistant"
def __init__(
self,
agent_name=cls_agent_name,
description="This is the personal assistant, who knows how to connect to the other agents and get information from them.",
):
super().__init__(agent_name, description)
self.fidelity: ActorConnector = None
self.financial_planner: ActorConnector = None
self.quant: ActorConnector = None
self.risk_manager: ActorConnector = None
def connect_network(self, network: LocalActorNetwork):
"""
Connects the personal assistant to the specified local actor network.
Args:
network (LocalActorNetwork): The local actor network to connect to.
"""
Debug(self.actor_name, f"is connecting to {network}")
self.fidelity = network.lookup_actor("Fidelity")
self.financial_planner = network.lookup_actor("Financial Planner")
self.quant = network.lookup_actor("Quant")
self.risk_manager = network.lookup_actor("Risk Manager")
Debug(self.actor_name, "connected")
def disconnect_network(self, network: LocalActorNetwork):
"""
Disconnects the personal assistant from the specified local actor network.
Args:
network (LocalActorNetwork): The local actor network to disconnect from.
"""
super().disconnect_network(network)
self.fidelity.close()
self.financial_planner.close()
self.quant.close()
self.risk_manager.close()
Debug(self.actor_name, "disconnected")
def _process_txt_msg(self, msg, msg_type, topic, sender):
"""
Processes a text message received by the personal assistant.
Args:
msg (str): The text message.
msg_type (str): The type of the message.
topic (str): The topic of the message.
sender (str): The sender of the message.
Returns:
bool: True if the message was processed successfully, False otherwise.
"""
if msg.strip().lower() != "quit" and msg.strip().lower() != "":
Info(self.actor_name, f"Helping user: {shorten(msg)}")
self.fidelity.send_txt_msg(f"I, {self.actor_name}, need your help to buy/sell assets for " + msg)
self.financial_planner.send_txt_msg(
f"I, {self.actor_name}, need your help in creating a financial plan for {msg}'s goals."
)
self.quant.send_txt_msg(
f"I, {self.actor_name}, need your help with quantitative analysis of the interest rate for " + msg
)
self.risk_manager.send_txt_msg(f"I, {self.actor_name}, need your help in analyzing {msg}'s portfolio risk")
return True

View File

@ -0,0 +1,40 @@
from autogen import AssistantAgent, UserProxyAgent, config_list_from_json
from autogencap.DebugLog import Info
from autogencap.LocalActorNetwork import LocalActorNetwork
from autogencap.ag_adapter.CAPGroupChat import CAPGroupChat
from autogencap.ag_adapter.CAPGroupChatManager import CAPGroupChatManager
def cap_ag_group_demo():
config_list = config_list_from_json(env_or_file="OAI_CONFIG_LIST")
gpt4_config = {
"cache_seed": 73,
"temperature": 0,
"config_list": config_list,
"timeout": 120,
}
user_proxy = UserProxyAgent(
name="User_proxy",
system_message="A human admin.",
is_termination_msg=lambda x: "TERMINATE" in x.get("content"),
code_execution_config={
"last_n_messages": 2,
"work_dir": "groupchat",
"use_docker": False,
},
human_input_mode="TERMINATE",
)
coder = AssistantAgent(name="Coder", llm_config=gpt4_config)
pm = AssistantAgent(
name="Product_manager",
system_message="Creative in software product ideas.",
llm_config=gpt4_config,
)
network = LocalActorNetwork()
cap_groupchat = CAPGroupChat(
agents=[user_proxy, coder, pm], messages=[], max_round=12, network=network, chat_initiator=user_proxy.name
)
manager = CAPGroupChatManager(groupchat=cap_groupchat, llm_config=gpt4_config, network=network)
manager.initiate_chat("Find a latest paper about gpt-4 on arxiv and find its potential applications in software.")
network.disconnect()
Info("App", "App Exit")

View File

@ -0,0 +1,31 @@
import time
from autogen import AssistantAgent, UserProxyAgent, config_list_from_json
from autogencap.DebugLog import Info
from autogencap.ag_adapter.CAPPair import CAPPair
from autogencap.LocalActorNetwork import LocalActorNetwork
def cap_ag_pair_demo():
config_list = config_list_from_json(env_or_file="OAI_CONFIG_LIST")
assistant = AssistantAgent("assistant", llm_config={"config_list": config_list})
user_proxy = UserProxyAgent(
"user_proxy",
code_execution_config={"work_dir": "coding"},
is_termination_msg=lambda x: "TERMINATE" in x.get("content"),
)
# Composable Agent Platform AutoGen Pair adapter
network = LocalActorNetwork()
pair = CAPPair(network, user_proxy, assistant)
pair.initiate_chat("Plot a chart of MSFT daily closing prices for last 1 Month.")
# Wait for the pair to finish
try:
while pair.running():
time.sleep(0.5)
except KeyboardInterrupt:
print("Interrupted by user, shutting down.")
network.disconnect()
Info("App", "App Exit")

View File

@ -0,0 +1,50 @@
import time
from termcolor import colored
from autogencap.LocalActorNetwork import LocalActorNetwork
from AppAgents import FidelityAgent, FinancialPlannerAgent, PersonalAssistant, QuantAgent, RiskManager
def complex_actor_demo():
"""
This function demonstrates the usage of a complex actor system.
It creates a local actor graph, registers various agents,
connects them, and interacts with a personal assistant agent.
The function continuously prompts the user for input messages,
sends them to the personal assistant agent, and terminates
when the user enters "quit".
"""
network = LocalActorNetwork()
# Register agents
network.register(PersonalAssistant())
network.register(FidelityAgent())
network.register(FinancialPlannerAgent())
network.register(RiskManager())
network.register(QuantAgent())
# Tell agents to connect to other agents
network.connect()
# Get a channel to the personal assistant agent
pa = network.lookup_actor(PersonalAssistant.cls_agent_name)
info_msg = """
This is an imaginary personal assistant agent scenario.
Five actors are connected in a self-determined graph. The user
can interact with the personal assistant agent by entering
their name. The personal assistant agent will then enlist
the other four agents to create a financial plan.
Start by entering your name.
"""
print(colored(info_msg, "blue"))
while True:
# For aesthetic reasons, better to let network messages complete
time.sleep(0.1)
# Get a message from the user
msg = input(colored("Enter your name (or quit): ", "light_red"))
# Send the message to the personal assistant agent
pa.send_txt_msg(msg)
if msg.lower() == "quit":
break
# Cleanup
pa.close()
network.disconnect()

View File

@ -0,0 +1,18 @@
# Start Broker & Assistant
# Start UserProxy - Let it run
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.
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)
"""
print(instructions)
input("Press Enter to return to demo menu...")
pass

View File

@ -0,0 +1,26 @@
import time
from AppAgents import GreeterAgent
from autogencap.LocalActorNetwork import LocalActorNetwork
def simple_actor_demo():
"""
Demonstrates the usage of the CAP platform by registering an actor, connecting to the actor,
sending a message, and performing cleanup operations.
"""
# CAP Platform
network = LocalActorNetwork()
# Register an actor
network.register(GreeterAgent())
# Tell actor to connect to other actors
network.connect()
# Get a channel to the actor
greeter_link = network.lookup_actor("Greeter")
time.sleep(1)
# Send a message to the actor
greeter_link.send_txt_msg("Hello World!")
time.sleep(1)
# Cleanup
greeter_link.close()
network.disconnect()

View File

@ -0,0 +1,7 @@
# Add autogencap to system path in case autogencap is not pip installed
# Since this library has not been published to PyPi, it is not easy to install using pip
import sys
import os
absparent = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.append(absparent)

View File

@ -0,0 +1,40 @@
import time
import _paths
from autogen import AssistantAgent, config_list_from_json
from autogencap.DebugLog import Info
from autogencap.LocalActorNetwork import LocalActorNetwork
from autogencap.ag_adapter.CAP2AG import CAP2AG
# Starts the Broker and the Assistant. The UserProxy is started separately.
class StandaloneAssistant:
def __init__(self):
pass
def run(self):
print("Running the StandaloneAssistant")
config_list = config_list_from_json(env_or_file="OAI_CONFIG_LIST")
assistant = AssistantAgent("assistant", llm_config={"config_list": config_list})
# Composable Agent Network adapter
network = LocalActorNetwork()
assistant_adptr = CAP2AG(ag_agent=assistant, the_other_name="user_proxy", init_chat=False, self_recursive=True)
network.register(assistant_adptr)
network.connect()
# Hang around for a while
try:
while assistant_adptr.run:
time.sleep(0.5)
except KeyboardInterrupt:
print("Interrupted by user, shutting down.")
network.disconnect()
Info("StandaloneAssistant", "App Exit")
def main():
assistant = StandaloneAssistant()
assistant.run()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,5 @@
import _paths
from autogencap.Broker import main
if __name__ == "__main__":
main()

View File

@ -0,0 +1,5 @@
import _paths
from autogencap.DirectorySvc import main
if __name__ == "__main__":
main()

View File

@ -0,0 +1,56 @@
import time
import _paths
from autogen import UserProxyAgent, config_list_from_json
from autogencap.DebugLog import Info
from autogencap.LocalActorNetwork import LocalActorNetwork
from autogencap.ag_adapter.CAP2AG import CAP2AG
from autogencap.Config import IGNORED_LOG_CONTEXTS
# Starts the Broker and the Assistant. The UserProxy is started separately.
class StandaloneUserProxy:
def __init__(self):
pass
def run(self):
print("Running the StandaloneUserProxy")
user_proxy = UserProxyAgent(
"user_proxy",
code_execution_config={"work_dir": "coding"},
is_termination_msg=lambda x: "TERMINATE" in x.get("content"),
)
# Composable Agent Network adapter
network = LocalActorNetwork()
user_proxy_adptr = CAP2AG(ag_agent=user_proxy, the_other_name="assistant", init_chat=True, self_recursive=True)
network.register(user_proxy_adptr)
network.connect()
# Send a message to the user_proxy
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 user_proxy_adptr.run:
time.sleep(0.5)
except KeyboardInterrupt:
print("Interrupted by user, shutting down.")
network.disconnect()
Info("StandaloneUserProxy", "App Exit")
def main():
IGNORED_LOG_CONTEXTS.extend(["BROKER", "DirectorySvc"])
assistant = StandaloneUserProxy()
assistant.run()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,7 @@
# Add autogencap to system path in case autogencap is not pip installed
# Since this library has not been published to PyPi, it is not easy to install using pip
import sys
import os
absparent = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
sys.path.append(absparent)