[CAP] Abstraction of actor_connector to go along with runtime factory and runtime abstraction (#3296)

* Added Runtime Factory to support multiple implementations

* Rename to ComponentEnsemble to ZMQRuntime

* rename zmq_runtime

* rename zmq_runtime

* pre-commit fixes

* pre-commit fix

* pre-commit fixes and default runtime

* pre-commit fixes

* Rename constants

* Rename Constants

* Create interfaces for connectors

* pre-commit fixes

* pre-commit fixes

* pre-commit fixes

* lower case file names

* rename files to lower _case

* rename files to _lowercase

* removed _

* Refactored to make Actor zmq agnostic

* fix for refactor

* fix refactor, circular dependency

* pre-commit fixes

* document classes

* pre-commit ruff

* fix ruff issues

* ruff fixes

* ruff fixes

* actor connector documentation

* better docs

---------

Co-authored-by: Li Jiang <bnujli@gmail.com>
Co-authored-by: Chi Wang <wang.chi@microsoft.com>
Co-authored-by: Ryan Sweet <rysweet@microsoft.com>
Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
This commit is contained in:
Rajan 2024-10-22 04:52:06 -04:00 committed by GitHub
parent 8a2a40d5d2
commit 1c5baf020f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 402 additions and 264 deletions

View File

@ -5,3 +5,4 @@ xpub_url: str = "tcp://127.0.0.1:5555"
xsub_url: str = "tcp://127.0.0.1:5556"
router_url: str = "tcp://127.0.0.1:5557"
dealer_url: str = "tcp://127.0.0.1:5558"
USE_COLOR_LOGGING = True

View File

@ -1,57 +1,38 @@
import threading
import traceback
import zmq
from .Config import xpub_url
from .DebugLog import Debug, Error, Info
from .actor_runtime import IMessageReceiver, IMsgActor, IRuntime
from .debug_log import Debug, Info
class Actor:
class Actor(IMsgActor):
def __init__(self, agent_name: str, description: str, start_thread: bool = True):
"""Initialize the Actor with a name, description, and threading option."""
self.actor_name: str = agent_name
self.agent_description: str = description
self.run = False
self._start_event = threading.Event()
self._start_thread = start_thread
self._msg_receiver: IMessageReceiver = None
self._runtime: IRuntime = None
def on_connect(self, network):
Debug(self.actor_name, f"is connecting to {network}")
def on_connect(self):
"""Connect the actor to the runtime."""
Debug(self.actor_name, f"is connecting to {self._runtime}")
Debug(self.actor_name, "connected")
def on_txt_msg(self, msg: str, msg_type: str, receiver: str, sender: str) -> bool:
"""Handle incoming text messages."""
Info(self.actor_name, f"InBox: {msg}")
return True
def on_bin_msg(self, msg: bytes, msg_type: str, receiver: str, sender: str) -> bool:
"""Handle incoming binary messages."""
Info(self.actor_name, f"Msg: receiver=[{receiver}], msg_type=[{msg_type}]")
return True
def _msg_loop_init(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}")
self._start_event.set()
def get_message(self):
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:
return None # No message received, continue to next iteration
except Exception as e:
Error(self.actor_name, f"recv thread encountered an error: {e}")
traceback.print_exc()
return None
return topic, msg_type, sender_topic, msg
def dispatch_message(self, message):
"""Dispatch the received message based on its type."""
if message is None:
return
topic, msg_type, sender_topic, msg = message
@ -65,40 +46,50 @@ class Actor:
if not self.on_bin_msg(msg, msg_type, topic, sender_topic):
self.run = False
def get_message(self):
"""Retrieve a message from the runtime implementation."""
return self._msg_receiver.get_message()
def _msg_loop(self):
"""Main message loop for receiving and dispatching messages."""
try:
self._msg_loop_init()
self._msg_receiver = self._runtime.get_new_msg_receiver()
self._msg_receiver.init(self.actor_name)
self._start_event.set()
while self.run:
message = self.get_message()
message = self._msg_receiver.get_message()
self.dispatch_message(message)
except Exception as e:
Debug(self.actor_name, f"recv thread encountered an error: {e}")
traceback.print_exc()
finally:
self.run = False
# In case there was an exception at startup signal
# the main thread.
self._start_event.set()
self.run = False
Debug(self.actor_name, "recv thread ended")
def on_start(self, context: zmq.Context):
self._context = context
self.run: bool = True
def on_start(self, runtime: IRuntime):
"""Start the actor and its message receiving thread if applicable."""
self._runtime = runtime # Save the runtime
self.run = True
if self._start_thread:
self._thread = threading.Thread(target=self._msg_loop)
self._thread.start()
self._start_event.wait()
else:
self._msg_loop_init()
self._msg_receiver = self._runtime.get_new_msg_receiver()
self._msg_receiver.init(self.actor_name)
def disconnect_network(self, network):
"""Disconnect the actor from the network."""
Debug(self.actor_name, f"is disconnecting from {network}")
Debug(self.actor_name, "disconnected")
self.stop()
def stop(self):
"""Stop the actor and its message receiver."""
self.run = False
if self._start_thread:
self._thread.join()
self._socket.setsockopt(zmq.LINGER, 0)
self._socket.close()
self._msg_receiver.stop()

View File

@ -0,0 +1,83 @@
from abc import ABC, abstractmethod
from typing import Any, Optional, Tuple
class IActorConnector(ABC):
"""
Abstract base class for actor connectors. Each runtime will have a different implementation.
Obtain an instance of the correct connector from the runtime by calling the runtime's find_by_xyz
method.
"""
@abstractmethod
def send_txt_msg(self, msg: str) -> None:
"""
Send a text message to the actor.
Args:
msg (str): The text message to send.
"""
pass
@abstractmethod
def send_bin_msg(self, msg_type: str, msg: bytes) -> None:
"""
Send a binary message to the actor.
Args:
msg_type (str): The type of the binary message.
msg (bytes): The binary message to send.
"""
pass
@abstractmethod
def send_proto_msg(self, msg: Any) -> None:
"""
Send a protocol buffer message to the actor.
Args:
msg (Any): The protocol buffer message to send.
"""
pass
@abstractmethod
def send_recv_proto_msg(
self, msg: Any, num_attempts: int = 5
) -> Tuple[Optional[str], Optional[str], Optional[bytes]]:
"""
Send a protocol buffer message and receive a response from the actor.
Args:
msg (Any): The protocol buffer message to send.
num_attempts (int, optional): Number of attempts to send and receive. Defaults to 5.
Returns:
Tuple[Optional[str], Optional[str], Optional[bytes]]: A tuple containing the topic,
message type, and response message, or None if no response is received.
"""
pass
@abstractmethod
def send_recv_msg(
self, msg_type: str, msg: bytes, num_attempts: int = 5
) -> Tuple[Optional[str], Optional[str], Optional[bytes]]:
"""
Send a binary message and receive a response from the actor.
Args:
msg_type (str): The type of the binary message.
msg (bytes): The binary message to send.
num_attempts (int, optional): Number of attempts to send and receive. Defaults to 5.
Returns:
Tuple[Optional[str], Optional[str], Optional[bytes]]: A tuple containing the topic,
message type, and response message, or None if no response is received.
"""
pass
@abstractmethod
def close(self) -> None:
"""
Close the actor connector and release any resources.
"""
pass

View File

@ -1,36 +1,108 @@
from abc import ABC, abstractmethod
from typing import List
from .Actor import Actor
from .ActorConnector import ActorConnector
from .actor_connector import IActorConnector
from .proto.CAP_pb2 import ActorInfo
class IRuntime(ABC):
class IMsgActor(ABC):
"""Abstract base class for message based actors."""
@abstractmethod
def register(self, actor: Actor):
def on_connect(self, runtime: "IRuntime"):
"""Called when the actor connects to the runtime."""
pass
@abstractmethod
def on_txt_msg(self, msg: str, msg_type: str, receiver: str, sender: str) -> bool:
"""Handle incoming text messages."""
pass
@abstractmethod
def on_bin_msg(self, msg: bytes, msg_type: str, receiver: str, sender: str) -> bool:
"""Handle incoming binary messages."""
pass
@abstractmethod
def on_start(self):
"""Called when the actor starts."""
pass
@abstractmethod
def stop(self):
"""Stop the actor."""
pass
@abstractmethod
def dispatch_message(self, message):
"""Dispatch the received message based on its type."""
pass
class IMessageReceiver(ABC):
"""Abstract base class for message receivers. Implementations are runtime specific."""
@abstractmethod
def init(self, actor_name: str):
"""Initialize the message receiver."""
pass
@abstractmethod
def add_listener(self, topic: str):
"""Add a topic to the message receiver."""
pass
@abstractmethod
def get_message(self):
"""Retrieve a message from the runtime implementation."""
pass
@abstractmethod
def stop(self):
"""Stop the message receiver."""
pass
# Abstract base class for the runtime environment
class IRuntime(ABC):
"""Abstract base class for the actor runtime environment."""
@abstractmethod
def register(self, actor: IMsgActor):
"""Register an actor with the runtime."""
pass
@abstractmethod
def get_new_msg_receiver(self) -> IMessageReceiver:
"""Create and return a new message receiver."""
pass
@abstractmethod
def connect(self):
"""Connect the runtime to the messaging system."""
pass
@abstractmethod
def disconnect(self):
"""Disconnect the runtime from the messaging system."""
pass
@abstractmethod
def find_by_topic(self, topic: str) -> ActorConnector:
def find_by_topic(self, topic: str) -> IActorConnector:
"""Find an actor connector by topic."""
pass
@abstractmethod
def find_by_name(self, name: str) -> ActorConnector:
def find_by_name(self, name: str) -> IActorConnector:
"""Find an actor connector by name."""
pass
@abstractmethod
def find_termination(self) -> ActorConnector:
def find_termination(self) -> IActorConnector:
"""Find the termination actor connector."""
pass
@abstractmethod
def find_by_name_regex(self, name_regex) -> List[ActorInfo]:
def find_by_name_regex(self, name_regex) -> List["ActorInfo"]:
"""Find actors by name using a regular expression."""
pass

View File

@ -1,13 +0,0 @@
import zmq
from autogencap.Actor import Actor
from autogencap.constants import Termination_Topic
from autogencap.DebugLog import Debug
class AGActor(Actor):
def on_start(self, context: zmq.Context):
super().on_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,11 @@
from autogencap.actor import Actor
from autogencap.constants import Termination_Topic
from autogencap.debug_log import Debug
class AGActor(Actor):
def on_start(self, runtime):
super().on_start(runtime)
str_topic = Termination_Topic
self._msg_receiver.add_listener(str_topic)
Debug(self.actor_name, f"subscribe to: {str_topic}")

View File

@ -4,7 +4,7 @@ from typing import Callable, Dict, List, Optional, Union
from autogen import Agent, ConversableAgent
from ..actor_runtime import IRuntime
from .AutoGenConnector import AutoGenConnector
from .autogen_connector import AutoGenConnector
class AG2CAP(ConversableAgent):

View File

@ -2,8 +2,8 @@ import time
from autogen import ConversableAgent
from ..DebugLog import Info, Warn
from .CAP2AG import CAP2AG
from ..debug_log import Info, Warn
from .cap_to_ag import CAP2AG
class Agent:

View File

@ -3,7 +3,7 @@ from typing import Dict, Optional, Union
from autogen import Agent
from ..ActorConnector import ActorConnector
from ..actor_connector import IActorConnector
from ..proto.Autogen_pb2 import GenReplyReq, GenReplyResp, PrepChat, ReceiveReq, Terminate
@ -13,8 +13,8 @@ class AutoGenConnector:
to/from the CAP system.
"""
def __init__(self, cap_sender: ActorConnector):
self._can_channel: ActorConnector = cap_sender
def __init__(self, cap_sender: IActorConnector):
self._can_channel: IActorConnector = cap_sender
def close(self):
"""

View File

@ -1,8 +1,8 @@
from typing import List
from autogen import Agent, AssistantAgent, GroupChat
from autogencap.ag_adapter.AG2CAP import AG2CAP
from autogencap.ag_adapter.CAP2AG import CAP2AG
from autogencap.ag_adapter.ag_to_cap import AG2CAP
from autogencap.ag_adapter.cap_to_ag import CAP2AG
from ..actor_runtime import IRuntime

View File

@ -1,16 +1,16 @@
import time
from autogen import GroupChatManager
from autogencap.ActorConnector import ActorConnector
from autogencap.ag_adapter.CAP2AG import CAP2AG
from autogencap.ag_adapter.CAPGroupChat import CAPGroupChat
from autogencap.actor_connector import IActorConnector
from autogencap.ag_adapter.cap_group_chat import CAPGroupChat
from autogencap.ag_adapter.cap_to_ag import CAP2AG
from ..actor_runtime import IRuntime
class CAPGroupChatManager:
def __init__(self, groupchat: CAPGroupChat, llm_config: dict, network: IRuntime):
self._ensemble: IRuntime = network
def __init__(self, groupchat: CAPGroupChat, llm_config: dict, runtime: IRuntime):
self._runtime: IRuntime = runtime
self._cap_group_chat: CAPGroupChat = groupchat
self._ag_group_chat_manager: GroupChatManager = GroupChatManager(
groupchat=self._cap_group_chat, llm_config=llm_config
@ -21,11 +21,11 @@ class CAPGroupChatManager:
init_chat=False,
self_recursive=True,
)
self._ensemble.register(self._cap_proxy)
self._runtime.register(self._cap_proxy)
def initiate_chat(self, txt_msg: str) -> None:
self._ensemble.connect()
user_proxy_conn: ActorConnector = self._ensemble.find_by_name(self._cap_group_chat.chat_initiator)
self._runtime.connect()
user_proxy_conn: IActorConnector = self._runtime.find_by_name(self._cap_group_chat.chat_initiator)
user_proxy_conn.send_txt_msg(txt_msg)
self._wait_for_user_exit()

View File

@ -1,4 +1,4 @@
from autogencap.ag_adapter.CAP2AG import CAP2AG
from autogencap.ag_adapter.cap_to_ag import CAP2AG
class CAPPair:

View File

@ -5,10 +5,10 @@ from typing import Optional
from autogen import ConversableAgent
from ..actor_runtime import IRuntime
from ..DebugLog import Debug, Error, Info, Warn, shorten
from ..debug_log import Debug, Error, Info, Warn, shorten
from ..proto.Autogen_pb2 import GenReplyReq, GenReplyResp, PrepChat, ReceiveReq, Terminate
from .AG2CAP import AG2CAP
from .AGActor import AGActor
from .ag_actor import AGActor
from .ag_to_cap import AG2CAP
class CAP2AG(AGActor):
@ -27,15 +27,13 @@ class CAP2AG(AGActor):
self.STATE = self.States.INIT
self._can2ag_name: str = self.actor_name + ".can2ag"
self._self_recursive: bool = self_recursive
self._ensemble: IRuntime = None
self._connectors = {}
def on_connect(self, ensemble: IRuntime):
def on_connect(self):
"""
Connect to the AutoGen system.
"""
self._ensemble = ensemble
self._ag2can_other_agent = AG2CAP(self._ensemble, self._other_agent_name)
self._ag2can_other_agent = AG2CAP(self._runtime, self._other_agent_name)
Debug(self._can2ag_name, "connected to {ensemble}")
def disconnect_network(self, ensemble: IRuntime):
@ -117,7 +115,7 @@ class CAP2AG(AGActor):
if topic in self._connectors:
return self._connectors[topic]
else:
connector = self._ensemble.find_by_topic(topic)
connector = self._runtime.find_by_topic(topic)
self._connectors[topic] = connector
return connector

View File

@ -3,8 +3,8 @@ import time
import zmq
from autogencap.Config import router_url, xpub_url, xsub_url
from autogencap.DebugLog import Debug, Info, Warn
from autogencap.config import router_url, xpub_url, xsub_url
from autogencap.debug_log import Debug, Info, Warn
class Broker:

View File

@ -3,7 +3,7 @@ import threading
from termcolor import colored
import autogencap.Config as Config
import autogencap.config as config
# Define log levels as constants
DEBUG = 0
@ -22,9 +22,9 @@ class BaseLogger:
def Log(self, level, context, msg):
# Check if the current level meets the threshold
if level >= Config.LOG_LEVEL: # Use the LOG_LEVEL from the Config module
if level >= config.LOG_LEVEL: # Use the LOG_LEVEL from the Config module
# Check if the context is in the list of ignored contexts
if context in Config.IGNORED_LOG_CONTEXTS:
if context in config.IGNORED_LOG_CONTEXTS:
return
with self._lock:
self.WriteLog(level, context, msg)
@ -34,7 +34,7 @@ class BaseLogger:
class ConsoleLogger(BaseLogger):
def __init__(self, use_color=True):
def __init__(self, use_color=config.USE_COLOR_LOGGING):
super().__init__()
self._use_color = use_color
@ -56,6 +56,7 @@ class ConsoleLogger(BaseLogger):
print(f"{thread_id} {timestamp} {level_name}: [{context}] {msg}")
# Modify this line to disable color logging by default
LOGGER = ConsoleLogger()

View File

@ -1,6 +1,6 @@
from autogencap.actor_runtime import IRuntime
from autogencap.constants import ZMQ_Runtime
from autogencap.DebugLog import Error
from autogencap.debug_log import Error
from autogencap.zmq_runtime import ZMQRuntime

View File

@ -1,4 +1,4 @@
from autogencap.DebugLog import Error
from autogencap.debug_log import Error
from autogencap.proto.CAP_pb2 import Error as ErrorMsg
from autogencap.proto.CAP_pb2 import ErrorCode

View File

@ -1,6 +1,3 @@
# 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 time
import uuid
from typing import Any, Dict
@ -8,11 +5,12 @@ from typing import Any, Dict
import zmq
from zmq.utils.monitor import recv_monitor_message
from .Config import router_url, xpub_url, xsub_url
from .DebugLog import Debug, Error, Info
from .actor_connector import IActorConnector
from .config import router_url, xpub_url, xsub_url
from .debug_log import Debug, Error, Info
class ActorSender:
class ZMQActorSender:
def __init__(self, context, topic):
self._context = context
self._topic = topic
@ -73,12 +71,12 @@ class ActorSender:
self._pub_socket.close()
class ActorConnector:
class ZMQActorConnector(IActorConnector):
def __init__(self, context, topic):
self._context = context
self._topic = topic
self._connect_sub_socket()
self._sender = ActorSender(context, topic)
self._sender = ZMQActorSender(context, topic)
time.sleep(0.1) # Wait for the socket to connect
def _connect_sub_socket(self):

View File

@ -4,12 +4,10 @@ import time
import zmq
from autogencap.Actor import Actor
from autogencap.ActorConnector import ActorConnector, ActorSender
from autogencap.Broker import Broker
from autogencap.Config import router_url, xpub_url, xsub_url
from autogencap.actor import Actor
from autogencap.broker import Broker
from autogencap.constants import Directory_Svc_Topic
from autogencap.DebugLog import Debug, Error, Info
from autogencap.debug_log import Debug, Error, Info
from autogencap.proto.CAP_pb2 import (
ActorInfo,
ActorInfoCollection,
@ -24,16 +22,18 @@ from autogencap.proto.CAP_pb2 import (
Error as ErrorMsg,
)
from autogencap.utility import report_error_msg
from autogencap.zmq_actor_connector import ZMQActorConnector, ZMQActorSender
# 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):
class ZMQDirectoryActor(Actor):
def __init__(self, topic: str, name: str, context: zmq.Context):
super().__init__(topic, name)
self._registered_actors = {}
self._network_prefix = ""
self._context = context
def on_bin_msg(self, msg: bytes, msg_type: str, topic: str, sender: str) -> bool:
if msg_type == ActorRegistration.__name__:
@ -50,7 +50,7 @@ class DirectoryActor(Actor):
Info("DirectorySvc", f"Ping received: {sender_topic}")
pong = Pong()
serialized_msg = pong.SerializeToString()
sender_connection = ActorSender(self._context, sender_topic)
sender_connection = ZMQActorSender(self._context, sender_topic)
sender_connection.send_bin_msg(Pong.__name__, serialized_msg)
def _on_actor_registration_msg(self, topic: str, msg_type: str, msg: bytes, sender_topic: str):
@ -67,7 +67,7 @@ class DirectoryActor(Actor):
else:
self._registered_actors[name] = actor_reg.actor_info
sender_connection = ActorSender(self._context, sender_topic)
sender_connection = ZMQActorSender(self._context, sender_topic)
serialized_msg = err.SerializeToString()
sender_connection.send_bin_msg(ErrorMsg.__name__, serialized_msg)
@ -96,16 +96,16 @@ class DirectoryActor(Actor):
else:
Error("DirectorySvc", f"Actor not found: {actor_lookup.actor_info.name}")
sender_connection = ActorSender(self._context, sender_topic)
sender_connection = ZMQActorSender(self._context, sender_topic)
serialized_msg = actor_lookup_resp.SerializeToString()
sender_connection.send_bin_msg(ActorLookupResponse.__name__, serialized_msg)
class DirectorySvc:
class ZMQDirectorySvc:
def __init__(self, context: zmq.Context = zmq.Context()):
self._context: zmq.Context = context
self._directory_connector: ActorConnector = None
self._directory_actor: DirectoryActor = None
self._directory_connector: ZMQActorConnector = None
self._directory_actor: ZMQDirectoryActor = None
def _no_other_directory(self) -> bool:
Debug("DirectorySvc", "Pinging existing DirectorySvc")
@ -116,12 +116,14 @@ class DirectorySvc:
return True
return False
def start(self):
def start(self, runtime):
Debug("DirectorySvc", "Starting.")
self._directory_connector = ActorConnector(self._context, Directory_Svc_Topic)
self._directory_connector = ZMQActorConnector(self._context, Directory_Svc_Topic)
if self._no_other_directory():
self._directory_actor = DirectoryActor(Directory_Svc_Topic, "Directory Service")
self._directory_actor.on_start(self._context)
self._directory_actor = ZMQDirectoryActor(
Directory_Svc_Topic, "Directory Service", self._context
) # Update this line
self._directory_actor.on_start(runtime)
Info("DirectorySvc", "Directory service started.")
else:
Info("DirectorySvc", "Another directory service is running. This instance will not start.")
@ -176,15 +178,8 @@ def main():
proxy: Broker = Broker(context)
proxy.start()
# Start the directory service
directory_svc = DirectorySvc(context)
directory_svc = ZMQDirectorySvc(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

View File

@ -0,0 +1,51 @@
# ZMQ implementation of the message receiver
import threading
import zmq
from autogencap.actor_runtime import IMessageReceiver
from autogencap.config import xpub_url
from autogencap.debug_log import Debug, Error
class ZMQMsgReceiver(IMessageReceiver):
def __init__(self, context: zmq.Context):
self._socket = None
self._context = context
self._start_event = threading.Event()
self.run = False
def init(self, actor_name: str):
"""Initialize the ZMQ message receiver."""
self.actor_name = actor_name
self._socket = self._context.socket(zmq.SUB)
self._socket.setsockopt(zmq.RCVTIMEO, 500)
self._socket.connect(xpub_url)
str_topic = f"{self.actor_name}"
self.add_listener(str_topic)
self._start_event.set()
def add_listener(self, topic: str):
"""Add a topic to the message receiver."""
Debug(self.actor_name, f"subscribe to: {topic}")
self._socket.setsockopt_string(zmq.SUBSCRIBE, f"{topic}")
def get_message(self):
"""Retrieve a message from the ZMQ socket."""
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:
return None # No message received, continue to next iteration
except Exception as e:
Error(self.actor_name, f"recv thread encountered an error: {e}")
return None
return topic, msg_type, sender_topic, msg
def stop(self):
"""Stop the ZMQ message receiver."""
self.run = False
self._socket.setsockopt(zmq.LINGER, 0)
self._socket.close()

View File

@ -3,27 +3,31 @@ from typing import List
import zmq
from .Actor import Actor
from .actor_runtime import IRuntime
from .ActorConnector import ActorConnector
from .Broker import Broker
from .actor import Actor
from .actor_connector import IActorConnector
from .actor_runtime import IMessageReceiver, IRuntime
from .broker import Broker
from .constants import Termination_Topic
from .DebugLog import Debug, Warn
from .DirectorySvc import DirectorySvc
from .debug_log import Debug, Warn
from .proto.CAP_pb2 import ActorInfo, ActorInfoCollection
from .zmq_actor_connector import ZMQActorConnector
class ZMQRuntime(IRuntime):
def __init__(self, name: str = "Local Actor Network", start_broker: bool = True):
def __init__(self, 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
self._directory_svc = None
self._log_name = self.__class__.__name__
def __str__(self):
return f"{self.name}"
return f" \
{self._log_name}\n \
is_broker: {self._broker is not None}\n \
is_directory_svc: {self._directory_svc is not None}\n \
local_actors: {self.local_actors}\n"
def _init_runtime(self):
if self._start_broker and self._broker is None:
@ -32,23 +36,28 @@ class ZMQRuntime(IRuntime):
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()
from .zmq_directory_svc import ZMQDirectorySvc
self._directory_svc = ZMQDirectorySvc(self._context)
self._directory_svc.start(self)
time.sleep(0.25) # Process queued thread events in Broker and Directory
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.on_start(self._context)
Debug("Local_Actor_Network", f"{actor.actor_name} registered in the network.")
actor.on_start(self) # Pass self (the runtime) to on_start
Debug(self._log_name, f"{actor.actor_name} registered in the network.")
def get_new_msg_receiver(self) -> IMessageReceiver:
from .zmq_msg_receiver import ZMQMsgReceiver
return ZMQMsgReceiver(self._context)
def connect(self):
self._init_runtime()
for actor in self.local_actors.values():
actor.on_connect(self)
actor.on_connect()
def disconnect(self):
for actor in self.local_actors.values():
@ -58,27 +67,27 @@ class ZMQRuntime(IRuntime):
if self._broker:
self._broker.stop()
def find_by_topic(self, topic: str) -> ActorConnector:
return ActorConnector(self._context, topic)
def find_by_topic(self, topic: str) -> IActorConnector:
return ZMQActorConnector(self._context, topic)
def find_by_name(self, name: str) -> ActorConnector:
def find_by_name(self, name: str) -> IActorConnector:
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.")
Warn(self._log_name, f"{name}, not found in the network.")
return None
Debug("Local_Actor_Network", f"[{name}] found in the network.")
Debug(self._log_name, f"[{name}] found in the network.")
return self.find_by_topic(name)
def find_termination(self) -> ActorConnector:
def find_termination(self) -> IActorConnector:
termination_topic: str = Termination_Topic
return self.find_by_topic(termination_topic)
def find_by_name_regex(self, name_regex) -> List[ActorInfo]:
actor_info: ActorInfoCollection = self._directory_svc.lookup_actor_info_by_name(name_regex)
if actor_info is None:
Warn("Local_Actor_Network", f"{name_regex}, not found in the network.")
Warn(self._log_name, f"{name_regex}, not found in the network.")
return None
Debug("Local_Actor_Network", f"[{name_regex}] found in the network.")
Debug(self._log_name, f"[{name_regex}] found in the network.")
actor_list = []
for actor in actor_info.info_coll:
actor_list.append(actor)

View File

@ -5,16 +5,16 @@ Demo App
import argparse
import _paths
import autogencap.Config as Config
import autogencap.DebugLog as DebugLog
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
import autogencap.config as config
import autogencap.debug_log as debug_log
from ag_demo import ag_demo
from ag_group_chat_demo import ag_groupchat_demo
from cap_autogen_group_demo import cap_ag_group_demo
from cap_autogen_pair_demo import cap_ag_pair_demo
from complex_actor_demo import complex_actor_demo
from list_agents import list_agents
from RemoteAGDemo import remote_ag_demo
from SimpleActorDemo import simple_actor_demo
from remote_autogen_demo import remote_ag_demo
from simple_actor_demo import simple_actor_demo
from single_threaded import single_threaded_demo
####################################################################################################
@ -28,8 +28,8 @@ def parse_args():
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]}")
Config.LOG_LEVEL = args.log_level
print(f"Log level: {debug_log.LEVEL_NAMES[args.log_level]}")
config.LOG_LEVEL = args.log_level
# Config.IGNORED_LOG_CONTEXTS.extend(["BROKER"])

View File

@ -4,11 +4,10 @@ Each agent represents a different role and knows how to connect to external syst
to retrieve information.
"""
from autogencap.Actor import Actor
from autogencap.actor import Actor
from autogencap.actor_connector import IActorConnector
from autogencap.actor_runtime import IRuntime
from autogencap.ActorConnector import ActorConnector
from autogencap.DebugLog import Debug, Info, shorten
from autogencap.runtime_factory import RuntimeFactory
from autogencap.debug_log import Debug, Info, shorten
class GreeterAgent(Actor):
@ -132,23 +131,23 @@ class PersonalAssistant(Actor):
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
self.fidelity: IActorConnector = None
self.financial_planner: IActorConnector = None
self.quant: IActorConnector = None
self.risk_manager: IActorConnector = None
def on_connect(self, network: IRuntime):
def on_connect(self):
"""
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.find_by_name("Fidelity")
self.financial_planner = network.find_by_name("Financial Planner")
self.quant = network.find_by_name("Quant")
self.risk_manager = network.find_by_name("Risk Manager")
Debug(self.actor_name, f"is connecting to {self._runtime}")
self.fidelity = self._runtime.find_by_name("Fidelity")
self.financial_planner = self._runtime.find_by_name("Financial Planner")
self.quant = self._runtime.find_by_name("Quant")
self.risk_manager = self._runtime.find_by_name("Risk Manager")
Debug(self.actor_name, "connected")
def disconnect_network(self, network: IRuntime):

View File

@ -1,6 +1,6 @@
from autogencap.ag_adapter.CAPGroupChat import CAPGroupChat
from autogencap.ag_adapter.CAPGroupChatManager import CAPGroupChatManager
from autogencap.DebugLog import Info
from autogencap.ag_adapter.cap_group_chat import CAPGroupChat
from autogencap.ag_adapter.cap_group_chat_manager import CAPGroupChatManager
from autogencap.debug_log import Info
from autogencap.runtime_factory import RuntimeFactory
from autogen import AssistantAgent, UserProxyAgent, config_list_from_json
@ -35,7 +35,7 @@ def cap_ag_group_demo():
cap_groupchat = CAPGroupChat(
agents=[user_proxy, coder, pm], messages=[], max_round=12, ensemble=ensemble, chat_initiator=user_proxy.name
)
manager = CAPGroupChatManager(groupchat=cap_groupchat, llm_config=gpt4_config, network=ensemble)
manager = CAPGroupChatManager(groupchat=cap_groupchat, llm_config=gpt4_config, runtime=ensemble)
manager.initiate_chat("Find a latest paper about gpt-4 on arxiv and find its potential applications in software.")
ensemble.disconnect()
Info("App", "App Exit")

View File

@ -1,15 +1,15 @@
import time
import autogencap.DebugLog as DebugLog
from autogencap.ag_adapter.CAPPair import CAPPair
from autogencap.DebugLog import ConsoleLogger, Info
import autogencap.debug_log as debug_log
from autogencap.ag_adapter.cap_pair import CAPPair
from autogencap.debug_log import ConsoleLogger, Info
from autogencap.runtime_factory import RuntimeFactory
from autogen import AssistantAgent, UserProxyAgent, config_list_from_json
def cap_ag_pair_demo():
DebugLog.LOGGER = ConsoleLogger(use_color=False)
debug_log.LOGGER = ConsoleLogger(use_color=False)
config_list = config_list_from_json(env_or_file="OAI_CONFIG_LIST")
assistant = AssistantAgent("assistant", llm_config={"config_list": config_list})

View File

@ -1,6 +1,6 @@
import time
from AppAgents import FidelityAgent, FinancialPlannerAgent, PersonalAssistant, QuantAgent, RiskManager
from app_agents import FidelityAgent, FinancialPlannerAgent, PersonalAssistant, QuantAgent, RiskManager
from autogencap.runtime_factory import RuntimeFactory
from termcolor import colored
@ -14,17 +14,17 @@ def complex_actor_demo():
sends them to the personal assistant agent, and terminates
when the user enters "quit".
"""
ensemble = RuntimeFactory.get_runtime("ZMQ")
runtime = RuntimeFactory.get_runtime("ZMQ")
# Register agents
ensemble.register(PersonalAssistant())
ensemble.register(FidelityAgent())
ensemble.register(FinancialPlannerAgent())
ensemble.register(RiskManager())
ensemble.register(QuantAgent())
runtime.register(PersonalAssistant())
runtime.register(FidelityAgent())
runtime.register(FinancialPlannerAgent())
runtime.register(RiskManager())
runtime.register(QuantAgent())
# Tell agents to connect to other agents
ensemble.connect()
runtime.connect()
# Get a channel to the personal assistant agent
pa = ensemble.find_by_name(PersonalAssistant.cls_agent_name)
pa = runtime.find_by_name(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
@ -48,4 +48,4 @@ def complex_actor_demo():
# Cleanup
pa.close()
ensemble.disconnect()
runtime.disconnect()

View File

@ -1,8 +1,8 @@
import time
from typing import List
from AppAgents import FidelityAgent, GreeterAgent
from autogencap.DebugLog import Info
from app_agents import FidelityAgent, GreeterAgent
from autogencap.debug_log import Info
from autogencap.proto.CAP_pb2 import ActorInfo
from autogencap.runtime_factory import RuntimeFactory

View File

@ -1,4 +1,4 @@
from AppAgents import GreeterAgent
from app_agents import GreeterAgent
from autogencap.runtime_factory import RuntimeFactory

View File

@ -1,6 +1,6 @@
import _paths
from AppAgents import GreeterAgent
from autogencap.DebugLog import Error
from app_agents import GreeterAgent
from autogencap.debug_log import Error
from autogencap.proto.CAP_pb2 import Ping
from autogencap.runtime_factory import RuntimeFactory

View File

@ -1,58 +0,0 @@
import time
import _paths
from autogencap.ag_adapter.CAP2AG import CAP2AG
from autogencap.Config import IGNORED_LOG_CONTEXTS
from autogencap.DebugLog import Info
from autogencap.runtime_factory import RuntimeFactory
from autogen import UserProxyAgent, config_list_from_json
# 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
ensemble = RuntimeFactory.get_runtime("ZMQ")
user_proxy_adptr = CAP2AG(ag_agent=user_proxy, the_other_name="assistant", init_chat=True, self_recursive=True)
ensemble.register(user_proxy_adptr)
ensemble.connect()
# Send a message to the user_proxy
user_proxy_conn = ensemble.find_by_name("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.")
ensemble.disconnect()
Info("StandaloneUserProxy", "App Exit")
def main():
IGNORED_LOG_CONTEXTS.extend(["BROKER", "DirectorySvc"])
assistant = StandaloneUserProxy()
assistant.run()
if __name__ == "__main__":
main()

View File

@ -1,8 +1,8 @@
import time
import _paths
from autogencap.ag_adapter.CAP2AG import CAP2AG
from autogencap.DebugLog import Info
from autogencap.ag_adapter.cap_to_ag import CAP2AG
from autogencap.debug_log import Info
from autogencap.runtime_factory import RuntimeFactory
from autogen import AssistantAgent, config_list_from_json

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import time
import _paths
from autogencap.ag_adapter.agent import Agent
from autogencap.Config import IGNORED_LOG_CONTEXTS
from autogencap.config import IGNORED_LOG_CONTEXTS
from autogencap.runtime_factory import RuntimeFactory
from autogen import UserProxyAgent

View File

@ -4,7 +4,7 @@ from typing import Any, Dict
import _paths
import zmq
from autogencap.Config import dealer_url, router_url, xpub_url, xsub_url
from autogencap.config import dealer_url, router_url, xpub_url, xsub_url
from zmq.utils.monitor import recv_monitor_message