diff --git a/samples/apps/cap/README.md b/samples/apps/cap/README.md new file mode 100644 index 00000000000..db93821203e --- /dev/null +++ b/samples/apps/cap/README.md @@ -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 diff --git a/samples/apps/cap/TODO.md b/samples/apps/cap/TODO.md new file mode 100644 index 00000000000..df37f86bb11 --- /dev/null +++ b/samples/apps/cap/TODO.md @@ -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? diff --git a/samples/apps/cap/c#/Readme.md b/samples/apps/cap/c#/Readme.md new file mode 100644 index 00000000000..684e27f7d7b --- /dev/null +++ b/samples/apps/cap/c#/Readme.md @@ -0,0 +1 @@ +Coming soon... diff --git a/samples/apps/cap/c++/Readme.md b/samples/apps/cap/c++/Readme.md new file mode 100644 index 00000000000..684e27f7d7b --- /dev/null +++ b/samples/apps/cap/c++/Readme.md @@ -0,0 +1 @@ +Coming soon... diff --git a/samples/apps/cap/node/Readme.md b/samples/apps/cap/node/Readme.md new file mode 100644 index 00000000000..684e27f7d7b --- /dev/null +++ b/samples/apps/cap/node/Readme.md @@ -0,0 +1 @@ +Coming soon... diff --git a/samples/apps/cap/py/autogencap/Actor.py b/samples/apps/cap/py/autogencap/Actor.py new file mode 100644 index 00000000000..64c56bfcd3c --- /dev/null +++ b/samples/apps/cap/py/autogencap/Actor.py @@ -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() diff --git a/samples/apps/cap/py/autogencap/ActorConnector.py b/samples/apps/cap/py/autogencap/ActorConnector.py new file mode 100644 index 00000000000..f48f9f8e84b --- /dev/null +++ b/samples/apps/cap/py/autogencap/ActorConnector.py @@ -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() diff --git a/samples/apps/cap/py/autogencap/Broker.py b/samples/apps/cap/py/autogencap/Broker.py new file mode 100644 index 00000000000..0bb844d2a04 --- /dev/null +++ b/samples/apps/cap/py/autogencap/Broker.py @@ -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() diff --git a/samples/apps/cap/py/autogencap/Config.py b/samples/apps/cap/py/autogencap/Config.py new file mode 100644 index 00000000000..299e4e273b8 --- /dev/null +++ b/samples/apps/cap/py/autogencap/Config.py @@ -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" diff --git a/samples/apps/cap/py/autogencap/Constants.py b/samples/apps/cap/py/autogencap/Constants.py new file mode 100644 index 00000000000..8326d6753d3 --- /dev/null +++ b/samples/apps/cap/py/autogencap/Constants.py @@ -0,0 +1,2 @@ +Termination_Topic: str = "Termination" +Directory_Svc_Topic: str = "Directory_Svc" diff --git a/samples/apps/cap/py/autogencap/DebugLog.py b/samples/apps/cap/py/autogencap/DebugLog.py new file mode 100644 index 00000000000..b31512f7ffa --- /dev/null +++ b/samples/apps/cap/py/autogencap/DebugLog.py @@ -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) diff --git a/samples/apps/cap/py/autogencap/DirectorySvc.py b/samples/apps/cap/py/autogencap/DirectorySvc.py new file mode 100644 index 00000000000..21d01c71207 --- /dev/null +++ b/samples/apps/cap/py/autogencap/DirectorySvc.py @@ -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() diff --git a/samples/apps/cap/py/autogencap/LocalActorNetwork.py b/samples/apps/cap/py/autogencap/LocalActorNetwork.py new file mode 100644 index 00000000000..d1133bdb7e7 --- /dev/null +++ b/samples/apps/cap/py/autogencap/LocalActorNetwork.py @@ -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) diff --git a/samples/apps/cap/py/autogencap/__init__.py b/samples/apps/cap/py/autogencap/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/apps/cap/py/autogencap/ag_adapter/AG2CAP.py b/samples/apps/cap/py/autogencap/ag_adapter/AG2CAP.py new file mode 100644 index 00000000000..6dfd6c51821 --- /dev/null +++ b/samples/apps/cap/py/autogencap/ag_adapter/AG2CAP.py @@ -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) diff --git a/samples/apps/cap/py/autogencap/ag_adapter/AGActor.py b/samples/apps/cap/py/autogencap/ag_adapter/AGActor.py new file mode 100644 index 00000000000..f53d9c0a07e --- /dev/null +++ b/samples/apps/cap/py/autogencap/ag_adapter/AGActor.py @@ -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}") diff --git a/samples/apps/cap/py/autogencap/ag_adapter/AutoGenConnector.py b/samples/apps/cap/py/autogencap/ag_adapter/AutoGenConnector.py new file mode 100644 index 00000000000..d126b067c0a --- /dev/null +++ b/samples/apps/cap/py/autogencap/ag_adapter/AutoGenConnector.py @@ -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) diff --git a/samples/apps/cap/py/autogencap/ag_adapter/CAP2AG.py b/samples/apps/cap/py/autogencap/ag_adapter/CAP2AG.py new file mode 100644 index 00000000000..f21da46ddcf --- /dev/null +++ b/samples/apps/cap/py/autogencap/ag_adapter/CAP2AG.py @@ -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 diff --git a/samples/apps/cap/py/autogencap/ag_adapter/CAPGroupChat.py b/samples/apps/cap/py/autogencap/ag_adapter/CAPGroupChat.py new file mode 100644 index 00000000000..1e8b2ec5f12 --- /dev/null +++ b/samples/apps/cap/py/autogencap/ag_adapter/CAPGroupChat.py @@ -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) diff --git a/samples/apps/cap/py/autogencap/ag_adapter/CAPGroupChatManager.py b/samples/apps/cap/py/autogencap/ag_adapter/CAPGroupChatManager.py new file mode 100644 index 00000000000..fd858cd3f38 --- /dev/null +++ b/samples/apps/cap/py/autogencap/ag_adapter/CAPGroupChatManager.py @@ -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.") diff --git a/samples/apps/cap/py/autogencap/ag_adapter/CAPPair.py b/samples/apps/cap/py/autogencap/ag_adapter/CAPPair.py new file mode 100644 index 00000000000..7cdeb12603f --- /dev/null +++ b/samples/apps/cap/py/autogencap/ag_adapter/CAPPair.py @@ -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 diff --git a/samples/apps/cap/py/autogencap/ag_adapter/__init__.py b/samples/apps/cap/py/autogencap/ag_adapter/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/apps/cap/py/autogencap/proto/Autogen.proto b/samples/apps/cap/py/autogencap/proto/Autogen.proto new file mode 100644 index 00000000000..eda8b6cf169 --- /dev/null +++ b/samples/apps/cap/py/autogencap/proto/Autogen.proto @@ -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 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; +} diff --git a/samples/apps/cap/py/autogencap/proto/Autogen_pb2.py b/samples/apps/cap/py/autogencap/proto/Autogen_pb2.py new file mode 100644 index 00000000000..400886a089d --- /dev/null +++ b/samples/apps/cap/py/autogencap/proto/Autogen_pb2.py @@ -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) diff --git a/samples/apps/cap/py/autogencap/proto/Autogen_pb2.pyi b/samples/apps/cap/py/autogencap/proto/Autogen_pb2.pyi new file mode 100644 index 00000000000..e73da3735fd --- /dev/null +++ b/samples/apps/cap/py/autogencap/proto/Autogen_pb2.pyi @@ -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: ... diff --git a/samples/apps/cap/py/autogencap/proto/CAP.proto b/samples/apps/cap/py/autogencap/proto/CAP.proto new file mode 100644 index 00000000000..ee857a2f24c --- /dev/null +++ b/samples/apps/cap/py/autogencap/proto/CAP.proto @@ -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 { +} diff --git a/samples/apps/cap/py/autogencap/proto/CAP_pb2.py b/samples/apps/cap/py/autogencap/proto/CAP_pb2.py new file mode 100644 index 00000000000..35fd1f28059 --- /dev/null +++ b/samples/apps/cap/py/autogencap/proto/CAP_pb2.py @@ -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) diff --git a/samples/apps/cap/py/autogencap/proto/CAP_pb2.pyi b/samples/apps/cap/py/autogencap/proto/CAP_pb2.pyi new file mode 100644 index 00000000000..77f5e797458 --- /dev/null +++ b/samples/apps/cap/py/autogencap/proto/CAP_pb2.pyi @@ -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: ... diff --git a/samples/apps/cap/py/autogencap/proto/__init__.py b/samples/apps/cap/py/autogencap/proto/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/apps/cap/py/autogencap/proto/proto-instructions.txt b/samples/apps/cap/py/autogencap/proto/proto-instructions.txt new file mode 100644 index 00000000000..7327a6e961f --- /dev/null +++ b/samples/apps/cap/py/autogencap/proto/proto-instructions.txt @@ -0,0 +1 @@ +.\protoc --pyi_out=. --python_out=. CAP.proto Autogen.proto diff --git a/samples/apps/cap/py/autogencap/requirements.txt b/samples/apps/cap/py/autogencap/requirements.txt new file mode 100644 index 00000000000..1b5ac10319f --- /dev/null +++ b/samples/apps/cap/py/autogencap/requirements.txt @@ -0,0 +1,4 @@ +pyzmq +protobuf +termcolor +pyautogen diff --git a/samples/apps/cap/py/autogencap/setup.py b/samples/apps/cap/py/autogencap/setup.py new file mode 100644 index 00000000000..0e239864e62 --- /dev/null +++ b/samples/apps/cap/py/autogencap/setup.py @@ -0,0 +1,7 @@ +from setuptools import setup, find_packages + +setup( + name="autogencap", + version="0.1", + packages=find_packages(), +) diff --git a/samples/apps/cap/py/demo/AGDemo.py b/samples/apps/cap/py/demo/AGDemo.py new file mode 100644 index 00000000000..b5f27f19e79 --- /dev/null +++ b/samples/apps/cap/py/demo/AGDemo.py @@ -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.") diff --git a/samples/apps/cap/py/demo/AGGroupChatDemo.py b/samples/apps/cap/py/demo/AGGroupChatDemo.py new file mode 100644 index 00000000000..de7006e991a --- /dev/null +++ b/samples/apps/cap/py/demo/AGGroupChatDemo.py @@ -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.", + ) diff --git a/samples/apps/cap/py/demo/App.py b/samples/apps/cap/py/demo/App.py new file mode 100644 index 00000000000..45dbeb224d7 --- /dev/null +++ b/samples/apps/cap/py/demo/App.py @@ -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() diff --git a/samples/apps/cap/py/demo/AppAgents.py b/samples/apps/cap/py/demo/AppAgents.py new file mode 100644 index 00000000000..3109f74a46f --- /dev/null +++ b/samples/apps/cap/py/demo/AppAgents.py @@ -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 diff --git a/samples/apps/cap/py/demo/CAPAutGenGroupDemo.py b/samples/apps/cap/py/demo/CAPAutGenGroupDemo.py new file mode 100644 index 00000000000..d3f650eb021 --- /dev/null +++ b/samples/apps/cap/py/demo/CAPAutGenGroupDemo.py @@ -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") diff --git a/samples/apps/cap/py/demo/CAPAutoGenPairDemo.py b/samples/apps/cap/py/demo/CAPAutoGenPairDemo.py new file mode 100644 index 00000000000..b20e9403838 --- /dev/null +++ b/samples/apps/cap/py/demo/CAPAutoGenPairDemo.py @@ -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") diff --git a/samples/apps/cap/py/demo/ComplexActorDemo.py b/samples/apps/cap/py/demo/ComplexActorDemo.py new file mode 100644 index 00000000000..6236682985b --- /dev/null +++ b/samples/apps/cap/py/demo/ComplexActorDemo.py @@ -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() diff --git a/samples/apps/cap/py/demo/RemoteAGDemo.py b/samples/apps/cap/py/demo/RemoteAGDemo.py new file mode 100644 index 00000000000..0c2a946c0a4 --- /dev/null +++ b/samples/apps/cap/py/demo/RemoteAGDemo.py @@ -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 diff --git a/samples/apps/cap/py/demo/SimpleActorDemo.py b/samples/apps/cap/py/demo/SimpleActorDemo.py new file mode 100644 index 00000000000..26a67814d31 --- /dev/null +++ b/samples/apps/cap/py/demo/SimpleActorDemo.py @@ -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() diff --git a/samples/apps/cap/py/demo/_paths.py b/samples/apps/cap/py/demo/_paths.py new file mode 100644 index 00000000000..82e5877f06f --- /dev/null +++ b/samples/apps/cap/py/demo/_paths.py @@ -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) diff --git a/samples/apps/cap/py/demo/standalone/Assistant.py b/samples/apps/cap/py/demo/standalone/Assistant.py new file mode 100644 index 00000000000..81e9580eba6 --- /dev/null +++ b/samples/apps/cap/py/demo/standalone/Assistant.py @@ -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() diff --git a/samples/apps/cap/py/demo/standalone/Broker.py b/samples/apps/cap/py/demo/standalone/Broker.py new file mode 100644 index 00000000000..f61064eb8e5 --- /dev/null +++ b/samples/apps/cap/py/demo/standalone/Broker.py @@ -0,0 +1,5 @@ +import _paths +from autogencap.Broker import main + +if __name__ == "__main__": + main() diff --git a/samples/apps/cap/py/demo/standalone/DirectorySvc.py b/samples/apps/cap/py/demo/standalone/DirectorySvc.py new file mode 100644 index 00000000000..3320b96e573 --- /dev/null +++ b/samples/apps/cap/py/demo/standalone/DirectorySvc.py @@ -0,0 +1,5 @@ +import _paths +from autogencap.DirectorySvc import main + +if __name__ == "__main__": + main() diff --git a/samples/apps/cap/py/demo/standalone/UserProxy.py b/samples/apps/cap/py/demo/standalone/UserProxy.py new file mode 100644 index 00000000000..6b70ae7304a --- /dev/null +++ b/samples/apps/cap/py/demo/standalone/UserProxy.py @@ -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() diff --git a/samples/apps/cap/py/demo/standalone/_paths.py b/samples/apps/cap/py/demo/standalone/_paths.py new file mode 100644 index 00000000000..7b4559f82c7 --- /dev/null +++ b/samples/apps/cap/py/demo/standalone/_paths.py @@ -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)