You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
swarms/swarms/structs/base_swarm.py

815 lines
24 KiB

import os
import asyncio
import json
import uuid
from swarms.utils.file_processing import create_file_in_folder
from abc import ABC
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import (
Any,
Callable,
Dict,
List,
Optional,
Sequence,
)
import yaml
from swarms_memory import BaseVectorDatabase
from swarms.structs.agent import Agent
from swarms.structs.conversation import Conversation
from swarms.structs.omni_agent_types import AgentType
from pydantic import BaseModel
from swarms.utils.pandas_utils import (
dict_to_dataframe,
display_agents_info,
pydantic_model_to_dataframe,
)
from swarms.utils.loguru_logger import initialize_logger
logger = initialize_logger(log_folder="base_swarm")
class BaseSwarm(ABC):
"""
Base Swarm Class for all multi-agent systems
Attributes:
agents (List[Agent]): A list of agents
max_loops (int): The maximum number of loops to run
Methods:
communicate: Communicate with the swarm through the orchestrator, protocols, and the universal communication layer
run: Run the swarm
step: Step the swarm
add_agent: Add a agent to the swarm
remove_agent: Remove a agent from the swarm
broadcast: Broadcast a message to all agents
reset: Reset the swarm
plan: agents must individually plan using a workflow or pipeline
direct_message: Send a direct message to a agent
autoscaler: Autoscaler that acts like kubernetes for autonomous agents
get_agent_by_id: Locate a agent by id
get_agent_by_name: Locate a agent by name
assign_task: Assign a task to a agent
get_all_tasks: Get all tasks
get_finished_tasks: Get all finished tasks
get_pending_tasks: Get all penPding tasks
pause_agent: Pause a agent
resume_agent: Resume a agent
stop_agent: Stop a agent
restart_agent: Restart agent
scale_up: Scale up the number of agents
scale_down: Scale down the number of agents
scale_to: Scale to a specific number of agents
get_all_agents: Get all agents
get_swarm_size: Get the size of the swarm
get_swarm_status: Get the status of the swarm
save_swarm_state: Save the swarm state
loop: Loop through the swarm
run_async: Run the swarm asynchronously
run_batch_async: Run the swarm asynchronously
run_batch: Run the swarm asynchronously
batched_run: Run the swarm asynchronously
abatch_run: Asynchronous batch run with language model
arun: Asynchronous run
"""
def __init__(
self,
name: Optional[str] = None,
description: Optional[str] = None,
agents: Optional[List[Agent]] = None,
models: Optional[List[Any]] = None,
max_loops: Optional[int] = 200,
callbacks: Optional[Sequence[callable]] = None,
autosave: Optional[bool] = False,
logging: Optional[bool] = False,
return_metadata: Optional[bool] = False,
metadata_filename: Optional[
str
] = "multiagent_structure_metadata.json",
stopping_function: Optional[Callable] = None,
stopping_condition: Optional[str] = "stop",
stopping_condition_args: Optional[Dict] = None,
agentops_on: Optional[bool] = False,
speaker_selection_func: Optional[Callable] = None,
rules: Optional[str] = None,
collective_memory_system: Optional[
BaseVectorDatabase
] = False,
agent_ops_on: bool = False,
output_schema: Optional[BaseModel] = None,
*args,
**kwargs,
):
"""Initialize the swarm with agents"""
self.name = name
self.description = description
self.agents = agents
self.models = models
self.max_loops = max_loops
self.callbacks = callbacks
self.autosave = autosave
self.logging = logging
self.return_metadata = return_metadata
self.metadata_filename = metadata_filename
self.stopping_function = stopping_function
self.stopping_condition = stopping_condition
self.stopping_condition_args = stopping_condition_args
self.agentops_on = agentops_on
self.speaker_selection_func = speaker_selection_func
self.rules = rules
self.collective_memory_system = collective_memory_system
self.agent_ops_on = agent_ops_on
self.output_schema = output_schema
logger.info("Reliability checks activated.")
# Ensure that agents is exists
if self.agents is None:
logger.info("Agents must be provided.")
raise ValueError("Agents must be provided.")
# Ensure that agents is a list
if not isinstance(self.agents, list):
logger.error("Agents must be a list.")
raise TypeError("Agents must be a list.")
# Ensure that agents is not empty
if len(self.agents) == 0:
logger.error("Agents list must not be empty.")
raise ValueError("Agents list must not be empty.")
# Initialize conversation
self.conversation = Conversation(
time_enabled=True, rules=self.rules, *args, **kwargs
)
# Handle callbacks
if callbacks is not None:
for callback in self.callbacks:
if not callable(callback):
raise TypeError("Callback must be callable.")
# Handle autosave
if autosave:
self.save_to_json(metadata_filename)
# Handle stopping function
if stopping_function is not None:
if not callable(stopping_function):
raise TypeError("Stopping function must be callable.")
if stopping_condition_args is None:
stopping_condition_args = {}
self.stopping_condition_args = stopping_condition_args
self.stopping_condition = stopping_condition
self.stopping_function = stopping_function
# Handle stopping condition
if stopping_condition is not None:
if stopping_condition_args is None:
stopping_condition_args = {}
self.stopping_condition_args = stopping_condition_args
self.stopping_condition = stopping_condition
# If agentops is enabled, try to import agentops
if agentops_on is True:
for agent in self.agents:
agent.agent_ops_on = True
# Handle speaker selection function
if speaker_selection_func is not None:
if not callable(speaker_selection_func):
raise TypeError(
"Speaker selection function must be callable."
)
self.speaker_selection_func = speaker_selection_func
# Add the check for all the agents to see if agent ops is on!
if agent_ops_on is True:
for agent in self.agents:
agent.agent_ops_on = True
# Agents dictionary with agent name as key and agent object as value
self.agents_dict = {
agent.agent_name: agent for agent in self.agents
}
def communicate(self):
"""Communicate with the swarm through the orchestrator, protocols, and the universal communication layer"""
...
def run(self):
"""Run the swarm"""
...
def __call__(
self,
task,
*args,
**kwargs,
):
"""Call self as a function
Args:
task (_type_): _description_
Returns:
_type_: _description_
"""
try:
return self.run(task, *args, **kwargs)
except Exception as error:
logger.error(f"Error running {self.__class__.__name__}")
raise error
def step(self):
"""Step the swarm"""
def add_agent(self, agent: AgentType):
"""Add a agent to the swarm"""
self.agents.append(agent)
def add_agents(self, agents: List[AgentType]):
"""Add a list of agents to the swarm"""
self.agents.extend(agents)
def add_agent_by_id(self, agent_id: str):
"""Add a agent to the swarm by id"""
agent = self.get_agent_by_id(agent_id)
self.add_agent(agent)
def remove_agent(self, agent: AgentType):
"""Remove a agent from the swarm"""
self.agents.remove(agent)
def get_agent_by_name(self, name: str):
"""Get a agent by name"""
for agent in self.agents:
if agent.name == name:
return agent
def reset_all_agents(self):
"""Resets the state of all agents."""
for agent in self.agents:
agent.reset()
def broadcast(
self, message: str, sender: Optional[AgentType] = None
):
"""Broadcast a message to all agents"""
def reset(self):
"""Reset the swarm"""
def plan(self, task: str):
"""agents must individually plan using a workflow or pipeline"""
def self_find_agent_by_name(self, name: str):
"""
Find an agent by its name.
Args:
name (str): The name of the agent to find.
Returns:
Agent: The Agent object if found, None otherwise.
"""
for agent in self.agents:
if agent.agent_name == name:
return agent
return None
def self_find_agent_by_id(self, id: uuid.UUID):
"""
Find an agent by its id.
Args:
id (str): The id of the agent to find.
Returns:
Agent: The Agent object if found, None otherwise.
"""
for agent in self.agents:
if agent.id == id:
return agent
return None
def agent_exists(self, name: str):
"""
Check if an agent exists in the swarm.
Args:
name (str): The name of the agent to check.
Returns:
bool: True if the agent exists, False otherwise.
"""
return self.self_find_agent_by_name(name) is not None
def direct_message(
self,
message: str,
sender: AgentType,
recipient: AgentType,
):
"""Send a direct message to a agent"""
def autoscaler(self, num_agents: int, agent: List[AgentType]):
"""Autoscaler that acts like kubernetes for autonomous agents"""
def get_agent_by_id(self, id: str) -> AgentType:
"""Locate a agent by id"""
def assign_task(self, agent: AgentType, task: Any) -> Dict:
"""Assign a task to a agent"""
def get_all_tasks(self, agent: AgentType, task: Any):
"""Get all tasks"""
def get_finished_tasks(self) -> List[Dict]:
"""Get all finished tasks"""
def get_pending_tasks(self) -> List[Dict]:
"""Get all pending tasks"""
def pause_agent(self, agent: AgentType, agent_id: str):
"""Pause a agent"""
def resume_agent(self, agent: AgentType, agent_id: str):
"""Resume a agent"""
def stop_agent(self, agent: AgentType, agent_id: str):
"""Stop a agent"""
def restart_agent(self, agent: AgentType):
"""Restart agent"""
def scale_up(self, num_agent: int):
"""Scale up the number of agents"""
def scale_down(self, num_agent: int):
"""Scale down the number of agents"""
def scale_to(self, num_agent: int):
"""Scale to a specific number of agents"""
def get_all_agents(self) -> List[AgentType]:
"""Get all agents"""
def get_swarm_size(self) -> int:
"""Get the size of the swarm"""
# #@abstractmethod
def get_swarm_status(self) -> Dict:
"""Get the status of the swarm"""
# #@abstractmethod
def save_swarm_state(self):
"""Save the swarm state"""
def batched_run(self, tasks: List[Any], *args, **kwargs):
"""_summary_
Args:
tasks (List[Any]): _description_
"""
# Implement batched run
return [self.run(task, *args, **kwargs) for task in tasks]
async def abatch_run(self, tasks: List[str], *args, **kwargs):
"""Asynchronous batch run with language model
Args:
tasks (List[str]): _description_
Returns:
_type_: _description_
"""
return await asyncio.gather(
*(self.arun(task, *args, **kwargs) for task in tasks)
)
async def arun(self, task: Optional[str] = None, *args, **kwargs):
"""Asynchronous run
Args:
task (Optional[str], optional): _description_. Defaults to None.
"""
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None, self.run, task, *args, **kwargs
)
return result
def loop(
self,
task: Optional[str] = None,
*args,
**kwargs,
):
"""Loop through the swarm
Args:
task (Optional[str], optional): _description_. Defaults to None.
"""
# Loop through the self.max_loops
for i in range(self.max_loops):
self.run(task, *args, **kwargs)
async def aloop(
self,
task: Optional[str] = None,
*args,
**kwargs,
):
"""Asynchronous loop through the swarm
Args:
task (Optional[str], optional): _description_. Defaults to None.
"""
# Async Loop through the self.max_loops
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None, self.loop, task, *args, **kwargs
)
return result
def run_async(self, task: Optional[str] = None, *args, **kwargs):
"""Run the swarm asynchronously
Args:
task (Optional[str], optional): _description_. Defaults to None.
"""
loop = asyncio.get_event_loop()
result = loop.run_until_complete(
self.arun(task, *args, **kwargs)
)
return result
def run_batch_async(self, tasks: List[str], *args, **kwargs):
"""Run the swarm asynchronously
Args:
task (Optional[str], optional): _description_. Defaults to None.
"""
loop = asyncio.get_event_loop()
result = loop.run_until_complete(
self.abatch_run(tasks, *args, **kwargs)
)
return result
def run_batch(self, tasks: List[str], *args, **kwargs):
"""Run the swarm asynchronously
Args:
task (Optional[str], optional): _description_. Defaults to None.
"""
return self.batched_run(tasks, *args, **kwargs)
def select_agent_by_name(self, agent_name: str):
"""
Select an agent through their name
"""
# Find agent with id
for agent in self.agents:
if agent.name == agent_name:
return agent
def task_assignment_by_id(
self, task: str, agent_id: str, *args, **kwargs
):
"""
Assign a task to an agent
"""
# Assign task to agent by their agent id
agent = self.select_agent(agent_id)
return agent.run(task, *args, **kwargs)
def task_assignment_by_name(
self, task: str, agent_name: str, *args, **kwargs
):
"""
Assign a task to an agent
"""
# Assign task to agent by their agent id
agent = self.select_agent_by_name(agent_name)
return agent.run(task, *args, **kwargs)
def concurrent_run(self, task: str) -> List[str]:
"""Synchronously run the task on all llms and collect responses"""
with ThreadPoolExecutor() as executor:
future_to_llm = {
executor.submit(agent, task): agent
for agent in self.agents
}
responses = []
for future in as_completed(future_to_llm):
try:
responses.append(future.result())
except Exception as error:
print(
f"{future_to_llm[future]} generated an"
f" exception: {error}"
)
self.last_responses = responses
self.task_history.append(task)
return responses
def add_llm(self, agent: Callable):
"""Add an llm to the god mode"""
self.agents.append(agent)
def remove_llm(self, agent: Callable):
"""Remove an llm from the god mode"""
self.agents.remove(agent)
def run_all(self, task: str = None, *args, **kwargs):
"""Run all agents
Args:
task (str, optional): _description_. Defaults to None.
Returns:
_type_: _description_
"""
responses = []
for agent in self.agents:
responses.append(agent(task, *args, **kwargs))
return responses
def run_on_all_agents(self, task: str = None, *args, **kwargs):
"""Run on all agents
Args:
task (str, optional): _description_. Defaults to None.
Returns:
_type_: _description_
"""
with ThreadPoolExecutor() as executor:
responses = executor.map(
lambda agent: agent(task, *args, **kwargs),
self.agents,
)
return list(responses)
def add_swarm_entry(self, swarm):
"""
Add the information of a joined Swarm to the registry.
Args:
swarm (SwarmManagerBase): Instance of SwarmManagerBase representing the joined Swarm.
Returns:
None
"""
def add_agent_entry(self, agent: Agent):
"""
Add the information of an Agent to the registry.
Args:
agent (Agent): Instance of Agent representing the Agent.
Returns:
None
"""
def retrieve_swarm_information(self, swarm_id: str):
"""
Retrieve the information of a specific Swarm from the registry.
Args:
swarm_id (str): Unique identifier of the Swarm.
Returns:
SwarmManagerBase: Instance of SwarmManagerBase representing the retrieved Swarm, or None if not found.
"""
def retrieve_joined_agents(self, agent_id: str) -> List[Agent]:
"""
Retrieve the information the Agents which have joined the registry.
Returns:
Agent: Instance of Agent representing the retrieved Agent, or None if not found.
"""
def join_swarm(
self, from_entity: Agent | Agent, to_entity: Agent
):
"""
Add a relationship between a Swarm and an Agent or other Swarm to the registry.
Args:
from (Agent | SwarmManagerBase): Instance of Agent or SwarmManagerBase representing the source of the relationship.
"""
def metadata(self):
"""
Get the metadata of the multi-agent structure.
Returns:
dict: The metadata of the multi-agent structure.
"""
return {
"agents": self.agents,
"callbacks": self.callbacks,
"autosave": self.autosave,
"logging": self.logging,
"conversation": self.conversation,
}
def save_to_json(self, filename: str):
"""
Save the current state of the multi-agent structure to a JSON file.
Args:
filename (str): The name of the file to save the multi-agent structure to.
Returns:
None
"""
try:
with open(filename, "w") as f:
json.dump(self.__dict__, f)
except Exception as e:
logger.error(e)
def load_from_json(self, filename: str):
"""
Load the state of the multi-agent structure from a JSON file.
Args:
filename (str): The name of the file to load the multi-agent structure from.
Returns:
None
"""
try:
with open(filename) as f:
self.__dict__ = json.load(f)
except Exception as e:
logger.error(e)
def save_to_yaml(self, filename: str):
"""
Save the current state of the multi-agent structure to a YAML file.
Args:
filename (str): The name of the file to save the multi-agent structure to.
Returns:
None
"""
try:
with open(filename, "w") as f:
yaml.dump(self.__dict__, f)
except Exception as e:
logger.error(e)
def load_from_yaml(self, filename: str):
"""
Load the state of the multi-agent structure from a YAML file.
Args:
filename (str): The name of the file to load the multi-agent structure from.
Returns:
None
"""
try:
with open(filename) as f:
self.__dict__ = yaml.load(f)
except Exception as e:
logger.error(e)
def __repr__(self):
return f"{self.__class__.__name__}({self.__dict__})"
def __str__(self):
return f"{self.__class__.__name__}({self.__dict__})"
def __len__(self):
return len(self.agents)
def __getitem__(self, index):
return self.agents[index]
def __setitem__(self, index, value):
self.agents[index] = value
def __delitem__(self, index):
del self.agents[index]
def __iter__(self):
return iter(self.agents)
def __reversed__(self):
return reversed(self.agents)
def __contains__(self, value):
return value in self.agents
def agent_error_handling_check(self):
try:
if self.agents is None:
message = "You have not passed in any agents, you need to input agents to run a swarm"
logger.info(message)
raise ValueError(message)
except Exception as error:
logger.info(error)
raise error
def swarm_initialization(self, *args, **kwargs):
"""
Initializes the hierarchical swarm.
Args:
*args: Additional positional arguments.
**kwargs: Additional keyword arguments.
Returns:
None
"""
logger.info(
f"Initializing the hierarchical swarm: {self.name}"
)
logger.info(f"Purpose of this swarm: {self.description}")
# Now log number of agnets and their names
logger.info(f"Number of agents: {len(self.agents)}")
logger.info(
f"Agent names: {[agent.name for agent in self.agents]}"
)
# Now see if agents is not empty
if len(self.agents) == 0:
logger.info(
"No agents found. Please add agents to the swarm."
)
return None
# Now see if director is not empty
if self.director is None:
logger.info(
"No director found. Please add a director to the swarm."
)
return None
logger.info(
f"Initialization complete for the hierarchical swarm: {self.name}"
)
def export_output_schema(self):
"""
Export the output schema of the swarm.
Returns:
dict: The output schema of the swarm.
"""
return self.output_schema.model_dump_json(indent=4)
def export_output_schema_dict(self):
return self.output_schema.model_dump()
def export_and_autosave(self):
content = self.export_output_schema()
create_file_in_folder(
os.getenv("WORKSPACE_DIR"),
self.metadata_filename,
content=content,
)
return logger.info(
f"Metadata saved to {self.metadata_filename}"
)
def list_agents(self):
"""
List all agents in the swarm.
Returns:
None
"""
display_agents_info(self.agents)
def agents_to_dataframe(self):
"""
Convert agents to a pandas DataFrame.
"""
data = [agent.agent_output.dict() for agent in self.agents]
return dict_to_dataframe(data)
def model_to_dataframe(self):
"""
Convert the Pydantic model to a pandas DataFrame.
"""
return pydantic_model_to_dataframe(self.output_schema)