From 73d23f1995fae920b32eb6de4c42d30c9eb55068 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 25 Nov 2024 23:38:33 -0800 Subject: [PATCH 01/13] [DELETE OLD PACKAGES] --- async_agents.py | 56 ++++ async_executor.py | 131 ++++++++++ groupchat_new.py | 244 ++++++++++++++++++ .../example_async_vs_multithread.py | 8 +- .../sequential_worflow_test.py | 0 .../sequential_workflow.py | 0 pyproject.toml | 25 +- requirements.txt | 11 +- swarms/structs/agent.py | 92 +++---- swarms/structs/base_workflow.py | 115 ++++----- swarms/structs/conversation.py | 62 ++--- swarms/structs/mixture_of_agents.py | 4 +- swarms/structs/multi_agent_exec.py | 2 +- swarms/structs/rearrange.py | 23 +- swarms/structs/sequential_workflow.py | 2 +- swarms/tools/json_former.py | 10 - swarms/tools/tool_utils.py | 20 +- swarms/utils/callable_name.py | 203 +++++++++++++++ swarms/utils/markdown_message.py | 9 +- 19 files changed, 787 insertions(+), 230 deletions(-) create mode 100644 async_agents.py create mode 100644 async_executor.py create mode 100644 groupchat_new.py rename example_async_vs_multithread.py => new_features_examples/example_async_vs_multithread.py (98%) rename sequential_worflow_test.py => new_features_examples/sequential_worflow_test.py (100%) rename sequential_workflow.py => new_features_examples/sequential_workflow.py (100%) create mode 100644 swarms/utils/callable_name.py diff --git a/async_agents.py b/async_agents.py new file mode 100644 index 00000000..0d9353db --- /dev/null +++ b/async_agents.py @@ -0,0 +1,56 @@ +import os + +from dotenv import load_dotenv +from swarm_models import OpenAIChat + +from swarms import Agent +from swarms.prompts.finance_agent_sys_prompt import ( + FINANCIAL_AGENT_SYS_PROMPT, +) +from async_executor import HighSpeedExecutor + +load_dotenv() + +# Get the OpenAI API key from the environment variable +api_key = os.getenv("OPENAI_API_KEY") + +# Create an instance of the OpenAIChat class +model = OpenAIChat( + openai_api_key=api_key, model_name="gpt-4o-mini", temperature=0.1 +) + +# Initialize the agent +agent = Agent( + agent_name="Financial-Analysis-Agent", + system_prompt=FINANCIAL_AGENT_SYS_PROMPT, + llm=model, + max_loops=1, + # autosave=True, + # dashboard=False, + # verbose=True, + # dynamic_temperature_enabled=True, + # saved_state_path="finance_agent.json", + # user_name="swarms_corp", + # retry_attempts=1, + # context_length=200000, + # return_step_meta=True, + # output_type="json", # "json", "dict", "csv" OR "string" soon "yaml" and + # auto_generate_prompt=False, # Auto generate prompt for the agent based on name, description, and system prompt, task + # # artifacts_on=True, + # artifacts_output_path="roth_ira_report", + # artifacts_file_extension=".txt", + # max_tokens=8000, + # return_history=True, +) + + +def execute_agent( + task: str = "How can I establish a ROTH IRA to buy stocks and get a tax break? What are the criteria. Create a report on this question.", +): + return agent.run(task) + + +executor = HighSpeedExecutor() +results = executor.run(execute_agent, 2) + +print(results) diff --git a/async_executor.py b/async_executor.py new file mode 100644 index 00000000..e9fcfa4e --- /dev/null +++ b/async_executor.py @@ -0,0 +1,131 @@ +import asyncio +import multiprocessing as mp +import time +from functools import partial +from typing import Any, Dict, Union + + +class HighSpeedExecutor: + def __init__(self, num_processes: int = None): + """ + Initialize the executor with configurable number of processes. + If num_processes is None, it uses CPU count. + """ + self.num_processes = num_processes or mp.cpu_count() + + async def _worker( + self, + queue: asyncio.Queue, + func: Any, + *args: Any, + **kwargs: Any, + ): + """Async worker that processes tasks from the queue""" + while True: + try: + # Non-blocking get from queue + await queue.get() + await asyncio.get_event_loop().run_in_executor( + None, partial(func, *args, **kwargs) + ) + queue.task_done() + except asyncio.CancelledError: + break + + async def _distribute_tasks( + self, num_tasks: int, queue: asyncio.Queue + ): + """Distribute tasks across the queue""" + for i in range(num_tasks): + await queue.put(i) + + async def execute_batch( + self, + func: Any, + num_executions: int, + *args: Any, + **kwargs: Any, + ) -> Dict[str, Union[int, float]]: + """ + Execute the given function multiple times concurrently. + + Args: + func: The function to execute + num_executions: Number of times to execute the function + *args, **kwargs: Arguments to pass to the function + + Returns: + A dictionary containing the number of executions, duration, and executions per second. + """ + queue = asyncio.Queue() + + # Create worker tasks + workers = [ + asyncio.create_task( + self._worker(queue, func, *args, **kwargs) + ) + for _ in range(self.num_processes) + ] + + # Start timing + start_time = time.perf_counter() + + # Distribute tasks + await self._distribute_tasks(num_executions, queue) + + # Wait for all tasks to complete + await queue.join() + + # Cancel workers + for worker in workers: + worker.cancel() + + # Wait for all workers to finish + await asyncio.gather(*workers, return_exceptions=True) + + end_time = time.perf_counter() + duration = end_time - start_time + + return { + "executions": num_executions, + "duration": duration, + "executions_per_second": num_executions / duration, + } + + def run( + self, + func: Any, + num_executions: int, + *args: Any, + **kwargs: Any, + ): + return asyncio.run( + self.execute_batch(func, num_executions, *args, **kwargs) + ) + + +# def example_function(x: int = 0) -> int: +# """Example function to execute""" +# return x * x + + +# async def main(): +# # Create executor with number of CPU cores +# executor = HighSpeedExecutor() + +# # Execute the function 1000 times +# result = await executor.execute_batch( +# example_function, num_executions=1000, x=42 +# ) + +# print( +# f"Completed {result['executions']} executions in {result['duration']:.2f} seconds" +# ) +# print( +# f"Rate: {result['executions_per_second']:.2f} executions/second" +# ) + + +# if __name__ == "__main__": +# # Run the async main function +# asyncio.run(main()) diff --git a/groupchat_new.py b/groupchat_new.py new file mode 100644 index 00000000..69c424d4 --- /dev/null +++ b/groupchat_new.py @@ -0,0 +1,244 @@ +import os +import asyncio +from pydantic import BaseModel, Field +from typing import List, Dict, Any +from swarms import Agent +from swarm_models import OpenAIChat +from dotenv import load_dotenv +from swarms.utils.formatter import formatter + +# Load environment variables +load_dotenv() + +# Get OpenAI API key +api_key = os.getenv("OPENAI_API_KEY") + + +# Define Pydantic schema for agent outputs +class AgentOutput(BaseModel): + """Schema for capturing the output of each agent.""" + + agent_name: str = Field(..., description="The name of the agent") + message: str = Field( + ..., + description="The agent's response or contribution to the group chat", + ) + metadata: Dict[str, Any] = Field( + default_factory=dict, + description="Additional metadata about the agent's response", + ) + + +class GroupChat: + """ + GroupChat class to enable multiple agents to communicate in an asynchronous group chat. + Each agent is aware of all other agents, every message exchanged, and the social context. + """ + + def __init__( + self, + name: str, + description: str, + agents: List[Agent], + max_loops: int = 1, + ): + """ + Initialize the GroupChat. + + Args: + name (str): Name of the group chat. + description (str): Description of the purpose of the group chat. + agents (List[Agent]): A list of agents participating in the chat. + max_loops (int): Maximum number of loops to run through all agents. + """ + self.name = name + self.description = description + self.agents = agents + self.max_loops = max_loops + self.chat_history = ( + [] + ) # Stores all messages exchanged in the chat + + formatter.print_panel( + f"Initialized GroupChat '{self.name}' with {len(self.agents)} agents. Max loops: {self.max_loops}", + title="Groupchat Swarm", + ) + + async def _agent_conversation( + self, agent: Agent, input_message: str + ) -> AgentOutput: + """ + Facilitate a single agent's response to the chat. + + Args: + agent (Agent): The agent responding. + input_message (str): The message triggering the response. + + Returns: + AgentOutput: The agent's response captured in a structured format. + """ + formatter.print_panel( + f"Agent '{agent.agent_name}' is responding to the message: {input_message}", + title="Groupchat Swarm", + ) + response = await asyncio.to_thread(agent.run, input_message) + + output = AgentOutput( + agent_name=agent.agent_name, + message=response, + metadata={"context_length": agent.context_length}, + ) + # logger.debug(f"Agent '{agent.agent_name}' response: {response}") + return output + + async def _run(self, initial_message: str) -> List[AgentOutput]: + """ + Execute the group chat asynchronously, looping through all agents up to max_loops. + + Args: + initial_message (str): The initial message to start the chat. + + Returns: + List[AgentOutput]: The responses of all agents across all loops. + """ + formatter.print_panel( + f"Starting group chat '{self.name}' with initial message: {initial_message}", + title="Groupchat Swarm", + ) + self.chat_history.append( + {"sender": "System", "message": initial_message} + ) + + outputs = [] + for loop in range(self.max_loops): + formatter.print_panel( + f"Group chat loop {loop + 1}/{self.max_loops}", + title="Groupchat Swarm", + ) + + for agent in self.agents: + # Create a custom input message for each agent, sharing the chat history and social context + input_message = ( + f"Chat History:\n{self._format_chat_history()}\n\n" + f"Participants:\n" + + "\n".join( + [ + f"- {a.agent_name}: {a.system_prompt}" + for a in self.agents + ] + ) + + f"\n\nNew Message: {initial_message}\n\n" + f"You are '{agent.agent_name}'. Remember to keep track of the social context, who is speaking, " + f"and respond accordingly based on your role: {agent.system_prompt}." + ) + + # Collect agent's response + output = await self._agent_conversation( + agent, input_message + ) + outputs.append(output) + + # Update chat history with the agent's response + self.chat_history.append( + { + "sender": agent.agent_name, + "message": output.message, + } + ) + + formatter.print_panel( + "Group chat completed. All agent responses captured.", + title="Groupchat Swarm", + ) + return outputs + + def run(self, task: str, *args, **kwargs): + return asyncio.run(self.run(task, *args, **kwargs)) + + def _format_chat_history(self) -> str: + """ + Format the chat history for agents to understand the context. + + Returns: + str: The formatted chat history as a string. + """ + return "\n".join( + [ + f"{entry['sender']}: {entry['message']}" + for entry in self.chat_history + ] + ) + + def __str__(self) -> str: + """String representation of the group chat's outputs.""" + return self._format_chat_history() + + def to_json(self) -> str: + """JSON representation of the group chat's outputs.""" + return [ + {"sender": entry["sender"], "message": entry["message"]} + for entry in self.chat_history + ] + + +# Example Usage +if __name__ == "__main__": + + load_dotenv() + + # Get the OpenAI API key from the environment variable + api_key = os.getenv("OPENAI_API_KEY") + + # Create an instance of the OpenAIChat class + model = OpenAIChat( + openai_api_key=api_key, + model_name="gpt-4o-mini", + temperature=0.1, + ) + + # Example agents + agent1 = Agent( + agent_name="Financial-Analysis-Agent", + system_prompt="You are a financial analyst specializing in investment strategies.", + llm=model, + max_loops=1, + autosave=False, + dashboard=False, + verbose=True, + dynamic_temperature_enabled=True, + user_name="swarms_corp", + retry_attempts=1, + context_length=200000, + output_type="string", + streaming_on=False, + ) + + agent2 = Agent( + agent_name="Tax-Adviser-Agent", + system_prompt="You are a tax adviser who provides clear and concise guidance on tax-related queries.", + llm=model, + max_loops=1, + autosave=False, + dashboard=False, + verbose=True, + dynamic_temperature_enabled=True, + user_name="swarms_corp", + retry_attempts=1, + context_length=200000, + output_type="string", + streaming_on=False, + ) + + # Create group chat + group_chat = GroupChat( + name="Financial Discussion", + description="A group chat for financial analysis and tax advice.", + agents=[agent1, agent2], + ) + + # Run the group chat + asyncio.run( + group_chat.run( + "How can I establish a ROTH IRA to buy stocks and get a tax break? What are the criteria? What do you guys think?" + ) + ) diff --git a/example_async_vs_multithread.py b/new_features_examples/example_async_vs_multithread.py similarity index 98% rename from example_async_vs_multithread.py rename to new_features_examples/example_async_vs_multithread.py index f547abc8..25d514aa 100644 --- a/example_async_vs_multithread.py +++ b/new_features_examples/example_async_vs_multithread.py @@ -1,6 +1,5 @@ import os import asyncio -import threading from swarms import Agent from swarm_models import OpenAIChat import time @@ -40,18 +39,21 @@ agent = Agent( streaming_on=False, ) + # Function to measure time and memory usage def measure_time_and_memory(func): def wrapper(*args, **kwargs): start_time = time.time() result = func(*args, **kwargs) end_time = time.time() - memory_usage = psutil.Process().memory_info().rss / 1024 ** 2 + memory_usage = psutil.Process().memory_info().rss / 1024**2 print(f"Time taken: {end_time - start_time} seconds") print(f"Memory used: {memory_usage} MB") return result + return wrapper + # Function to run the agent asynchronously @measure_time_and_memory async def run_agent_async(): @@ -61,11 +63,13 @@ async def run_agent_async(): ) ) + # Function to run the agent on another thread @measure_time_and_memory def run_agent_thread(): asyncio.run(run_agent_async()) + # Run the agent asynchronously and on another thread to test the speed asyncio.run(run_agent_async()) run_agent_thread() diff --git a/sequential_worflow_test.py b/new_features_examples/sequential_worflow_test.py similarity index 100% rename from sequential_worflow_test.py rename to new_features_examples/sequential_worflow_test.py diff --git a/sequential_workflow.py b/new_features_examples/sequential_workflow.py similarity index 100% rename from sequential_workflow.py rename to new_features_examples/sequential_workflow.py diff --git a/pyproject.toml b/pyproject.toml index d8d06c61..bb3d5043 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "swarms" -version = "6.2.9" +version = "6.3.6" description = "Swarms - Pytorch" license = "MIT" authors = ["Kye Gomez "] @@ -37,6 +37,14 @@ keywords = [ "Generative AI", "Agent Marketplace", "Agent Store", + "quant", + "finance", + "algorithmic trading", + "portfolio optimization", + "risk management", + "financial modeling", + "machine learning for finance", + "natural language processing for finance", ] classifiers = [ "Development Status :: 4 - Beta", @@ -52,27 +60,18 @@ python = ">=3.10,<4.0" torch = ">=2.1.1,<3.0" transformers = ">= 4.39.0, <5.0.0" asyncio = ">=3.4.3,<4.0" -langchain-community = "0.0.29" -langchain-experimental = "0.0.55" -backoff = "2.2.1" toml = "*" pypdf = "4.3.1" -loguru = "0.7.2" +loguru = "*" pydantic = "2.8.2" -tenacity = "8.5.0" -Pillow = "10.4.0" +tenacity = "*" psutil = "*" sentry-sdk = {version = "*", extras = ["http"]} # Updated here python-dotenv = "*" PyYAML = "*" docstring_parser = "0.16" -fastapi = "*" -openai = ">=1.30.1,<2.0" -termcolor = "*" tiktoken = "*" networkx = "*" -swarms-memory = "*" -black = "*" aiofiles = "*" swarm-models = "*" clusterops = "*" @@ -96,9 +95,7 @@ mypy-protobuf = "^3.0.0" [tool.poetry.group.test.dependencies] pytest = "^8.1.1" -termcolor = "^2.4.0" pandas = "^2.2.2" -fastapi = ">=0.110.1,<0.116.0" [tool.ruff] line-length = 70 diff --git a/requirements.txt b/requirements.txt index d422222b..ca9fdcdd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,21 +2,16 @@ torch>=2.1.1,<3.0 transformers>=4.39.0,<5.0.0 asyncio>=3.4.3,<4.0 -langchain-community==0.0.28 -langchain-experimental==0.0.55 -backoff==2.2.1 toml pypdf==4.3.1 ratelimit==2.2.1 loguru==0.7.2 pydantic==2.8.2 -tenacity==8.5.0 -Pillow==10.4.0 +tenacity rich psutil sentry-sdk python-dotenv -opencv-python-headless PyYAML docstring_parser==0.16 black>=23.1,<25.0 @@ -26,12 +21,8 @@ types-pytz>=2023.3,<2025.0 types-chardet>=5.0.4.6 mypy-protobuf>=3.0.0 pytest>=8.1.1 -termcolor>=2.4.0 pandas>=2.2.2 -fastapi>=0.110.1 networkx -swarms-memory -pre-commit aiofiles swarm-models clusterops diff --git a/swarms/structs/agent.py b/swarms/structs/agent.py index a4c04a16..d1f3a745 100644 --- a/swarms/structs/agent.py +++ b/swarms/structs/agent.py @@ -24,8 +24,6 @@ import toml import yaml from pydantic import BaseModel from swarm_models.tiktoken_wrapper import TikTokenizer -from termcolor import colored - from swarms.agents.ape_agent import auto_generate_prompt from swarms.prompts.agent_system_prompts import AGENT_SYSTEM_PROMPT_3 from swarms.prompts.multi_modal_autonomous_instruction_prompt import ( @@ -671,11 +669,8 @@ class Agent: return self.stopping_condition(response) return False except Exception as error: - print( - colored( - f"Error checking stopping condition: {error}", - "red", - ) + logger.error( + f"Error checking stopping condition: {error}" ) def dynamic_temperature(self): @@ -688,21 +683,20 @@ class Agent: try: if hasattr(self.llm, "temperature"): # Randomly change the temperature attribute of self.llm object - logger.info("Enabling Random Dyamic Temperature") self.llm.temperature = random.uniform(0.0, 1.0) else: # Use a default temperature self.llm.temperature = 0.5 except Exception as error: - print( - colored( - f"Error dynamically changing temperature: {error}" - ) + logger.error( + f"Error dynamically changing temperature: {error}" ) def print_dashboard(self): """Print dashboard""" - print(colored("Initializing Agent Dashboard...", "yellow")) + formatter.print_panel( + f"Initializing Agent: {self.agent_name}" + ) data = self.to_dict() @@ -710,22 +704,19 @@ class Agent: # data = json.dumps(data, indent=4) # json_data = json.dumps(data, indent=4) - print( - colored( - f""" - Agent Dashboard - -------------------------------------------- + formatter.print_panel( + f""" + Agent Dashboard + -------------------------------------------- - Agent {self.agent_name} is initializing for {self.max_loops} with the following configuration: - ---------------------------------------- + Agent {self.agent_name} is initializing for {self.max_loops} with the following configuration: + ---------------------------------------- - Agent Configuration: - Configuration: {data} + Agent Configuration: + Configuration: {data} - ---------------------------------------- - """, - "green", - ) + ---------------------------------------- + """, ) def loop_count_print( @@ -737,7 +728,7 @@ class Agent: loop_count (_type_): _description_ max_loops (_type_): _description_ """ - print(colored(f"\nLoop {loop_count} of {max_loops}", "cyan")) + logger.info(f"\nLoop {loop_count} of {max_loops}") print("\n") # Check parameters @@ -761,8 +752,8 @@ class Agent: self, task: Optional[str] = None, img: Optional[str] = None, - is_last: bool = False, - print_task: bool = False, + is_last: Optional[bool] = False, + print_task: Optional[bool] = False, *args, **kwargs, ) -> Any: @@ -960,7 +951,7 @@ class Agent: if self.interactive: logger.info("Interactive mode enabled.") - user_input = colored(input("You: "), "red") + user_input = formatter.print_panel(input("You: ")) # User-defined exit command if ( @@ -1060,7 +1051,7 @@ class Agent: except Exception as error: logger.info( - f"Error running agent: {error} optimize your input parameters" + f"Error running agent: {error} optimize your input parameter" ) raise error @@ -1261,7 +1252,7 @@ class Agent: logger.info(f"Running bulk tasks: {inputs}") return [self.run(**input_data) for input_data in inputs] except Exception as error: - print(colored(f"Error running bulk run: {error}", "red")) + logger.info(f"Error running bulk run: {error}", "red") def save(self) -> None: """Save the agent history to a file. @@ -1438,9 +1429,7 @@ class Agent: with open(file_path, "w") as f: yaml.dump(self.to_dict(), f) except Exception as error: - logger.error( - colored(f"Error saving agent to YAML: {error}", "red") - ) + logger.error(f"Error saving agent to YAML: {error}") raise error def get_llm_parameters(self): @@ -1505,7 +1494,7 @@ class Agent: role=self.user_name, content=data ) except Exception as error: - print(colored(f"Error ingesting docs: {error}", "red")) + logger.info(f"Error ingesting docs: {error}", "red") def ingest_pdf(self, pdf: str): """Ingest the pdf into the memory @@ -1520,7 +1509,7 @@ class Agent: role=self.user_name, content=text ) except Exception as error: - print(colored(f"Error ingesting pdf: {error}", "red")) + logger.info(f"Error ingesting pdf: {error}", "red") def receieve_message(self, name: str, message: str): """Receieve a message""" @@ -1604,12 +1593,10 @@ class Agent: role=self.user_name, content=text ) except Exception as error: - print( - colored( - f"Error getting docs from doc folders: {error}", - "red", - ) + logger.error( + f"Error getting docs from doc folders: {error}" ) + raise error def check_end_session_agentops(self): if self.agent_ops_on is True: @@ -1629,7 +1616,8 @@ class Agent: try: # Query the long term memory if self.long_term_memory is not None: - logger.info(f"Querying long term memory for: {task}") + formatter.print_panel(f"Querying RAG for: {task}") + memory_retrieval = self.long_term_memory.query( task, *args, **kwargs ) @@ -1638,15 +1626,15 @@ class Agent: f"Documents Available: {str(memory_retrieval)}" ) - # Count the tokens - memory_token_count = self.tokenizer.count_tokens( - memory_retrieval - ) - if memory_token_count > self.memory_chunk_size: - # Truncate the memory by the memory chunk size - memory_retrieval = self.truncate_string_by_tokens( - memory_retrieval, self.memory_chunk_size - ) + # # Count the tokens + # memory_token_count = self.tokenizer.count_tokens( + # memory_retrieval + # ) + # if memory_token_count > self.memory_chunk_size: + # # Truncate the memory by the memory chunk size + # memory_retrieval = self.truncate_string_by_tokens( + # memory_retrieval, self.memory_chunk_size + # ) self.short_memory.add( role="Database", diff --git a/swarms/structs/base_workflow.py b/swarms/structs/base_workflow.py index b75bfe2c..4107042a 100644 --- a/swarms/structs/base_workflow.py +++ b/swarms/structs/base_workflow.py @@ -1,8 +1,7 @@ import json from typing import Any, Dict, List, Optional -from termcolor import colored - +from swarms.utils.formatter import formatter from swarms.structs.agent import Agent from swarms.structs.base_structure import BaseStructure from swarms.structs.task import Task @@ -132,9 +131,10 @@ class BaseWorkflow(BaseStructure): for task in self.tasks: task.result = None except Exception as error: - print( - colored(f"Error resetting workflow: {error}", "red"), + formatter.print_panel( + f"Error resetting workflow: {error}" ) + raise error def get_task_results(self) -> Dict[str, Any]: """ @@ -148,10 +148,8 @@ class BaseWorkflow(BaseStructure): task.description: task.result for task in self.tasks } except Exception as error: - print( - colored( - f"Error getting task results: {error}", "red" - ), + formatter.print_panel( + f"Error getting task results: {error}" ) def remove_task(self, task: str) -> None: @@ -163,12 +161,10 @@ class BaseWorkflow(BaseStructure): if task.description != task ] except Exception as error: - print( - colored( - f"Error removing task from workflow: {error}", - "red", - ), + formatter.print_panel( + f"Error removing task from workflow: {error}", ) + raise error def update_task(self, task: str, **updates) -> None: """ @@ -203,11 +199,9 @@ class BaseWorkflow(BaseStructure): f"Task {task} not found in workflow." ) except Exception as error: - print( - colored( - f"Error updating task in workflow: {error}", "red" - ), - ) + formatter.print_panel( + f"Error updating task in workflow: {error}" + ), def delete_task(self, task: str) -> None: """ @@ -240,12 +234,10 @@ class BaseWorkflow(BaseStructure): f"Task {task} not found in workflow." ) except Exception as error: - print( - colored( - f"Error deleting task from workflow: {error}", - "red", - ), + formatter.print_panel( + f"Error deleting task from workflow: {error}", ) + raise error def save_workflow_state( self, @@ -287,23 +279,18 @@ class BaseWorkflow(BaseStructure): } json.dump(state, f, indent=4) except Exception as error: - print( - colored( - f"Error saving workflow state: {error}", - "red", - ) + formatter.print_panel( + f"Error saving workflow state: {error}", ) + raise error def add_objective_to_workflow(self, task: str, **kwargs) -> None: """Adds an objective to the workflow.""" try: - print( - colored( - """ - Adding Objective to Workflow...""", - "green", - attrs=["bold", "underline"], - ) + formatter.print_panel( + """ + Adding Objective to Workflow...""", + "green", ) task = Task( @@ -314,12 +301,10 @@ class BaseWorkflow(BaseStructure): ) self.tasks.append(task) except Exception as error: - print( - colored( - f"Error adding objective to workflow: {error}", - "red", - ) + formatter.print_panel( + f"Error adding objective to workflow: {error}", ) + raise error def load_workflow_state( self, filepath: str = None, **kwargs @@ -359,11 +344,8 @@ class BaseWorkflow(BaseStructure): ) self.tasks.append(task) except Exception as error: - print( - colored( - f"Error loading workflow state: {error}", - "red", - ) + formatter.print_panel( + f"Error loading workflow state: {error}", ) def workflow_dashboard(self, **kwargs) -> None: @@ -383,25 +365,21 @@ class BaseWorkflow(BaseStructure): >>> workflow.workflow_dashboard() """ - print( - colored( - f""" - Sequential Workflow Dashboard - -------------------------------- - Name: {self.name} - Description: {self.description} - task_pool: {len(self.task_pool)} - Max Loops: {self.max_loops} - Autosave: {self.autosave} - Autosave Filepath: {self.saved_state_filepath} - Restore Filepath: {self.restore_state_filepath} - -------------------------------- - Metadata: - kwargs: {kwargs} - """, - "cyan", - attrs=["bold", "underline"], - ) + formatter.print_panel( + f""" + Sequential Workflow Dashboard + -------------------------------- + Name: {self.name} + Description: {self.description} + task_pool: {len(self.task_pool)} + Max Loops: {self.max_loops} + Autosave: {self.autosave} + Autosave Filepath: {self.saved_state_filepath} + Restore Filepath: {self.restore_state_filepath} + -------------------------------- + Metadata: + kwargs: {kwargs} + """ ) def workflow_bootup(self, **kwargs) -> None: @@ -409,11 +387,6 @@ class BaseWorkflow(BaseStructure): Workflow bootup. """ - print( - colored( - """ - Sequential Workflow Initializing...""", - "green", - attrs=["bold", "underline"], - ) + formatter.print_panel( + """Sequential Workflow Initializing...""", ) diff --git a/swarms/structs/conversation.py b/swarms/structs/conversation.py index f808382d..a86a6d3b 100644 --- a/swarms/structs/conversation.py +++ b/swarms/structs/conversation.py @@ -3,10 +3,9 @@ import json from typing import Any, Optional import yaml -from termcolor import colored - from swarms.structs.base_structure import BaseStructure from typing import TYPE_CHECKING +from swarms.utils.formatter import formatter if TYPE_CHECKING: from swarms.structs.agent import ( @@ -191,18 +190,9 @@ class Conversation(BaseStructure): Args: detailed (bool, optional): detailed. Defaults to False. """ - role_to_color = { - "system": "red", - "user": "green", - "assistant": "blue", - "function": "magenta", - } for message in self.conversation_history: - print( - colored( - f"{message['role']}: {message['content']}\n\n", - role_to_color[message["role"]], - ) + formatter.print_panel( + f"{message['role']}: {message['content']}\n\n" ) def export_conversation(self, filename: str, *args, **kwargs): @@ -307,46 +297,36 @@ class Conversation(BaseStructure): for message in messages: if message["role"] == "system": - print( - colored( - f"system: {message['content']}\n", - role_to_color[message["role"]], - ) + formatter.print_panel( + f"system: {message['content']}\n", + role_to_color[message["role"]], ) elif message["role"] == "user": - print( - colored( - f"user: {message['content']}\n", - role_to_color[message["role"]], - ) + formatter.print_panel( + f"user: {message['content']}\n", + role_to_color[message["role"]], ) elif message["role"] == "assistant" and message.get( "function_call" ): - print( - colored( - f"assistant: {message['function_call']}\n", - role_to_color[message["role"]], - ) + formatter.print_panel( + f"assistant: {message['function_call']}\n", + role_to_color[message["role"]], ) elif message["role"] == "assistant" and not message.get( "function_call" ): - print( - colored( - f"assistant: {message['content']}\n", - role_to_color[message["role"]], - ) + formatter.print_panel( + f"assistant: {message['content']}\n", + role_to_color[message["role"]], ) elif message["role"] == "tool": - print( - colored( - ( - f"function ({message['name']}):" - f" {message['content']}\n" - ), - role_to_color[message["role"]], - ) + formatter.print_panel( + ( + f"function ({message['name']}):" + f" {message['content']}\n" + ), + role_to_color[message["role"]], ) def truncate_memory_with_tokenizer(self): diff --git a/swarms/structs/mixture_of_agents.py b/swarms/structs/mixture_of_agents.py index f80701ef..e91d565f 100644 --- a/swarms/structs/mixture_of_agents.py +++ b/swarms/structs/mixture_of_agents.py @@ -86,9 +86,7 @@ class MixtureOfAgents: self.input_schema = MixtureOfAgentsInput( name=name, description=description, - agents=[ - agent.to_dict() for agent in self.agents - ], + agents=[agent.to_dict() for agent in self.agents], aggregator_agent=aggregator_agent.to_dict(), aggregator_system_prompt=self.aggregator_system_prompt, layers=self.layers, diff --git a/swarms/structs/multi_agent_exec.py b/swarms/structs/multi_agent_exec.py index d733f49f..b66af8a5 100644 --- a/swarms/structs/multi_agent_exec.py +++ b/swarms/structs/multi_agent_exec.py @@ -414,7 +414,7 @@ def run_agents_with_tasks_concurrently( List[Any]: A list of outputs from each agent execution. """ # Make the first agent not use the ifrs - + if no_clusterops: return _run_agents_with_tasks_concurrently( agents, tasks, batch_size, max_workers diff --git a/swarms/structs/rearrange.py b/swarms/structs/rearrange.py index 7c71bd04..a61efbee 100644 --- a/swarms/structs/rearrange.py +++ b/swarms/structs/rearrange.py @@ -121,16 +121,14 @@ class AgentRearrange(BaseSwarm): output_type: OutputType = "final", docs: List[str] = None, doc_folder: str = None, + device: str = "cpu", + device_id: int = 0, + all_cores: bool = False, + all_gpus: bool = True, + no_use_clusterops: bool = True, *args, **kwargs, ): - # reliability_check( - # agents=agents, - # name=name, - # description=description, - # flow=flow, - # max_loops=max_loops, - # ) super(AgentRearrange, self).__init__( name=name, description=description, @@ -150,6 +148,11 @@ class AgentRearrange(BaseSwarm): self.output_type = output_type self.docs = docs self.doc_folder = doc_folder + self.device = device + self.device_id = device_id + self.all_cores = all_cores + self.all_gpus = all_gpus + self.no_use_clusterops = no_use_clusterops self.swarm_history = { agent.agent_name: [] for agent in agents } @@ -506,7 +509,11 @@ class AgentRearrange(BaseSwarm): Returns: The result from executing the task through the cluster operations wrapper. """ - if no_use_clusterops: + no_use_clusterops = ( + no_use_clusterops or self.no_use_clusterops + ) + + if no_use_clusterops is True: return self._run( task=task, img=img, diff --git a/swarms/structs/sequential_workflow.py b/swarms/structs/sequential_workflow.py index cebcd7f0..ed55102d 100644 --- a/swarms/structs/sequential_workflow.py +++ b/swarms/structs/sequential_workflow.py @@ -107,7 +107,7 @@ class SequentialWorkflow: all_cores: bool = False, all_gpus: bool = False, device_id: int = 0, - no_use_clusterops: bool = False, + no_use_clusterops: bool = True, *args, **kwargs, ) -> str: diff --git a/swarms/tools/json_former.py b/swarms/tools/json_former.py index 01d608a5..dcca9932 100644 --- a/swarms/tools/json_former.py +++ b/swarms/tools/json_former.py @@ -1,7 +1,6 @@ import json from typing import Any, Dict, List, Union -from termcolor import cprint from transformers import PreTrainedModel, PreTrainedTokenizer from pydantic import BaseModel from swarms.tools.logits_processor import ( @@ -68,15 +67,6 @@ class Jsonformer: self.temperature = temperature self.max_string_token_length = max_string_token_length - def debug(self, caller: str, value: str, is_prompt: bool = False): - if self.debug_on: - if is_prompt: - cprint(caller, "green", end=" ") - cprint(value, "yellow") - else: - cprint(caller, "green", end=" ") - cprint(value, "blue") - def generate_number( self, temperature: Union[float, None] = None, iterations=0 ): diff --git a/swarms/tools/tool_utils.py b/swarms/tools/tool_utils.py index 9076e2d1..972f98ec 100644 --- a/swarms/tools/tool_utils.py +++ b/swarms/tools/tool_utils.py @@ -3,8 +3,7 @@ from typing import Any, List import inspect from typing import Callable - -from termcolor import colored +from swarms.utils.formatter import formatter def scrape_tool_func_docs(fn: Callable) -> str: @@ -37,17 +36,16 @@ def scrape_tool_func_docs(fn: Callable) -> str: f" {inspect.getdoc(fn)}\nParameters:\n{parameters_str}" ) except Exception as error: - print( - colored( - ( - f"Error scraping tool function docs {error} try" - " optimizing your inputs with different" - " variables and attempt once more." - ), - "red", - ) + error_msg = ( + formatter.print_panel( + f"Error scraping tool function docs {error} try" + " optimizing your inputs with different" + " variables and attempt once more." + ), ) + raise error + def tool_find_by_name(tool_name: str, tools: List[Any]): """Find the tool by name""" diff --git a/swarms/utils/callable_name.py b/swarms/utils/callable_name.py new file mode 100644 index 00000000..9a0b037f --- /dev/null +++ b/swarms/utils/callable_name.py @@ -0,0 +1,203 @@ +from typing import Any +import inspect +from functools import partial +import logging + + +class NameResolver: + """Utility class for resolving names of various objects""" + + @staticmethod + def get_name(obj: Any, default: str = "unnamed_callable") -> str: + """ + Get the name of any object with multiple fallback strategies. + + Args: + obj: The object to get the name from + default: Default name if all strategies fail + + Returns: + str: The resolved name + """ + strategies = [ + # Try getting __name__ attribute + lambda x: getattr(x, "__name__", None), + # Try getting class name + lambda x: ( + x.__class__.__name__ + if hasattr(x, "__class__") + else None + ), + # Try getting function name if it's a partial + lambda x: ( + x.func.__name__ if isinstance(x, partial) else None + ), + # Try getting the name from the class's type + lambda x: type(x).__name__, + # Try getting qualname + lambda x: getattr(x, "__qualname__", None), + # Try getting the module and class name + lambda x: ( + f"{x.__module__}.{x.__class__.__name__}" + if hasattr(x, "__module__") + else None + ), + # For async functions + lambda x: ( + x.__name__ if inspect.iscoroutinefunction(x) else None + ), + # For classes with custom __str__ + lambda x: ( + str(x) + if hasattr(x, "__str__") + and x.__str__ != object.__str__ + else None + ), + # For wrapped functions + lambda x: ( + getattr(x, "__wrapped__", None).__name__ + if hasattr(x, "__wrapped__") + else None + ), + ] + + # Try each strategy + for strategy in strategies: + try: + name = strategy(obj) + if name and isinstance(name, str): + return name.replace(" ", "_").replace("-", "_") + except Exception: + continue + + # Return default if all strategies fail + return default + + @staticmethod + def get_callable_details(obj: Any) -> dict: + """ + Get detailed information about a callable object. + + Returns: + dict: Dictionary containing: + - name: The resolved name + - type: The type of callable + - signature: The signature if available + - module: The module name if available + - doc: The docstring if available + """ + details = { + "name": NameResolver.get_name(obj), + "type": "unknown", + "signature": None, + "module": getattr(obj, "__module__", "unknown"), + "doc": inspect.getdoc(obj) + or "No documentation available", + } + + # Determine the type + if inspect.isclass(obj): + details["type"] = "class" + elif inspect.iscoroutinefunction(obj): + details["type"] = "async_function" + elif inspect.isfunction(obj): + details["type"] = "function" + elif isinstance(obj, partial): + details["type"] = "partial" + elif callable(obj): + details["type"] = "callable" + + # Try to get signature + try: + details["signature"] = str(inspect.signature(obj)) + except (ValueError, TypeError): + details["signature"] = "Unknown signature" + + return details + + @classmethod + def get_safe_name(cls, obj: Any, max_retries: int = 3) -> str: + """ + Safely get a name with retries and validation. + + Args: + obj: Object to get name from + max_retries: Maximum number of retry attempts + + Returns: + str: A valid name string + """ + retries = 0 + last_error = None + + while retries < max_retries: + try: + name = cls.get_name(obj) + + # Validate and clean the name + if name: + # Remove invalid characters + clean_name = "".join( + c + for c in name + if c.isalnum() or c in ["_", "."] + ) + + # Ensure it starts with a letter or underscore + if ( + not clean_name[0].isalpha() + and clean_name[0] != "_" + ): + clean_name = f"_{clean_name}" + + return clean_name + + except Exception as e: + last_error = e + retries += 1 + + # If all retries failed, generate a unique fallback name + import uuid + + fallback = f"callable_{uuid.uuid4().hex[:8]}" + logging.warning( + f"Failed to get name after {max_retries} retries. Using fallback: {fallback}. " + f"Last error: {str(last_error)}" + ) + return fallback + + +# # Example usage +# if __name__ == "__main__": +# def test_resolver(): +# # Test cases +# class TestClass: +# def method(self): +# pass + +# async def async_func(): +# pass + +# test_cases = [ +# TestClass, # Class +# TestClass(), # Instance +# async_func, # Async function +# lambda x: x, # Lambda +# partial(print, end=""), # Partial +# TestClass.method, # Method +# print, # Built-in function +# str, # Built-in class +# ] + +# resolver = NameResolver() + +# print("\nName Resolution Results:") +# print("-" * 50) +# for obj in test_cases: +# details = resolver.get_callable_details(obj) +# safe_name = resolver.get_safe_name(obj) +# print(f"\nObject: {obj}") +# print(f"Safe Name: {safe_name}") +# print(f"Details: {details}") + +# test_resolver() diff --git a/swarms/utils/markdown_message.py b/swarms/utils/markdown_message.py index a85cb4a1..03a35092 100644 --- a/swarms/utils/markdown_message.py +++ b/swarms/utils/markdown_message.py @@ -1,4 +1,4 @@ -from termcolor import colored +from swarms.utils.formatter import formatter def display_markdown_message(message: str, color: str = "cyan"): @@ -12,13 +12,10 @@ def display_markdown_message(message: str, color: str = "cyan"): if line == "": print() elif line == "---": - print(colored("-" * 50, color)) + formatter.print_panel("-" * 50) else: - print(colored(line, color)) + formatter.print_panel(line) if "\n" not in message and message.startswith(">"): # Aesthetic choice. For these tags, they need a space below them print() - - -# display_markdown_message("I love you and you are beautiful.", "cyan") From 96c50bb20c00f25e15fd702735661a1473d70e32 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 27 Nov 2024 12:20:25 -0800 Subject: [PATCH 02/13] [CLEANUP] --- .../rearrange_test.py => rearrange_test.py | 4 +- swarms/structs/agents_available.py | 160 ++++++++---------- swarms/structs/rearrange.py | 101 ++++++----- swarms/utils/update_agent_system_prompts.py | 53 ++++++ 4 files changed, 180 insertions(+), 138 deletions(-) rename new_features_examples/rearrange_test.py => rearrange_test.py (98%) create mode 100644 swarms/utils/update_agent_system_prompts.py diff --git a/new_features_examples/rearrange_test.py b/rearrange_test.py similarity index 98% rename from new_features_examples/rearrange_test.py rename to rearrange_test.py index ddfd7670..d85e435a 100644 --- a/new_features_examples/rearrange_test.py +++ b/rearrange_test.py @@ -95,12 +95,14 @@ flow = "BossAgent -> ExpenseAnalyzer -> SummaryGenerator" # Using AgentRearrange class to manage the swarm agent_system = AgentRearrange( + name="pe-swarm", + description="ss", agents=agents, flow=flow, return_json=False, output_type="final", max_loops=1, - docs=["SECURITY.md"], + # docs=["SECURITY.md"], ) # Input task for the swarm diff --git a/swarms/structs/agents_available.py b/swarms/structs/agents_available.py index f676877d..5651f9b0 100644 --- a/swarms/structs/agents_available.py +++ b/swarms/structs/agents_available.py @@ -1,109 +1,87 @@ -import os -from typing import List, Any from swarms.structs.agent import Agent -from loguru import logger -import uuid - -WORKSPACE_DIR = os.getenv("WORKSPACE_DIR") -uuid_for_log = str(uuid.uuid4()) -logger.add( - os.path.join( - WORKSPACE_DIR, - "agents_available", - f"agents-available-{uuid_for_log}.log", - ), - level="INFO", - colorize=True, - backtrace=True, - diagnose=True, -) - - -def get_agent_name(agent: Any) -> str: - """Helper function to safely get agent name - - Args: - agent (Any): The agent object to get name from - - Returns: - str: The agent's name if found, 'Unknown' otherwise - """ - if isinstance(agent, Agent) and hasattr(agent, "agent_name"): - return agent.agent_name - return "Unknown" - - -def get_agent_description(agent: Any) -> str: - """Helper function to get agent description or system prompt preview - - Args: - agent (Any): The agent object - - Returns: - str: Description or first 100 chars of system prompt - """ - if not isinstance(agent, Agent): - return "N/A" - - if hasattr(agent, "description") and agent.description: - return agent.description - - if hasattr(agent, "system_prompt") and agent.system_prompt: - return f"{agent.system_prompt[:150]}..." - - return "N/A" +from typing import List def showcase_available_agents( + agents: List[Agent], name: str = None, description: str = None, - agents: List[Agent] = [], - update_agents_on: bool = False, + format: str = "XML", ) -> str: """ - Generate a formatted string showcasing all available agents and their descriptions. + Format the available agents in either XML or Table format. Args: - agents (List[Agent]): List of Agent objects to showcase. - update_agents_on (bool, optional): If True, updates each agent's system prompt with - the showcase information. Defaults to False. + agents (List[Agent]): A list of agents to represent + name (str, optional): Name of the swarm + description (str, optional): Description of the swarm + format (str, optional): Output format ("XML" or "Table"). Defaults to "XML" Returns: - str: Formatted string containing agent information, including names, descriptions - and IDs for all available agents. + str: Formatted string containing agent information """ - logger.info(f"Showcasing {len(agents)} available agents") - - formatted_agents = [] - header = f"\n####### Agents available in the swarm: {name} ############\n" - header += f"{description}\n" - row_format = "{:<5} | {:<20} | {:<50}" - header_row = row_format.format("ID", "Agent Name", "Description") - separator = "-" * 80 - - formatted_agents.append(header) - formatted_agents.append(separator) - formatted_agents.append(header_row) - formatted_agents.append(separator) - - for idx, agent in enumerate(agents): - if not isinstance(agent, Agent): - logger.warning( - f"Skipping non-Agent object: {type(agent)}" - ) - continue - agent_name = get_agent_name(agent) - description = ( - get_agent_description(agent)[:100] + "..." - if len(get_agent_description(agent)) > 100 - else get_agent_description(agent) + def truncate(text: str, max_length: int = 130) -> str: + return ( + f"{text[:max_length]}..." + if len(text) > max_length + else text ) - formatted_agents.append( - row_format.format(idx + 1, agent_name, description) + output = [] + + if format.upper() == "TABLE": + output.append("\n| ID | Agent Name | Description |") + output.append("|-----|------------|-------------|") + for idx, agent in enumerate(agents): + if isinstance(agent, Agent): + agent_name = getattr(agent, "agent_name", str(agent)) + description = getattr( + agent, + "description", + getattr( + agent, "system_prompt", "Unknown description" + ), + ) + desc = truncate(description, 50) + output.append( + f"| {idx + 1} | {agent_name} | {desc} |" + ) + else: + output.append( + f"| {idx + 1} | {agent} | Unknown description |" + ) + return "\n".join(output) + + # Default XML format + output.append("") + if name: + output.append(f" {name}") + if description: + output.append( + f" {truncate(description)}" ) + for idx, agent in enumerate(agents): + output.append(f" ") + if isinstance(agent, Agent): + agent_name = getattr(agent, "agent_name", str(agent)) + description = getattr( + agent, + "description", + getattr( + agent, "system_prompt", "Unknown description" + ), + ) + output.append(f" {agent_name}") + output.append( + f" {truncate(description)}" + ) + else: + output.append(f" {agent}") + output.append( + " Unknown description" + ) + output.append(" ") + output.append("") - showcase = "\n".join(formatted_agents) - - return showcase + return "\n".join(output) diff --git a/swarms/structs/rearrange.py b/swarms/structs/rearrange.py index a61efbee..801861b0 100644 --- a/swarms/structs/rearrange.py +++ b/swarms/structs/rearrange.py @@ -1,5 +1,5 @@ -import traceback import asyncio +import traceback import uuid from concurrent.futures import ThreadPoolExecutor from datetime import datetime @@ -13,10 +13,10 @@ from swarms.structs.agent import Agent from swarms.structs.agents_available import showcase_available_agents from swarms.structs.base_swarm import BaseSwarm from swarms.utils.add_docs_to_agents import handle_input_docs +from swarms.utils.loguru_logger import initialize_logger from swarms.utils.wrapper_clusterop import ( exec_callable_with_clusterops, ) -from swarms.utils.loguru_logger import initialize_logger logger = initialize_logger(log_folder="rearrange") @@ -153,33 +153,6 @@ class AgentRearrange(BaseSwarm): self.all_cores = all_cores self.all_gpus = all_gpus self.no_use_clusterops = no_use_clusterops - self.swarm_history = { - agent.agent_name: [] for agent in agents - } - - self.id = uuid.uuid4().hex if id is None else id - - # Output schema - self.input_config = AgentRearrangeInput( - swarm_id=self.id, - name=self.name, - description=self.description, - flow=self.flow, - max_loops=self.max_loops, - output_type=self.output_type, - ) - - # Output schema - self.output_schema = AgentRearrangeOutput( - Input=self.input_config, - outputs=[], - ) - - # Run the reliability checks to validate the swarm - # self.handle_input_docs() - - # Show the agents whose in the swarm - # self.showcase_agents() def showcase_agents(self): # Get formatted agent info once @@ -187,12 +160,34 @@ class AgentRearrange(BaseSwarm): name=self.name, description=self.description, agents=self.agents, + format="Table", ) - # Update all agents in one pass using values() - for agent in self.agents.values(): - if isinstance(agent, Agent): - agent.system_prompt += agents_available + return agents_available + + def rearrange_prompt_prep(self) -> str: + """Prepares a formatted prompt describing the swarm configuration. + + Returns: + str: A formatted string containing the swarm's name, description, + flow pattern, and participating agents. + """ + agents_available = self.showcase_agents() + prompt = f""" + ===== Swarm Configuration ===== + + Name: {self.name} + Description: {self.description} + + ===== Execution Flow ===== + {self.flow} + + ===== Participating Agents ===== + {agents_available} + + =========================== + """ + return prompt def set_custom_flow(self, flow: str): self.flow = flow @@ -325,6 +320,7 @@ class AgentRearrange(BaseSwarm): current_task = task all_responses = [] response_dict = {} + previous_agent = None logger.info( f"Starting task execution with {len(tasks)} steps" @@ -349,12 +345,19 @@ class AgentRearrange(BaseSwarm): f"Starting loop {loop_count + 1}/{self.max_loops}" ) - for task in tasks: + for task_idx, task in enumerate(tasks): is_last = task == tasks[-1] agent_names = [ name.strip() for name in task.split(",") ] + # Prepare prompt with previous agent info + prompt_prefix = "" + if previous_agent and task_idx > 0: + prompt_prefix = f"Previous agent {previous_agent} output: {current_task}\n" + elif task_idx == 0: + prompt_prefix = "Initial task: " + if len(agent_names) > 1: # Parallel processing logger.info( @@ -370,12 +373,14 @@ class AgentRearrange(BaseSwarm): ): current_task = ( self.custom_human_in_the_loop( - current_task + prompt_prefix + + str(current_task) ) ) else: current_task = input( - "Enter your response:" + prompt_prefix + + "Enter your response: " ) results.append(current_task) response_dict[agent_name] = ( @@ -383,13 +388,13 @@ class AgentRearrange(BaseSwarm): ) else: agent = self.agents[agent_name] - current_task = ( - str(current_task) + task_with_context = ( + prompt_prefix + str(current_task) if current_task - else "" + else prompt_prefix ) result = agent.run( - task=current_task, + task=task_with_context, img=img, is_last=is_last, *args, @@ -407,6 +412,7 @@ class AgentRearrange(BaseSwarm): current_task = "; ".join(results) all_responses.extend(results) + previous_agent = ",".join(agent_names) else: # Sequential processing @@ -422,23 +428,25 @@ class AgentRearrange(BaseSwarm): ): current_task = ( self.custom_human_in_the_loop( - current_task + prompt_prefix + + str(current_task) ) ) else: current_task = input( - "Enter the next task: " + prompt_prefix + + "Enter the next task: " ) response_dict[agent_name] = current_task else: agent = self.agents[agent_name] - current_task = ( - str(current_task) + task_with_context = ( + prompt_prefix + str(current_task) if current_task - else "" + else prompt_prefix ) current_task = agent.run( - task=current_task, + task=task_with_context, img=img, is_last=is_last, *args, @@ -454,6 +462,7 @@ class AgentRearrange(BaseSwarm): ) all_responses.append(current_task) + previous_agent = agent_name loop_count += 1 diff --git a/swarms/utils/update_agent_system_prompts.py b/swarms/utils/update_agent_system_prompts.py new file mode 100644 index 00000000..e6f82426 --- /dev/null +++ b/swarms/utils/update_agent_system_prompts.py @@ -0,0 +1,53 @@ +import concurrent.futures +from typing import List, Union +from swarms.structs.agent import Agent + + +def update_system_prompts( + agents: List[Union[Agent, str]], + prompt: str, +) -> List[Agent]: + """ + Update system prompts for a list of agents concurrently. + + Args: + agents: List of Agent objects or strings to update + prompt: The prompt text to append to each agent's system prompt + + Returns: + List of updated Agent objects + """ + if not agents: + return agents + + def update_agent_prompt(agent: Union[Agent, str]) -> Agent: + # Convert string to Agent if needed + if isinstance(agent, str): + agent = Agent( + agent_name=agent, + system_prompt=prompt, # Initialize with the provided prompt + ) + else: + # Preserve existing prompt and append new one + existing_prompt = ( + agent.system_prompt if agent.system_prompt else "" + ) + agent.system_prompt = existing_prompt + "\n" + prompt + return agent + + # Use ThreadPoolExecutor for concurrent execution + max_workers = min(len(agents), 4) # Reasonable thread count + with concurrent.futures.ThreadPoolExecutor( + max_workers=max_workers + ) as executor: + futures = [] + for agent in agents: + future = executor.submit(update_agent_prompt, agent) + futures.append(future) + + # Collect results as they complete + updated_agents = [] + for future in concurrent.futures.as_completed(futures): + updated_agents.append(future.result()) + + return updated_agents From e6d616b2c3730bd9a95f30d81243812abbe12ae7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 27 Nov 2024 12:22:01 -0800 Subject: [PATCH 03/13] [CLEANUP] --- docs/mkdocs.yml | 38 ++-- multi_tool_usage_agent.py | 352 ++++++++++++++++++++++++++++++++++++++ swarms/__init__.py | 34 +++- 3 files changed, 399 insertions(+), 25 deletions(-) create mode 100644 multi_tool_usage_agent.py diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 23958829..53b4d273 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -57,29 +57,35 @@ extra: property: G-MPE9C65596 theme: - name: material - custom_dir: overrides - logo: assets/img/swarms-logo.png - palette: + name: material + custom_dir: overrides + logo: assets/img/swarms-logo.png + palette: - scheme: default - primary: black + primary: white # White background + accent: white # Black accents for interactive elements toggle: - icon: material/brightness-7 + icon: material/brightness-7 name: Switch to dark mode - # Palette toggle for dark mode - - scheme: slate + - scheme: slate # Optional: lighter shades for accessibility primary: black + accent: black toggle: icon: material/brightness-4 name: Switch to light mode - features: - - content.code.copy - - content.code.annotate - - navigation.tabs - - navigation.sections - - navigation.expand - - navigation.top - - announce.dismiss + features: + - content.code.copy + - content.code.annotate + - navigation.tabs + - navigation.sections + - navigation.expand + - navigation.top + - announce.dismiss + font: + text: "Fira Sans" # Clean and readable text + code: "Fira Code" # Modern look for code snippets + + # Extensions markdown_extensions: - abbr diff --git a/multi_tool_usage_agent.py b/multi_tool_usage_agent.py new file mode 100644 index 00000000..16f07cae --- /dev/null +++ b/multi_tool_usage_agent.py @@ -0,0 +1,352 @@ +from typing import List, Dict, Any, Optional, Callable +from dataclasses import dataclass +import json +from datetime import datetime +import inspect +import typing +from typing import Union +from swarms import Agent +from swarm_models import OpenAIChat +from dotenv import load_dotenv + + +@dataclass +class ToolDefinition: + name: str + description: str + parameters: Dict[str, Any] + required_params: List[str] + callable: Optional[Callable] = None + + +@dataclass +class ExecutionStep: + step_id: str + tool_name: str + parameters: Dict[str, Any] + purpose: str + depends_on: List[str] + completed: bool = False + result: Optional[Any] = None + + +def extract_type_hints(func: Callable) -> Dict[str, Any]: + """Extract parameter types from function type hints.""" + return typing.get_type_hints(func) + + +def extract_tool_info(func: Callable) -> ToolDefinition: + """Extract tool information from a callable function.""" + # Get function name + name = func.__name__ + + # Get docstring + description = inspect.getdoc(func) or "No description available" + + # Get parameters and their types + signature = inspect.signature(func) + type_hints = extract_type_hints(func) + + parameters = {} + required_params = [] + + for param_name, param in signature.parameters.items(): + # Skip self parameter for methods + if param_name == "self": + continue + + param_type = type_hints.get(param_name, Any) + + # Handle optional parameters + is_optional = ( + param.default != inspect.Parameter.empty + or getattr(param_type, "__origin__", None) is Union + and type(None) in param_type.__args__ + ) + + if not is_optional: + required_params.append(param_name) + + parameters[param_name] = { + "type": str(param_type), + "default": ( + None + if param.default is inspect.Parameter.empty + else param.default + ), + "required": not is_optional, + } + + return ToolDefinition( + name=name, + description=description, + parameters=parameters, + required_params=required_params, + callable=func, + ) + + +class ToolUsingAgent: + def __init__( + self, + tools: List[Callable], + openai_api_key: str, + model_name: str = "gpt-4", + temperature: float = 0.1, + max_loops: int = 10, + ): + # Convert callable tools to ToolDefinitions + self.available_tools = { + tool.__name__: extract_tool_info(tool) for tool in tools + } + + self.execution_plan: List[ExecutionStep] = [] + self.current_step_index = 0 + self.max_loops = max_loops + + # Initialize the OpenAI model + self.model = OpenAIChat( + openai_api_key=openai_api_key, + model_name=model_name, + temperature=temperature, + ) + + # Create system prompt with tool descriptions + self.system_prompt = self._create_system_prompt() + + self.agent = Agent( + agent_name="Tool-Using-Agent", + system_prompt=self.system_prompt, + llm=self.model, + max_loops=1, + autosave=True, + verbose=True, + saved_state_path="tool_agent_state.json", + context_length=200000, + ) + + def _create_system_prompt(self) -> str: + """Create system prompt with available tools information.""" + tools_description = [] + for tool_name, tool in self.available_tools.items(): + tools_description.append( + f""" + Tool: {tool_name} + Description: {tool.description} + Parameters: {json.dumps(tool.parameters, indent=2)} + Required Parameters: {tool.required_params} + """ + ) + + output = f"""You are an autonomous agent capable of executing complex tasks using available tools. + + Available Tools: + {chr(10).join(tools_description)} + + Follow these protocols: + 1. Create a detailed plan using available tools + 2. Execute each step in order + 3. Handle errors appropriately + 4. Maintain execution state + 5. Return results in structured format + + You must ALWAYS respond in the following JSON format: + {{ + "plan": {{ + "description": "Brief description of the overall plan", + "steps": [ + {{ + "step_number": 1, + "tool_name": "name_of_tool", + "description": "What this step accomplishes", + "parameters": {{ + "param1": "value1", + "param2": "value2" + }}, + "expected_output": "Description of expected output" + }} + ] + }}, + "reasoning": "Explanation of why this plan was chosen" + }} + + Before executing any tool: + 1. Validate all required parameters are present + 2. Verify parameter types match specifications + 3. Check parameter values are within valid ranges/formats + 4. Ensure logical dependencies between steps are met + + If any validation fails: + 1. Return error in JSON format with specific details + 2. Suggest corrections if possible + 3. Do not proceed with execution + + After each step execution: + 1. Verify output matches expected format + 2. Log results and any warnings/errors + 3. Update execution state + 4. Determine if plan adjustment needed + + Error Handling: + 1. Catch and classify all errors + 2. Provide detailed error messages + 3. Suggest recovery steps + 4. Maintain system stability + + The final output must be valid JSON that can be parsed. Always check your response can be parsed as JSON before returning. + """ + return output + + def execute_tool( + self, tool_name: str, parameters: Dict[str, Any] + ) -> Any: + """Execute a tool with given parameters.""" + tool = self.available_tools[tool_name] + if not tool.callable: + raise ValueError( + f"Tool {tool_name} has no associated callable" + ) + + # Convert parameters to appropriate types + converted_params = {} + for param_name, param_value in parameters.items(): + param_info = tool.parameters[param_name] + param_type = eval( + param_info["type"] + ) # Note: Be careful with eval + converted_params[param_name] = param_type(param_value) + + return tool.callable(**converted_params) + + def run(self, task: str) -> Dict[str, Any]: + """Execute the complete task with proper logging and error handling.""" + execution_log = { + "task": task, + "start_time": datetime.utcnow().isoformat(), + "steps": [], + "final_result": None + } + + try: + # Create and execute plan + plan_response = self.agent.run(f"Create a plan for: {task}") + plan_data = json.loads(plan_response) + + # Extract steps from the correct path in JSON + steps = plan_data["plan"]["steps"] # Changed from plan_data["steps"] + + for step in steps: + try: + # Check if parameters need default values + for param_name, param_value in step["parameters"].items(): + if isinstance(param_value, str) and not param_value.replace(".", "").isdigit(): + # If parameter is a description rather than a value, set default + if "income" in param_name.lower(): + step["parameters"][param_name] = 75000.0 + elif "year" in param_name.lower(): + step["parameters"][param_name] = 2024 + elif "investment" in param_name.lower(): + step["parameters"][param_name] = 1000.0 + + # Execute the tool + result = self.execute_tool( + step["tool_name"], + step["parameters"] + ) + + execution_log["steps"].append({ + "step_number": step["step_number"], + "tool": step["tool_name"], + "parameters": step["parameters"], + "success": True, + "result": result, + "description": step["description"] + }) + + except Exception as e: + execution_log["steps"].append({ + "step_number": step["step_number"], + "tool": step["tool_name"], + "parameters": step["parameters"], + "success": False, + "error": str(e), + "description": step["description"] + }) + print(f"Error executing step {step['step_number']}: {str(e)}") + # Continue with next step instead of raising + continue + + # Only mark as success if at least some steps succeeded + successful_steps = [s for s in execution_log["steps"] if s["success"]] + if successful_steps: + execution_log["final_result"] = { + "success": True, + "results": successful_steps, + "reasoning": plan_data.get("reasoning", "No reasoning provided") + } + else: + execution_log["final_result"] = { + "success": False, + "error": "No steps completed successfully", + "plan": plan_data + } + + except Exception as e: + execution_log["final_result"] = { + "success": False, + "error": str(e), + "plan": plan_data if 'plan_data' in locals() else None + } + + execution_log["end_time"] = datetime.utcnow().isoformat() + return execution_log + + +# Example usage +if __name__ == "__main__": + load_dotenv() + + # Example tool functions + def research_ira_requirements() -> Dict[str, Any]: + """Research and return ROTH IRA eligibility requirements.""" + return { + "age_requirement": "Must have earned income", + "income_limits": {"single": 144000, "married": 214000}, + } + + def calculate_contribution_limit( + income: float, tax_year: int + ) -> Dict[str, float]: + """Calculate maximum ROTH IRA contribution based on income and tax year.""" + base_limit = 6000 if tax_year <= 2022 else 6500 + if income > 144000: + return {"limit": 0} + return {"limit": base_limit} + + def find_brokers(min_investment: float) -> List[Dict[str, Any]]: + """Find suitable brokers for ROTH IRA based on minimum investment.""" + return [ + {"name": "Broker A", "min_investment": min_investment}, + { + "name": "Broker B", + "min_investment": min_investment * 1.5, + }, + ] + + # Initialize agent with tools + agent = ToolUsingAgent( + tools=[ + research_ira_requirements, + calculate_contribution_limit, + find_brokers, + ], + openai_api_key="", + ) + + # Run a task + result = agent.run( + "How can I establish a ROTH IRA to buy stocks and get a tax break? " + "What are the criteria?" + ) + + print(json.dumps(result, indent=2)) diff --git a/swarms/__init__.py b/swarms/__init__.py index 59fee672..d73b8439 100644 --- a/swarms/__init__.py +++ b/swarms/__init__.py @@ -1,22 +1,38 @@ +import os import concurrent.futures from dotenv import load_dotenv - -# from swarms.structs.workspace_manager import WorkspaceManager -# workspace_manager = WorkspaceManager() -# workspace_manager.run() +from loguru import logger load_dotenv() +# More reliable string comparison +if os.getenv("SWARMS_VERBOSE_GLOBAL", "True").lower() == "false": + logger.disable("") +# Import telemetry functions with error handling from swarms.telemetry.bootup import bootup # noqa: E402, F403 -from swarms.telemetry.sentry_active import ( +from swarms.telemetry.sentry_active import ( # noqa: E402 activate_sentry, ) # noqa: E402 -# Use ThreadPoolExecutor to run bootup and activate_sentry concurrently -with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: - executor.submit(bootup) - executor.submit(activate_sentry) + +# Run telemetry functions concurrently with error handling +def run_telemetry(): + try: + with concurrent.futures.ThreadPoolExecutor( + max_workers=2 + ) as executor: + future_bootup = executor.submit(bootup) + future_sentry = executor.submit(activate_sentry) + + # Wait for completion and check for exceptions + future_bootup.result() + future_sentry.result() + except Exception as e: + logger.error(f"Error running telemetry functions: {e}") + + +run_telemetry() from swarms.agents import * # noqa: E402, F403 from swarms.artifacts import * # noqa: E402, F403 From c2cd6775751d1d412b55d131d8bb60e274b19b6a Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 27 Nov 2024 16:51:09 -0800 Subject: [PATCH 04/13] [DOCS][CSS] --- docs/assets/css/extra.css | 38 +- ethchain_agent.py | 308 ++++++++++++++ ethereum_analysis.csv | 4 + multi_tool_usage_agent.py | 513 +++++++++++++----------- pyproject.toml | 2 +- swarms/__init__.py | 4 +- swarms/telemetry/auto_upgrade_swarms.py | 2 +- swarms/tools/tool_utils.py | 2 +- test.py | 292 ++++++++++++++ 9 files changed, 921 insertions(+), 244 deletions(-) create mode 100644 ethchain_agent.py create mode 100644 ethereum_analysis.csv create mode 100644 test.py diff --git a/docs/assets/css/extra.css b/docs/assets/css/extra.css index b639e2f7..d11f77f5 100644 --- a/docs/assets/css/extra.css +++ b/docs/assets/css/extra.css @@ -1,18 +1,26 @@ - -/* Further customization as needed */ +/* * Further customization as needed */ */ .md-typeset__table { - min-width: 100%; -} - -.md-typeset table:not([class]) { - display: table; -} - -/* -:root { - --md-primary-fg-color: #EE0F0F; - --md-primary-fg-color--light: #ECB7B7; - --md-primary-fg-color--dark: #90030C; - } */ \ No newline at end of file + min-width: 100%; + } + + .md-typeset table:not([class]) { + display: table; + } + + /* Dark mode */ + [data-md-color-scheme="slate"] { + --md-default-bg-color: black; + } + + .header__ellipsis { + color: black; + } + + /* + :root { + --md-primary-fg-color: #EE0F0F; + --md-primary-fg-color--light: #ECB7B7; + --md-primary-fg-color--dark: #90030C; + } */ \ No newline at end of file diff --git a/ethchain_agent.py b/ethchain_agent.py new file mode 100644 index 00000000..cc06aeb5 --- /dev/null +++ b/ethchain_agent.py @@ -0,0 +1,308 @@ +import os +from swarms import Agent +from swarm_models import OpenAIChat +from web3 import Web3 +from typing import Dict, Optional, Any +from datetime import datetime +import asyncio +from loguru import logger +from dotenv import load_dotenv +import csv +import requests +import time + +BLOCKCHAIN_AGENT_PROMPT = """ +You are an expert blockchain and cryptocurrency analyst with deep knowledge of Ethereum markets and DeFi ecosystems. +You have access to real-time ETH price data and transaction information. + +For each transaction, analyze: + +1. MARKET CONTEXT +- Current ETH price and what this transaction means in USD terms +- How this movement compares to typical market volumes +- Whether this could impact ETH price + +2. BEHAVIORAL ANALYSIS +- Whether this appears to be institutional, whale, or protocol movement +- If this fits any known wallet patterns or behaviors +- Signs of smart contract interaction or DeFi activity + +3. RISK & IMPLICATIONS +- Potential market impact or price influence +- Signs of potential market manipulation or unusual activity +- Protocol or DeFi risks if applicable + +4. STRATEGIC INSIGHTS +- What traders should know about this movement +- Potential chain reactions or follow-up effects +- Market opportunities or risks created + +Write naturally but precisely. Focus on actionable insights and important patterns. +Your analysis helps traders and researchers understand significant market movements in real-time.""" + + +class EthereumAnalyzer: + def __init__(self, min_value_eth: float = 100.0): + load_dotenv() + + logger.add( + "eth_analysis.log", + rotation="500 MB", + retention="10 days", + level="INFO", + format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message}", + ) + + self.w3 = Web3( + Web3.HTTPProvider( + "https://mainnet.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161" + ) + ) + if not self.w3.is_connected(): + raise ConnectionError( + "Failed to connect to Ethereum network" + ) + + self.min_value_eth = min_value_eth + self.last_processed_block = self.w3.eth.block_number + self.eth_price = self.get_eth_price() + self.last_price_update = time.time() + + # Initialize AI agent + api_key = os.getenv("OPENAI_API_KEY") + if not api_key: + raise ValueError( + "OpenAI API key not found in environment variables" + ) + + model = OpenAIChat( + openai_api_key=api_key, + model_name="gpt-4", + temperature=0.1, + ) + + self.agent = Agent( + agent_name="Ethereum-Analysis-Agent", + system_prompt=BLOCKCHAIN_AGENT_PROMPT, + llm=model, + max_loops=1, + autosave=True, + dashboard=False, + verbose=True, + dynamic_temperature_enabled=True, + saved_state_path="eth_agent.json", + user_name="eth_analyzer", + retry_attempts=1, + context_length=200000, + output_type="string", + streaming_on=False, + ) + + self.csv_filename = "ethereum_analysis.csv" + self.initialize_csv() + + def get_eth_price(self) -> float: + """Get current ETH price from CoinGecko API.""" + try: + response = requests.get( + "https://api.coingecko.com/api/v3/simple/price", + params={"ids": "ethereum", "vs_currencies": "usd"}, + ) + return float(response.json()["ethereum"]["usd"]) + except Exception as e: + logger.error(f"Error fetching ETH price: {str(e)}") + return 0.0 + + def update_eth_price(self): + """Update ETH price if more than 5 minutes have passed.""" + if time.time() - self.last_price_update > 300: # 5 minutes + self.eth_price = self.get_eth_price() + self.last_price_update = time.time() + logger.info(f"Updated ETH price: ${self.eth_price:,.2f}") + + def initialize_csv(self): + """Initialize CSV file with headers.""" + headers = [ + "timestamp", + "transaction_hash", + "from_address", + "to_address", + "value_eth", + "value_usd", + "eth_price", + "gas_used", + "gas_price_gwei", + "block_number", + "analysis", + ] + + if not os.path.exists(self.csv_filename): + with open(self.csv_filename, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(headers) + + async def analyze_transaction( + self, tx_hash: str + ) -> Optional[Dict[str, Any]]: + """Analyze a single transaction.""" + try: + tx = self.w3.eth.get_transaction(tx_hash) + receipt = self.w3.eth.get_transaction_receipt(tx_hash) + + value_eth = float(self.w3.from_wei(tx.value, "ether")) + + if value_eth < self.min_value_eth: + return None + + block = self.w3.eth.get_block(tx.blockNumber) + + # Update ETH price if needed + self.update_eth_price() + + value_usd = value_eth * self.eth_price + + analysis = { + "timestamp": datetime.fromtimestamp( + block.timestamp + ).isoformat(), + "transaction_hash": tx_hash.hex(), + "from_address": tx["from"], + "to_address": tx.to if tx.to else "Contract Creation", + "value_eth": value_eth, + "value_usd": value_usd, + "eth_price": self.eth_price, + "gas_used": receipt.gasUsed, + "gas_price_gwei": float( + self.w3.from_wei(tx.gasPrice, "gwei") + ), + "block_number": tx.blockNumber, + } + + # Check if it's a contract + if tx.to: + code = self.w3.eth.get_code(tx.to) + analysis["is_contract"] = len(code) > 0 + + # Get contract events + if analysis["is_contract"]: + analysis["events"] = receipt.logs + + return analysis + + except Exception as e: + logger.error( + f"Error analyzing transaction {tx_hash}: {str(e)}" + ) + return None + + def prepare_analysis_prompt(self, tx_data: Dict[str, Any]) -> str: + """Prepare detailed analysis prompt including price context.""" + value_usd = tx_data["value_usd"] + eth_price = tx_data["eth_price"] + + prompt = f"""Analyze this Ethereum transaction in current market context: + +Transaction Details: +- Value: {tx_data['value_eth']:.2f} ETH (${value_usd:,.2f} at current price) +- Current ETH Price: ${eth_price:,.2f} +- From: {tx_data['from_address']} +- To: {tx_data['to_address']} +- Contract Interaction: {tx_data.get('is_contract', False)} +- Gas Used: {tx_data['gas_used']:,} units +- Gas Price: {tx_data['gas_price_gwei']:.2f} Gwei +- Block: {tx_data['block_number']} +- Timestamp: {tx_data['timestamp']} + +{f"Event Count: {len(tx_data['events'])} events" if tx_data.get('events') else "No contract events"} + +Consider the transaction's significance given the current ETH price of ${eth_price:,.2f} and total USD value of ${value_usd:,.2f}. +Analyze market impact, patterns, risks, and strategic implications.""" + + return prompt + + def save_to_csv(self, tx_data: Dict[str, Any], ai_analysis: str): + """Save transaction data and analysis to CSV.""" + row = [ + tx_data["timestamp"], + tx_data["transaction_hash"], + tx_data["from_address"], + tx_data["to_address"], + tx_data["value_eth"], + tx_data["value_usd"], + tx_data["eth_price"], + tx_data["gas_used"], + tx_data["gas_price_gwei"], + tx_data["block_number"], + ai_analysis.replace("\n", " "), + ] + + with open(self.csv_filename, "a", newline="") as f: + writer = csv.writer(f) + writer.writerow(row) + + async def monitor_transactions(self): + """Monitor and analyze transactions one at a time.""" + logger.info( + f"Starting transaction monitor (minimum value: {self.min_value_eth} ETH)" + ) + + while True: + try: + current_block = self.w3.eth.block_number + block = self.w3.eth.get_block( + current_block, full_transactions=True + ) + + for tx in block.transactions: + tx_analysis = await self.analyze_transaction( + tx.hash + ) + + if tx_analysis: + # Get AI analysis + analysis_prompt = ( + self.prepare_analysis_prompt(tx_analysis) + ) + ai_analysis = self.agent.run(analysis_prompt) + print(ai_analysis) + + # Save to CSV + self.save_to_csv(tx_analysis, ai_analysis) + + # Print analysis + print("\n" + "=" * 50) + print("New Transaction Analysis") + print( + f"Hash: {tx_analysis['transaction_hash']}" + ) + print( + f"Value: {tx_analysis['value_eth']:.2f} ETH (${tx_analysis['value_usd']:,.2f})" + ) + print( + f"Current ETH Price: ${self.eth_price:,.2f}" + ) + print("=" * 50) + print(ai_analysis) + print("=" * 50 + "\n") + + await asyncio.sleep(1) # Wait for next block + + except Exception as e: + logger.error(f"Error in monitoring loop: {str(e)}") + await asyncio.sleep(1) + + +async def main(): + """Entry point for the analysis system.""" + analyzer = EthereumAnalyzer(min_value_eth=100.0) + await analyzer.monitor_transactions() + + +if __name__ == "__main__": + print("Starting Ethereum Transaction Analyzer...") + print("Saving results to ethereum_analysis.csv") + print("Press Ctrl+C to stop") + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("\nStopping analyzer...") diff --git a/ethereum_analysis.csv b/ethereum_analysis.csv new file mode 100644 index 00000000..703c4fbe --- /dev/null +++ b/ethereum_analysis.csv @@ -0,0 +1,4 @@ +timestamp,transaction_hash,from_address,to_address,value_eth,gas_used,gas_price_gwei,block_number,analysis +2024-11-27T13:50:35,ddbb665bc75fe848e7ce3d3ce1729243e92466c38ca407deccce8bf629987652,0x267be1C1D684F78cb4F6a176C4911b741E4Ffdc0,0xa40dFEE99E1C85DC97Fdc594b16A460717838703,3200.0,21000,19.968163737,21281878,"Transaction Analysis: This transaction represents a significant transfer of value in the Ethereum network with 3200 ETH (~$6.72 million USD at the current rate) moved from one address to another. It is essential to note that this transaction did not involve smart contract interaction, suggesting it could be a straightforward transfer of funds rather than part of a more complex operation. Looking at the broader market context, large transactions like this can potentially indicate major investment activities or redistribution of assets, which can have ripple effects in the market. If this transaction is part of a larger pattern of significant transfers, it could suggest substantial liquidity moving in the Ethereum ecosystem, possibly affecting the ETH prices. From a DeFi point of view, since there's no contract interaction, it's difficult to infer any direct implications. However, given the substantial value involved, it could be a step in preparation for involvement in DeFi protocols or a move from one DeFi platform to another by a large investor. The transaction fee paid, calculated from the given Gas Used and Gas Price, appears to be within reasonable range. This suggests that the transaction was not rushed and that the sender was willing to wait for this transaction to be confirmed, which might hint towards the non-urgent nature of the transaction. As for potential risk factors or security concerns, the transaction itself appears to be standard and doesn't raise any immediate red flags. However, the parties involved should always be cautious about the address security, maintaining privacy, and avoiding social engineering attacks. For traders and investors, this transaction can be interpreted as a potential bullish sign if it signifies increased liquidity and investment in the Ethereum market, especially if it's followed by similar large transfers. However, due to the anonymous nature of the transaction, it's critical to combine this with other market indicators and not to rely solely on transaction analysis for investment decisions." +2024-11-27T13:52:23,b98bcbf6d57a158b67a126d8f023766e03fb15c3e74becc1189d4244fda61a13,0xEae7380dD4CeF6fbD1144F49E4D1e6964258A4F4,0x28C6c06298d514Db089934071355E5743bf21d60,401.99463589018103,21000,14.978063737,21281887,"Ethereum-Analysis-Agent: Transaction Analysis: This transaction marks a significant transfer of 401.99 ETH, approximately $845,000 at the current rate. The transaction did not involve any smart contract interaction, suggesting a simple fund transfer rather than a complicated operation or interaction with a DeFi protocol. From a broader market perspective, this transaction is meaningful but not as potentially impactful as larger transactions. It can nonetheless be part of a larger pattern of asset movement within the Ethereum ecosystem. If this transaction is part of larger investment activities, it could suggest an increase in demand for ETH and potentially impact its price. Without contract interaction, it's challenging to assess direct implications for DeFi protocols. However, the substantial ETH transfer could suggest a step towards participation in DeFi activities, or a movement of funds between different DeFi platforms. The transaction fee appears reasonable, given the Gas Used and Gas Price. This implies that the transaction wasn't urgent, and the sender was willing to wait for the transaction to be confirmed, indicating a non-critical movement of funds. In terms of security and risk factors, there are no immediate concerns from the transaction itself. Nevertheless, as with any crypto transaction, the parties involved should ensure secure storage of their keys, maintain privacy, and be wary of potential phishing or social engineering attacks. For traders and investors, this transaction could be seen as a bullish sign if it forms part of a trend of increased investment activities in the Ethereum market. However, it's important to remember that transaction analysis should be combined with other market indicators due to the anonymous nature of blockchain transactions." +2024-11-27T13:59:47,a985b74fd3dfee09cbe4a2e6890509e583a3f0ce13f68c98e82996e0f66428be,0xf7858Da8a6617f7C6d0fF2bcAFDb6D2eeDF64840,0xA294cCa691e4C83B1fc0c8d63D9a3eeF0A196DE1,136.0668,494665.408728,3635.46,21000,18.866443971,21281923,"1. MARKET CONTEXT The transaction of 136.07 ETH, equivalent to $494,665.41, is a significant movement in the Ethereum market. However, compared to the daily trading volume of Ethereum, which often exceeds billions of dollars, this transaction is not large enough to significantly impact the ETH price on its own. 2. BEHAVIORAL ANALYSIS The transaction does not appear to be a protocol movement as there is no contract interaction involved. It could be a whale movement, given the substantial amount of ETH transferred. However, without additional information about the wallets involved, it's difficult to definitively determine the nature of the transaction. The gas price of 18.87 Gwei is relatively standard, suggesting that the transaction was not urgent or time-sensitive. 3. RISK & IMPLICATIONS The transaction does not show signs of market manipulation or unusual activity. The absence of contract interaction suggests that this transaction does not directly involve DeFi protocols, reducing the risk of smart contract vulnerabilities or DeFi-related risks. However, the large amount of ETH transferred could potentially influence market sentiment if it is part of a larger trend of similar transactions. 4. STRATEGIC INSIGHTS Traders should note this transaction as part of the broader market activity. While a single transaction of this size is unlikely to significantly impact the market, a series of similar transactions could indicate a larger trend. If this is part of a larger movement of ETH out of exchanges, it could suggest a decrease in selling pressure, which could be bullish for ETH. Conversely, if this is part of a larger movement into exchanges, it could indicate an increase in selling pressure, which could be bearish for ETH. Traders should monitor the market for further similar transactions to gain a better understanding of the potential market trends." diff --git a/multi_tool_usage_agent.py b/multi_tool_usage_agent.py index 16f07cae..44577528 100644 --- a/multi_tool_usage_agent.py +++ b/multi_tool_usage_agent.py @@ -1,5 +1,6 @@ +import os from typing import List, Dict, Any, Optional, Callable -from dataclasses import dataclass +from dataclasses import dataclass, field import json from datetime import datetime import inspect @@ -7,7 +8,6 @@ import typing from typing import Union from swarms import Agent from swarm_models import OpenAIChat -from dotenv import load_dotenv @dataclass @@ -19,17 +19,6 @@ class ToolDefinition: callable: Optional[Callable] = None -@dataclass -class ExecutionStep: - step_id: str - tool_name: str - parameters: Dict[str, Any] - purpose: str - depends_on: List[str] - completed: bool = False - result: Optional[Any] = None - - def extract_type_hints(func: Callable) -> Dict[str, Any]: """Extract parameter types from function type hints.""" return typing.get_type_hints(func) @@ -86,267 +75,343 @@ def extract_tool_info(func: Callable) -> ToolDefinition: ) -class ToolUsingAgent: +@dataclass +class FunctionSpec: + """Specification for a callable tool function.""" + + name: str + description: str + parameters: Dict[ + str, dict + ] # Contains type and description for each parameter + return_type: str + return_description: str + + +@dataclass +class ExecutionStep: + """Represents a single step in the execution plan.""" + + step_id: int + function_name: str + parameters: Dict[str, Any] + expected_output: str + completed: bool = False + result: Any = None + + +@dataclass +class ExecutionContext: + """Maintains state during execution.""" + + task: str + steps: List[ExecutionStep] = field(default_factory=list) + results: Dict[int, Any] = field(default_factory=dict) + current_step: int = 0 + history: List[Dict[str, Any]] = field(default_factory=list) + + +class ToolAgent: def __init__( self, - tools: List[Callable], + functions: List[Callable], openai_api_key: str, model_name: str = "gpt-4", temperature: float = 0.1, - max_loops: int = 10, ): - # Convert callable tools to ToolDefinitions - self.available_tools = { - tool.__name__: extract_tool_info(tool) for tool in tools - } + self.functions = {func.__name__: func for func in functions} + self.function_specs = self._analyze_functions(functions) - self.execution_plan: List[ExecutionStep] = [] - self.current_step_index = 0 - self.max_loops = max_loops - - # Initialize the OpenAI model self.model = OpenAIChat( openai_api_key=openai_api_key, model_name=model_name, temperature=temperature, ) - # Create system prompt with tool descriptions self.system_prompt = self._create_system_prompt() - self.agent = Agent( - agent_name="Tool-Using-Agent", + agent_name="Tool-Agent", system_prompt=self.system_prompt, llm=self.model, max_loops=1, - autosave=True, verbose=True, - saved_state_path="tool_agent_state.json", - context_length=200000, ) + def _analyze_functions( + self, functions: List[Callable] + ) -> Dict[str, FunctionSpec]: + """Analyze functions to create detailed specifications.""" + specs = {} + for func in functions: + hints = get_type_hints(func) + sig = inspect.signature(func) + doc = inspect.getdoc(func) or "" + + # Parse docstring for parameter descriptions + param_descriptions = {} + current_param = None + for line in doc.split("\n"): + if ":param" in line: + param_name = ( + line.split(":param")[1].split(":")[0].strip() + ) + desc = line.split(":", 2)[-1].strip() + param_descriptions[param_name] = desc + elif ":return:" in line: + return_desc = line.split(":return:")[1].strip() + + # Build parameter specifications + parameters = {} + for name, param in sig.parameters.items(): + param_type = hints.get(name, Any) + parameters[name] = { + "type": str(param_type), + "type_class": param_type, + "description": param_descriptions.get(name, ""), + "required": param.default == param.empty, + } + + specs[func.__name__] = FunctionSpec( + name=func.__name__, + description=doc.split("\n")[0], + parameters=parameters, + return_type=str(hints.get("return", Any)), + return_description=( + return_desc if "return_desc" in locals() else "" + ), + ) + + return specs + def _create_system_prompt(self) -> str: - """Create system prompt with available tools information.""" - tools_description = [] - for tool_name, tool in self.available_tools.items(): - tools_description.append( + """Create system prompt with detailed function specifications.""" + functions_desc = [] + for spec in self.function_specs.values(): + params_desc = [] + for name, details in spec.parameters.items(): + params_desc.append( + f" - {name}: {details['type']} - {details['description']}" + ) + + functions_desc.append( f""" - Tool: {tool_name} - Description: {tool.description} - Parameters: {json.dumps(tool.parameters, indent=2)} - Required Parameters: {tool.required_params} - """ +Function: {spec.name} +Description: {spec.description} +Parameters: +{chr(10).join(params_desc)} +Returns: {spec.return_type} - {spec.return_description} + """ ) - output = f"""You are an autonomous agent capable of executing complex tasks using available tools. - - Available Tools: - {chr(10).join(tools_description)} - - Follow these protocols: - 1. Create a detailed plan using available tools - 2. Execute each step in order - 3. Handle errors appropriately - 4. Maintain execution state - 5. Return results in structured format - - You must ALWAYS respond in the following JSON format: - {{ - "plan": {{ - "description": "Brief description of the overall plan", - "steps": [ - {{ - "step_number": 1, - "tool_name": "name_of_tool", - "description": "What this step accomplishes", - "parameters": {{ - "param1": "value1", - "param2": "value2" - }}, - "expected_output": "Description of expected output" - }} - ] - }}, - "reasoning": "Explanation of why this plan was chosen" - }} - - Before executing any tool: - 1. Validate all required parameters are present - 2. Verify parameter types match specifications - 3. Check parameter values are within valid ranges/formats - 4. Ensure logical dependencies between steps are met - - If any validation fails: - 1. Return error in JSON format with specific details - 2. Suggest corrections if possible - 3. Do not proceed with execution - - After each step execution: - 1. Verify output matches expected format - 2. Log results and any warnings/errors - 3. Update execution state - 4. Determine if plan adjustment needed - - Error Handling: - 1. Catch and classify all errors - 2. Provide detailed error messages - 3. Suggest recovery steps - 4. Maintain system stability - - The final output must be valid JSON that can be parsed. Always check your response can be parsed as JSON before returning. - """ - return output - - def execute_tool( - self, tool_name: str, parameters: Dict[str, Any] + return f"""You are an AI agent that creates and executes plans using available functions. + +Available Functions: +{chr(10).join(functions_desc)} + +You must respond in two formats depending on the phase: + +1. Planning Phase: +{{ + "phase": "planning", + "plan": {{ + "description": "Overall plan description", + "steps": [ + {{ + "step_id": 1, + "function": "function_name", + "parameters": {{ + "param1": "value1", + "param2": "value2" + }}, + "purpose": "Why this step is needed" + }} + ] + }} +}} + +2. Execution Phase: +{{ + "phase": "execution", + "analysis": "Analysis of current result", + "next_action": {{ + "type": "continue|request_input|complete", + "reason": "Why this action was chosen", + "needed_input": {{}} # If requesting input + }} +}} + +Always: +- Use exact function names +- Ensure parameter types match specifications +- Provide clear reasoning for each decision +""" + + def _execute_function( + self, spec: FunctionSpec, parameters: Dict[str, Any] ) -> Any: - """Execute a tool with given parameters.""" - tool = self.available_tools[tool_name] - if not tool.callable: - raise ValueError( - f"Tool {tool_name} has no associated callable" - ) - - # Convert parameters to appropriate types + """Execute a function with type checking.""" converted_params = {} - for param_name, param_value in parameters.items(): - param_info = tool.parameters[param_name] - param_type = eval( - param_info["type"] - ) # Note: Be careful with eval - converted_params[param_name] = param_type(param_value) - - return tool.callable(**converted_params) + for name, value in parameters.items(): + param_spec = spec.parameters[name] + try: + # Convert value to required type + param_type = param_spec["type_class"] + if param_type in (int, float, str, bool): + converted_params[name] = param_type(value) + else: + converted_params[name] = value + except (ValueError, TypeError) as e: + raise ValueError( + f"Parameter '{name}' conversion failed: {str(e)}" + ) + + return self.functions[spec.name](**converted_params) def run(self, task: str) -> Dict[str, Any]: - """Execute the complete task with proper logging and error handling.""" + """Execute task with planning and step-by-step execution.""" + context = ExecutionContext(task=task) execution_log = { "task": task, "start_time": datetime.utcnow().isoformat(), "steps": [], - "final_result": None + "final_result": None, } - + try: - # Create and execute plan - plan_response = self.agent.run(f"Create a plan for: {task}") - plan_data = json.loads(plan_response) - - # Extract steps from the correct path in JSON - steps = plan_data["plan"]["steps"] # Changed from plan_data["steps"] - - for step in steps: + # Planning phase + plan_prompt = f"Create a plan to: {task}" + plan_response = self.agent.run(plan_prompt) + plan_data = json.loads( + plan_response.replace("System:", "").strip() + ) + + # Convert plan to execution steps + for step in plan_data["plan"]["steps"]: + context.steps.append( + ExecutionStep( + step_id=step["step_id"], + function_name=step["function"], + parameters=step["parameters"], + expected_output=step["purpose"], + ) + ) + + # Execution phase + while context.current_step < len(context.steps): + step = context.steps[context.current_step] + print( + f"\nExecuting step {step.step_id}: {step.function_name}" + ) + try: - # Check if parameters need default values - for param_name, param_value in step["parameters"].items(): - if isinstance(param_value, str) and not param_value.replace(".", "").isdigit(): - # If parameter is a description rather than a value, set default - if "income" in param_name.lower(): - step["parameters"][param_name] = 75000.0 - elif "year" in param_name.lower(): - step["parameters"][param_name] = 2024 - elif "investment" in param_name.lower(): - step["parameters"][param_name] = 1000.0 - - # Execute the tool - result = self.execute_tool( - step["tool_name"], - step["parameters"] + # Execute function + spec = self.function_specs[step.function_name] + result = self._execute_function( + spec, step.parameters ) + context.results[step.step_id] = result + step.completed = True + step.result = result + + # Get agent's analysis + analysis_prompt = f""" + Step {step.step_id} completed: + Function: {step.function_name} + Result: {json.dumps(result)} + Remaining steps: {len(context.steps) - context.current_step - 1} - execution_log["steps"].append({ - "step_number": step["step_number"], - "tool": step["tool_name"], - "parameters": step["parameters"], - "success": True, - "result": result, - "description": step["description"] - }) - + Analyze the result and decide next action. + """ + + analysis_response = self.agent.run( + analysis_prompt + ) + analysis_data = json.loads( + analysis_response.replace( + "System:", "" + ).strip() + ) + + execution_log["steps"].append( + { + "step_id": step.step_id, + "function": step.function_name, + "parameters": step.parameters, + "result": result, + "analysis": analysis_data, + } + ) + + if ( + analysis_data["next_action"]["type"] + == "complete" + ): + if ( + context.current_step + < len(context.steps) - 1 + ): + continue + break + + context.current_step += 1 + except Exception as e: - execution_log["steps"].append({ - "step_number": step["step_number"], - "tool": step["tool_name"], - "parameters": step["parameters"], - "success": False, - "error": str(e), - "description": step["description"] - }) - print(f"Error executing step {step['step_number']}: {str(e)}") - # Continue with next step instead of raising - continue + print(f"Error in step {step.step_id}: {str(e)}") + execution_log["steps"].append( + { + "step_id": step.step_id, + "function": step.function_name, + "parameters": step.parameters, + "error": str(e), + } + ) + raise + + # Final analysis + final_prompt = f""" + Task completed. Results: + {json.dumps(context.results, indent=2)} - # Only mark as success if at least some steps succeeded - successful_steps = [s for s in execution_log["steps"] if s["success"]] - if successful_steps: - execution_log["final_result"] = { - "success": True, - "results": successful_steps, - "reasoning": plan_data.get("reasoning", "No reasoning provided") - } - else: - execution_log["final_result"] = { - "success": False, - "error": "No steps completed successfully", - "plan": plan_data - } - + Provide final analysis and recommendations. + """ + + final_analysis = self.agent.run(final_prompt) + execution_log["final_result"] = { + "success": True, + "results": context.results, + "analysis": json.loads( + final_analysis.replace("System:", "").strip() + ), + } + except Exception as e: execution_log["final_result"] = { "success": False, "error": str(e), - "plan": plan_data if 'plan_data' in locals() else None } - + execution_log["end_time"] = datetime.utcnow().isoformat() return execution_log -# Example usage -if __name__ == "__main__": - load_dotenv() - - # Example tool functions - def research_ira_requirements() -> Dict[str, Any]: - """Research and return ROTH IRA eligibility requirements.""" - return { - "age_requirement": "Must have earned income", - "income_limits": {"single": 144000, "married": 214000}, - } +def calculate_investment_return( + principal: float, rate: float, years: int +) -> float: + """Calculate investment return with compound interest. - def calculate_contribution_limit( - income: float, tax_year: int - ) -> Dict[str, float]: - """Calculate maximum ROTH IRA contribution based on income and tax year.""" - base_limit = 6000 if tax_year <= 2022 else 6500 - if income > 144000: - return {"limit": 0} - return {"limit": base_limit} - - def find_brokers(min_investment: float) -> List[Dict[str, Any]]: - """Find suitable brokers for ROTH IRA based on minimum investment.""" - return [ - {"name": "Broker A", "min_investment": min_investment}, - { - "name": "Broker B", - "min_investment": min_investment * 1.5, - }, - ] + :param principal: Initial investment amount in dollars + :param rate: Annual interest rate as decimal (e.g., 0.07 for 7%) + :param years: Number of years to invest + :return: Final investment value + """ + return principal * (1 + rate) ** years - # Initialize agent with tools - agent = ToolUsingAgent( - tools=[ - research_ira_requirements, - calculate_contribution_limit, - find_brokers, - ], - openai_api_key="", - ) - # Run a task - result = agent.run( - "How can I establish a ROTH IRA to buy stocks and get a tax break? " - "What are the criteria?" - ) +agent = ToolAgent( + functions=[calculate_investment_return], + openai_api_key=os.getenv("OPENAI_API_KEY"), +) - print(json.dumps(result, indent=2)) +result = agent.run( + "Calculate returns for $10000 invested at 7% for 10 years" +) diff --git a/pyproject.toml b/pyproject.toml index bb3d5043..51bb898f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "swarms" -version = "6.3.6" +version = "6.3.7" description = "Swarms - Pytorch" license = "MIT" authors = ["Kye Gomez "] diff --git a/swarms/__init__.py b/swarms/__init__.py index d73b8439..0c3b5ca5 100644 --- a/swarms/__init__.py +++ b/swarms/__init__.py @@ -5,8 +5,8 @@ from loguru import logger load_dotenv() -# More reliable string comparison -if os.getenv("SWARMS_VERBOSE_GLOBAL", "True").lower() == "false": +# Disable logging by default +if os.getenv("SWARMS_VERBOSE_GLOBAL", "False").lower() == "false": logger.disable("") # Import telemetry functions with error handling diff --git a/swarms/telemetry/auto_upgrade_swarms.py b/swarms/telemetry/auto_upgrade_swarms.py index 7203cd85..440f70ed 100644 --- a/swarms/telemetry/auto_upgrade_swarms.py +++ b/swarms/telemetry/auto_upgrade_swarms.py @@ -12,7 +12,7 @@ def auto_update(): try: # Check if auto-update is disabled auto_update_enabled = os.getenv( - "SWARMS_AUTOUPDATE_ON", "true" + "SWARMS_AUTOUPDATE_ON", "false" ).lower() if auto_update_enabled == "false": logger.info( diff --git a/swarms/tools/tool_utils.py b/swarms/tools/tool_utils.py index 972f98ec..b448d7a9 100644 --- a/swarms/tools/tool_utils.py +++ b/swarms/tools/tool_utils.py @@ -36,7 +36,7 @@ def scrape_tool_func_docs(fn: Callable) -> str: f" {inspect.getdoc(fn)}\nParameters:\n{parameters_str}" ) except Exception as error: - error_msg = ( + ( formatter.print_panel( f"Error scraping tool function docs {error} try" " optimizing your inputs with different" diff --git a/test.py b/test.py new file mode 100644 index 00000000..ce12ec1c --- /dev/null +++ b/test.py @@ -0,0 +1,292 @@ +import torch +import torch.nn as nn +import torch.distributed as dist +from dataclasses import dataclass +from typing import Optional, Tuple, Union +from loguru import logger +import math + + +@dataclass +class StarAttentionConfig: + """Configuration for StarAttention module. + + Attributes: + hidden_size: Dimension of the model's hidden states + num_attention_heads: Number of attention heads + num_hosts: Number of hosts in the distributed system + block_size: Size of each context block + anchor_size: Size of the anchor block + dropout_prob: Dropout probability (default: 0.1) + layer_norm_eps: Layer normalization epsilon (default: 1e-12) + """ + + hidden_size: int + num_attention_heads: int + num_hosts: int + block_size: int + anchor_size: int + dropout_prob: float = 0.1 + layer_norm_eps: float = 1e-12 + + +class StarAttention(nn.Module): + """ + Implementation of Star Attention mechanism for distributed inference. + + The module implements a two-phase attention mechanism: + 1. Local Context Encoding with Anchor Blocks + 2. Query Encoding and Output Generation with Global Attention + """ + + def __init__(self, config: StarAttentionConfig): + super().__init__() + + if config.hidden_size % config.num_attention_heads != 0: + raise ValueError( + f"Hidden size {config.hidden_size} not divisible by number of attention " + f"heads {config.num_attention_heads}" + ) + + self.config = config + self.head_dim = ( + config.hidden_size // config.num_attention_heads + ) + + # Initialize components + self.query = nn.Linear(config.hidden_size, config.hidden_size) + self.key = nn.Linear(config.hidden_size, config.hidden_size) + self.value = nn.Linear(config.hidden_size, config.hidden_size) + + self.dropout = nn.Dropout(config.dropout_prob) + self.layer_norm = nn.LayerNorm( + config.hidden_size, eps=config.layer_norm_eps + ) + + # KV cache for storing computed key/value pairs + self.kv_cache = {} + + logger.info( + f"Initialized StarAttention with config: {config}" + ) + + def _split_heads( + self, tensor: torch.Tensor, num_heads: int + ) -> torch.Tensor: + """Split the last dimension into (num_heads, head_dim).""" + batch_size, seq_len, _ = tensor.size() + tensor = tensor.view( + batch_size, seq_len, num_heads, self.head_dim + ) + # Transpose to (batch_size, num_heads, seq_len, head_dim) + return tensor.transpose(1, 2) + + def _merge_heads(self, tensor: torch.Tensor) -> torch.Tensor: + """Merge the head dimension back into hidden_size.""" + batch_size, _, seq_len, _ = tensor.size() + tensor = tensor.transpose(1, 2) + return tensor.reshape( + batch_size, seq_len, self.config.hidden_size + ) + + def _compute_attention_scores( + self, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + mask: Optional[torch.Tensor] = None, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Compute attention scores and weighted values.""" + # Scale dot-product attention + scores = torch.matmul( + query, key.transpose(-2, -1) + ) / math.sqrt(self.head_dim) + + if mask is not None: + scores = scores.masked_fill(mask == 0, float("-inf")) + + # Online softmax computation + attention_probs = torch.nn.functional.softmax(scores, dim=-1) + attention_probs = self.dropout(attention_probs) + + context = torch.matmul(attention_probs, value) + + return context, attention_probs + + def phase1_local_context_encoding( + self, + input_ids: torch.Tensor, + host_id: int, + device: Union[str, torch.device] = "cuda", + ) -> None: + """ + Phase 1: Local Context Encoding with Anchor Blocks + + Args: + input_ids: Input tensor of shape (batch_size, seq_len) + host_id: ID of the current host + device: Device to run computations on + """ + logger.debug(f"Starting Phase 1 on host {host_id}") + + # Calculate block assignments + block_start = host_id * self.config.block_size + block_end = block_start + self.config.block_size + + # Get local block + local_block = input_ids[:, block_start:block_end].to(device) + + # Get anchor block (first block) + anchor_block = input_ids[:, : self.config.anchor_size].to( + device + ) + + # Compute KV pairs for local block + local_hidden = self.layer_norm(local_block) + local_key = self._split_heads( + self.key(local_hidden), self.config.num_attention_heads + ) + local_value = self._split_heads( + self.value(local_hidden), self.config.num_attention_heads + ) + + # Store in KV cache + self.kv_cache[host_id] = { + "key": local_key, + "value": local_value, + "anchor_key": ( + None + if host_id == 0 + else self._split_heads( + self.key(self.layer_norm(anchor_block)), + self.config.num_attention_heads, + ) + ), + } + + logger.debug( + f"Phase 1 complete on host {host_id}. KV cache shapes - " + f"key: {local_key.shape}, value: {local_value.shape}" + ) + + def phase2_query_encoding( + self, + query_input: torch.Tensor, + host_id: int, + is_query_host: bool, + device: Union[str, torch.device] = "cuda", + ) -> Optional[torch.Tensor]: + """ + Phase 2: Query Encoding and Output Generation + + Args: + query_input: Query tensor of shape (batch_size, seq_len, hidden_size) + host_id: ID of the current host + is_query_host: Whether this host is the query host + device: Device to run computations on + + Returns: + Output tensor if this is the query host, None otherwise + """ + logger.debug(f"Starting Phase 2 on host {host_id}") + + # Transform query + query_hidden = self.layer_norm(query_input) + query = self._split_heads( + self.query(query_hidden), self.config.num_attention_heads + ) + + # Compute local attention scores + local_context, local_probs = self._compute_attention_scores( + query, + self.kv_cache[host_id]["key"], + self.kv_cache[host_id]["value"], + ) + + if not is_query_host: + # Non-query hosts send their local attention statistics + dist.send(local_probs, dst=self.config.num_hosts - 1) + return None + + # Query host aggregates attention from all hosts + all_attention_probs = [local_probs] + for src_rank in range(self.config.num_hosts - 1): + probs = torch.empty_like(local_probs) + dist.recv(probs, src=src_rank) + all_attention_probs.append(probs) + + # Compute global attention + torch.mean(torch.stack(all_attention_probs), dim=0) + + # Final output computation + output = self._merge_heads(local_context) + output = self.dropout(output) + + logger.debug( + f"Phase 2 complete on host {host_id}. Output shape: {output.shape}" + ) + + return output + + def forward( + self, + input_ids: torch.Tensor, + query_input: torch.Tensor, + host_id: int, + is_query_host: bool, + device: Union[str, torch.device] = "cuda", + ) -> Optional[torch.Tensor]: + """ + Forward pass of the StarAttention module. + + Args: + input_ids: Input tensor of shape (batch_size, seq_len) + query_input: Query tensor of shape (batch_size, seq_len, hidden_size) + host_id: ID of the current host + is_query_host: Whether this host is the query host + device: Device to run computations on + + Returns: + Output tensor if this is the query host, None otherwise + """ + # Phase 1: Local Context Encoding + self.phase1_local_context_encoding(input_ids, host_id, device) + + # Phase 2: Query Encoding and Output Generation + return self.phase2_query_encoding( + query_input, host_id, is_query_host, device + ) + + +# Example forward pass +config = StarAttentionConfig( + hidden_size=768, + num_attention_heads=12, + num_hosts=3, + block_size=512, + anchor_size=128, +) + +# Initialize model +model = StarAttention(config) + +# Example input tensors +batch_size = 4 +seq_len = 512 +input_ids = torch.randint( + 0, 1000, (batch_size, seq_len) +) # Random input IDs +query_input = torch.randn( + batch_size, seq_len, config.hidden_size +) # Random query input + +# Example forward pass for query host (host_id = 2) +output = model( + input_ids=input_ids, + query_input=query_input, + host_id=2, + is_query_host=True, + device="cpu", +) + +print(output) From 8b9e424dd5ed6c950fbecbac5132384caca7bff2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 27 Nov 2024 16:56:07 -0800 Subject: [PATCH 05/13] [DOCS][CLEANUP] --- docs/assets/css/extra.css | 45 ++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/docs/assets/css/extra.css b/docs/assets/css/extra.css index d11f77f5..8a8a758f 100644 --- a/docs/assets/css/extra.css +++ b/docs/assets/css/extra.css @@ -1,26 +1,27 @@ /* * Further customization as needed */ */ - .md-typeset__table { min-width: 100%; - } - - .md-typeset table:not([class]) { - display: table; - } - - /* Dark mode */ - [data-md-color-scheme="slate"] { - --md-default-bg-color: black; - } - - .header__ellipsis { - color: black; - } - - /* - :root { - --md-primary-fg-color: #EE0F0F; - --md-primary-fg-color--light: #ECB7B7; - --md-primary-fg-color--dark: #90030C; - } */ \ No newline at end of file +} + +.md-typeset table:not([class]) { + display: table; +} + +/* Dark mode */ +[data-md-color-scheme="slate"] { + --md-default-bg-color: black; +} + +.header__ellipsis { + color: black; +} + +.md-copyright__highlight { + color: black; +} + + +.md-header.md-header--shadow { + color: black; +} \ No newline at end of file From 7a66cbd70571ae49de9a144bf6d7d614e0acfe22 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 29 Nov 2024 10:49:38 -0800 Subject: [PATCH 06/13] [6.4.7] --- README.md | 24 +- ethereum_analysis.csv | 4 - graph_swarm.py | 666 ++++++++++ .../async_agents.py | 2 +- .../async_executor.py | 0 .../concurrent_mix.py | 0 .../dict_to_table.py | 0 .../ethchain_agent.py | 0 new_features_examples/microstructure.py | 1074 +++++++++++++++++ .../multi_tool_usage_agent.py | 0 .../rearrange_test.py | 0 no_llm_agent.py | 7 + pulsar_swarm.py | 276 +++++ pyproject.toml | 2 +- swarms/agents/auto_generate_swarm_config.py | 253 ++++ swarms/agents/create_agents_from_yaml.py | 396 +++--- swarms/cli/main.py | 450 ++++--- swarms/cli/onboarding_process.py | 13 - swarms/structs/agent.py | 36 +- .../structs/groupchat_new.py | 0 swarms/utils/litellm.py | 105 ++ swarms/utils/parse_code.py | 80 +- test.py | 292 ----- 23 files changed, 2992 insertions(+), 688 deletions(-) delete mode 100644 ethereum_analysis.csv create mode 100644 graph_swarm.py rename async_agents.py => new_features_examples/async_agents.py (95%) rename async_executor.py => new_features_examples/async_executor.py (100%) rename concurrent_mix.py => new_features_examples/concurrent_mix.py (100%) rename dict_to_table.py => new_features_examples/dict_to_table.py (100%) rename ethchain_agent.py => new_features_examples/ethchain_agent.py (100%) create mode 100644 new_features_examples/microstructure.py rename multi_tool_usage_agent.py => new_features_examples/multi_tool_usage_agent.py (100%) rename rearrange_test.py => new_features_examples/rearrange_test.py (100%) create mode 100644 no_llm_agent.py create mode 100644 pulsar_swarm.py create mode 100644 swarms/agents/auto_generate_swarm_config.py rename groupchat_new.py => swarms/structs/groupchat_new.py (100%) create mode 100644 swarms/utils/litellm.py delete mode 100644 test.py diff --git a/README.md b/README.md index f98f43ea..6469883d 100644 --- a/README.md +++ b/README.md @@ -113,15 +113,37 @@ Here are some example scripts to get you started. For more comprehensive documen | Swarms Examples | A collection of simple examples to demonstrate Swarms capabilities. | Basic Usage | [https://github.com/The-Swarm-Corporation/swarms-examples?tab=readme-ov-file](https://github.com/The-Swarm-Corporation/swarms-examples?tab=readme-ov-file) | | Cookbook | A comprehensive guide with recipes for various use cases and scenarios. | Advanced Usage | [https://github.com/The-Swarm-Corporation/Cookbook](https://github.com/The-Swarm-Corporation/Cookbook) | + + + --- ## `Agent` Class The `Agent` class is a fundamental component of the Swarms framework, designed to execute tasks autonomously. It fuses llms, tools and long-term memory capabilities to create a full stack agent. The `Agent` class is highly customizable, allowing for fine-grained control over its behavior and interactions. + ### `run` Method -The `run` method is the primary entry point for executing tasks with an `Agent` instance. It accepts a task string as the main input task and processes it according to the agent's configuration. And, it can also accept an `img` parameter such as `img="image_filepath.png` to process images if you have a VLM +The `run` method is the primary entry point for executing tasks with an `Agent` instance. It accepts a task string as the main input task and processes it according to the agent's configuration. And, it can also accept an `img` parameter such as `img="image_filepath.png` to process images if you have a VLM attached such as `GPT4VisionAPI` + +## Simple Example + +```python +from swarms import Agent + +agent = Agent( + agent_name="Stock-Analysis-Agent", + model_name="gpt-4o-mini", + max_loops="auto", + interactive=True, + streaming_on=True, +) + +agent.run("What is the current market trend for tech stocks?") + +``` + ### Settings and Customization The `Agent` class offers a range of settings to tailor its behavior to specific needs. Some key settings include: diff --git a/ethereum_analysis.csv b/ethereum_analysis.csv deleted file mode 100644 index 703c4fbe..00000000 --- a/ethereum_analysis.csv +++ /dev/null @@ -1,4 +0,0 @@ -timestamp,transaction_hash,from_address,to_address,value_eth,gas_used,gas_price_gwei,block_number,analysis -2024-11-27T13:50:35,ddbb665bc75fe848e7ce3d3ce1729243e92466c38ca407deccce8bf629987652,0x267be1C1D684F78cb4F6a176C4911b741E4Ffdc0,0xa40dFEE99E1C85DC97Fdc594b16A460717838703,3200.0,21000,19.968163737,21281878,"Transaction Analysis: This transaction represents a significant transfer of value in the Ethereum network with 3200 ETH (~$6.72 million USD at the current rate) moved from one address to another. It is essential to note that this transaction did not involve smart contract interaction, suggesting it could be a straightforward transfer of funds rather than part of a more complex operation. Looking at the broader market context, large transactions like this can potentially indicate major investment activities or redistribution of assets, which can have ripple effects in the market. If this transaction is part of a larger pattern of significant transfers, it could suggest substantial liquidity moving in the Ethereum ecosystem, possibly affecting the ETH prices. From a DeFi point of view, since there's no contract interaction, it's difficult to infer any direct implications. However, given the substantial value involved, it could be a step in preparation for involvement in DeFi protocols or a move from one DeFi platform to another by a large investor. The transaction fee paid, calculated from the given Gas Used and Gas Price, appears to be within reasonable range. This suggests that the transaction was not rushed and that the sender was willing to wait for this transaction to be confirmed, which might hint towards the non-urgent nature of the transaction. As for potential risk factors or security concerns, the transaction itself appears to be standard and doesn't raise any immediate red flags. However, the parties involved should always be cautious about the address security, maintaining privacy, and avoiding social engineering attacks. For traders and investors, this transaction can be interpreted as a potential bullish sign if it signifies increased liquidity and investment in the Ethereum market, especially if it's followed by similar large transfers. However, due to the anonymous nature of the transaction, it's critical to combine this with other market indicators and not to rely solely on transaction analysis for investment decisions." -2024-11-27T13:52:23,b98bcbf6d57a158b67a126d8f023766e03fb15c3e74becc1189d4244fda61a13,0xEae7380dD4CeF6fbD1144F49E4D1e6964258A4F4,0x28C6c06298d514Db089934071355E5743bf21d60,401.99463589018103,21000,14.978063737,21281887,"Ethereum-Analysis-Agent: Transaction Analysis: This transaction marks a significant transfer of 401.99 ETH, approximately $845,000 at the current rate. The transaction did not involve any smart contract interaction, suggesting a simple fund transfer rather than a complicated operation or interaction with a DeFi protocol. From a broader market perspective, this transaction is meaningful but not as potentially impactful as larger transactions. It can nonetheless be part of a larger pattern of asset movement within the Ethereum ecosystem. If this transaction is part of larger investment activities, it could suggest an increase in demand for ETH and potentially impact its price. Without contract interaction, it's challenging to assess direct implications for DeFi protocols. However, the substantial ETH transfer could suggest a step towards participation in DeFi activities, or a movement of funds between different DeFi platforms. The transaction fee appears reasonable, given the Gas Used and Gas Price. This implies that the transaction wasn't urgent, and the sender was willing to wait for the transaction to be confirmed, indicating a non-critical movement of funds. In terms of security and risk factors, there are no immediate concerns from the transaction itself. Nevertheless, as with any crypto transaction, the parties involved should ensure secure storage of their keys, maintain privacy, and be wary of potential phishing or social engineering attacks. For traders and investors, this transaction could be seen as a bullish sign if it forms part of a trend of increased investment activities in the Ethereum market. However, it's important to remember that transaction analysis should be combined with other market indicators due to the anonymous nature of blockchain transactions." -2024-11-27T13:59:47,a985b74fd3dfee09cbe4a2e6890509e583a3f0ce13f68c98e82996e0f66428be,0xf7858Da8a6617f7C6d0fF2bcAFDb6D2eeDF64840,0xA294cCa691e4C83B1fc0c8d63D9a3eeF0A196DE1,136.0668,494665.408728,3635.46,21000,18.866443971,21281923,"1. MARKET CONTEXT The transaction of 136.07 ETH, equivalent to $494,665.41, is a significant movement in the Ethereum market. However, compared to the daily trading volume of Ethereum, which often exceeds billions of dollars, this transaction is not large enough to significantly impact the ETH price on its own. 2. BEHAVIORAL ANALYSIS The transaction does not appear to be a protocol movement as there is no contract interaction involved. It could be a whale movement, given the substantial amount of ETH transferred. However, without additional information about the wallets involved, it's difficult to definitively determine the nature of the transaction. The gas price of 18.87 Gwei is relatively standard, suggesting that the transaction was not urgent or time-sensitive. 3. RISK & IMPLICATIONS The transaction does not show signs of market manipulation or unusual activity. The absence of contract interaction suggests that this transaction does not directly involve DeFi protocols, reducing the risk of smart contract vulnerabilities or DeFi-related risks. However, the large amount of ETH transferred could potentially influence market sentiment if it is part of a larger trend of similar transactions. 4. STRATEGIC INSIGHTS Traders should note this transaction as part of the broader market activity. While a single transaction of this size is unlikely to significantly impact the market, a series of similar transactions could indicate a larger trend. If this is part of a larger movement of ETH out of exchanges, it could suggest a decrease in selling pressure, which could be bullish for ETH. Conversely, if this is part of a larger movement into exchanges, it could indicate an increase in selling pressure, which could be bearish for ETH. Traders should monitor the market for further similar transactions to gain a better understanding of the potential market trends." diff --git a/graph_swarm.py b/graph_swarm.py new file mode 100644 index 00000000..31842225 --- /dev/null +++ b/graph_swarm.py @@ -0,0 +1,666 @@ +""" +GraphSwarm: A production-grade framework for orchestrating swarms of agents +Author: Claude +License: MIT +Version: 2.0.0 +""" + +import asyncio +import json +import time +from concurrent.futures import ThreadPoolExecutor +from datetime import datetime +from typing import Any, Dict, List, Optional, Tuple, Union + +import chromadb +import networkx as nx +from loguru import logger +from pydantic import BaseModel, Field + +from swarms import Agent + + +# Configure logging +logger.add( + "graphswarm.log", + rotation="500 MB", + retention="10 days", + level="INFO", + format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message}", +) + + +class AgentOutput(BaseModel): + """Structured output from an agent.""" + + agent_name: str + timestamp: float = Field(default_factory=time.time) + output: Any + execution_time: float + error: Optional[str] = None + metadata: Dict = Field(default_factory=dict) + + +class SwarmOutput(BaseModel): + """Structured output from the entire swarm.""" + + timestamp: float = Field(default_factory=time.time) + outputs: Dict[str, AgentOutput] + execution_time: float + success: bool + error: Optional[str] = None + metadata: Dict = Field(default_factory=dict) + + +class SwarmMemory: + """Vector-based memory system for GraphSwarm using ChromaDB.""" + + def __init__(self, collection_name: str = "swarm_memories"): + """Initialize SwarmMemory with ChromaDB.""" + self.client = chromadb.Client() + + # Get or create collection + self.collection = self.client.get_or_create_collection( + name=collection_name, + metadata={"description": "GraphSwarm execution memories"}, + ) + + def store_execution(self, task: str, result: SwarmOutput): + """Store execution results in vector memory.""" + try: + # Create metadata + metadata = { + "timestamp": datetime.now().isoformat(), + "success": result.success, + "execution_time": result.execution_time, + "agent_sequence": json.dumps( + [name for name in result.outputs.keys()] + ), + "error": result.error if result.error else "", + } + + # Create document from outputs + document = { + "task": task, + "outputs": json.dumps( + { + name: { + "output": str(output.output), + "execution_time": output.execution_time, + "error": output.error, + } + for name, output in result.outputs.items() + } + ), + } + + # Store in ChromaDB + self.collection.add( + documents=[json.dumps(document)], + metadatas=[metadata], + ids=[f"exec_{datetime.now().timestamp()}"], + ) + + print("added to database") + + logger.info(f"Stored execution in memory: {task}") + + except Exception as e: + logger.error( + f"Failed to store execution in memory: {str(e)}" + ) + + def get_similar_executions(self, task: str, limit: int = 5): + """Retrieve similar past executions.""" + try: + # Query ChromaDB for similar executions + results = self.collection.query( + query_texts=[task], + n_results=limit, + include=["documents", "metadatas"], + ) + + print(results) + + if not results["documents"]: + return [] + + # Process results + executions = [] + for doc, metadata in zip( + results["documents"][0], results["metadatas"][0] + ): + doc_dict = json.loads(doc) + executions.append( + { + "task": doc_dict["task"], + "outputs": json.loads(doc_dict["outputs"]), + "success": metadata["success"], + "execution_time": metadata["execution_time"], + "agent_sequence": json.loads( + metadata["agent_sequence"] + ), + "timestamp": metadata["timestamp"], + } + ) + + return executions + + except Exception as e: + logger.error( + f"Failed to retrieve similar executions: {str(e)}" + ) + return [] + + def get_optimal_sequence(self, task: str) -> Optional[List[str]]: + """Get the most successful agent sequence for similar tasks.""" + similar_executions = self.get_similar_executions(task) + print(f"similar_executions {similar_executions}") + + if not similar_executions: + return None + + # Sort by success and execution time + successful_execs = [ + ex for ex in similar_executions if ex["success"] + ] + + if not successful_execs: + return None + + # Return sequence from most successful execution + return successful_execs[0]["agent_sequence"] + + def clear_memory(self): + """Clear all memories.""" + self.client.delete_collection(self.collection.name) + self.collection = self.client.get_or_create_collection( + name=self.collection.name + ) + + +class GraphSwarm: + """ + Enhanced framework for creating and managing swarms of collaborative agents. + """ + + def __init__( + self, + agents: Union[ + List[Agent], List[Tuple[Agent, List[str]]], None + ] = None, + max_workers: Optional[int] = None, + swarm_name: str = "Collaborative Agent Swarm", + memory_collection: str = "swarm_memory", + ): + """Initialize GraphSwarm.""" + self.graph = nx.DiGraph() + self.agents: Dict[str, Agent] = {} + self.dependencies: Dict[str, List[str]] = {} + self.executor = ThreadPoolExecutor(max_workers=max_workers) + self.swarm_name = swarm_name + self.memory_collection = memory_collection + self.memory = SwarmMemory(collection_name=memory_collection) + + + if agents: + self.initialize_agents(agents) + + logger.info(f"Initialized GraphSwarm: {swarm_name}") + + def initialize_agents( + self, + agents: Union[List[Agent], List[Tuple[Agent, List[str]]]], + ): + """Initialize agents and their dependencies.""" + try: + # Handle list of Agents or (Agent, dependencies) tuples + for item in agents: + if isinstance(item, tuple): + agent, dependencies = item + else: + agent, dependencies = item, [] + + if not isinstance(agent, Agent): + raise ValueError( + f"Expected Agent object, got {type(agent)}" + ) + + self.agents[agent.agent_name] = agent + self.dependencies[agent.agent_name] = dependencies + self.graph.add_node(agent.agent_name, agent=agent) + + # Add dependencies + for dep in dependencies: + if dep not in self.agents: + raise ValueError( + f"Dependency {dep} not found for agent {agent.agent_name}" + ) + self.graph.add_edge(dep, agent.agent_name) + + self._validate_graph() + + except Exception as e: + logger.error(f"Failed to initialize agents: {str(e)}") + raise + + def _validate_graph(self): + """Validate the agent dependency graph.""" + if not self.graph.nodes(): + raise ValueError("No agents added to swarm") + + if not nx.is_directed_acyclic_graph(self.graph): + cycles = list(nx.simple_cycles(self.graph)) + raise ValueError( + f"Agent dependency graph contains cycles: {cycles}" + ) + + def _get_agent_role_description(self, agent_name: str) -> str: + """Generate a description of the agent's role in the swarm.""" + predecessors = list(self.graph.predecessors(agent_name)) + successors = list(self.graph.successors(agent_name)) + position = ( + "initial" + if not predecessors + else ("final" if not successors else "intermediate") + ) + + role = f"""You are {agent_name}, a specialized agent in the {self.swarm_name}. + Position: {position} agent in the workflow + + Your relationships:""" + + if predecessors: + role += ( + f"\nYou receive input from: {', '.join(predecessors)}" + ) + if successors: + role += f"\nYour output will be used by: {', '.join(successors)}" + + return role + + def _generate_workflow_context(self) -> str: + """Generate a description of the entire workflow.""" + execution_order = list(nx.topological_sort(self.graph)) + + workflow = f"""Workflow Overview of {self.swarm_name}: + + Processing Order: + {' -> '.join(execution_order)} + + Agent Roles: + """ + + for agent_name in execution_order: + predecessors = list(self.graph.predecessors(agent_name)) + successors = list(self.graph.successors(agent_name)) + + workflow += f"\n\n{agent_name}:" + if predecessors: + workflow += ( + f"\n- Receives from: {', '.join(predecessors)}" + ) + if successors: + workflow += f"\n- Sends to: {', '.join(successors)}" + if not predecessors and not successors: + workflow += "\n- Independent agent" + + return workflow + + def _build_agent_prompt( + self, agent_name: str, task: str, context: Dict = None + ) -> str: + """Build a comprehensive prompt for the agent including role and context.""" + prompt_parts = [ + self._get_agent_role_description(agent_name), + "\nWorkflow Context:", + self._generate_workflow_context(), + "\nYour Task:", + task, + ] + + if context: + prompt_parts.extend( + ["\nContext from Previous Agents:", str(context)] + ) + + prompt_parts.extend( + [ + "\nInstructions:", + "1. Process the task according to your role", + "2. Consider the input from previous agents when available", + "3. Provide clear, structured output", + "4. Remember that your output will be used by subsequent agents", + "\nResponse Guidelines:", + "- Provide clear, well-organized output", + "- Include relevant details and insights", + "- Highlight key findings", + "- Flag any uncertainties or issues", + ] + ) + + return "\n".join(prompt_parts) + + async def _execute_agent( + self, agent_name: str, task: str, context: Dict = None + ) -> AgentOutput: + """Execute a single agent.""" + start_time = time.time() + agent = self.agents[agent_name] + + try: + # Build comprehensive prompt + full_prompt = self._build_agent_prompt( + agent_name, task, context + ) + logger.debug(f"Prompt for {agent_name}:\n{full_prompt}") + + # Execute agent + output = await asyncio.to_thread(agent.run, full_prompt) + + return AgentOutput( + agent_name=agent_name, + output=output, + execution_time=time.time() - start_time, + metadata={ + "task": task, + "context": context, + "position_in_workflow": list( + nx.topological_sort(self.graph) + ).index(agent_name), + }, + ) + + except Exception as e: + logger.error( + f"Error executing agent {agent_name}: {str(e)}" + ) + return AgentOutput( + agent_name=agent_name, + output=None, + execution_time=time.time() - start_time, + error=str(e), + metadata={"task": task}, + ) + + async def execute(self, task: str) -> SwarmOutput: + """ + Execute the entire swarm of agents with memory integration. + + Args: + task: Initial task to execute + + Returns: + SwarmOutput: Structured output from all agents + """ + start_time = time.time() + outputs = {} + success = True + error = None + + try: + # Get similar past executions + similar_executions = self.memory.get_similar_executions( + task, limit=3 + ) + optimal_sequence = self.memory.get_optimal_sequence(task) + + # Get base execution order + base_execution_order = list( + nx.topological_sort(self.graph) + ) + + # Determine final execution order + if optimal_sequence and all( + agent in base_execution_order + for agent in optimal_sequence + ): + logger.info( + f"Using optimal sequence from memory: {optimal_sequence}" + ) + execution_order = optimal_sequence + else: + execution_order = base_execution_order + + # Get historical context if available + historical_context = {} + if similar_executions: + best_execution = similar_executions[0] + if best_execution["success"]: + historical_context = { + "similar_task": best_execution["task"], + "previous_outputs": best_execution["outputs"], + "execution_time": best_execution[ + "execution_time" + ], + "success_patterns": self._extract_success_patterns( + similar_executions + ), + } + + # Execute agents in order + for agent_name in execution_order: + try: + # Get context from dependencies and history + agent_context = { + "dependencies": { + dep: outputs[dep].output + for dep in self.graph.predecessors( + agent_name + ) + if dep in outputs + }, + "historical": historical_context, + "position": execution_order.index(agent_name), + "total_agents": len(execution_order), + } + + # Execute agent with enhanced context + output = await self._execute_agent( + agent_name, task, agent_context + ) + outputs[agent_name] = output + + # Update historical context with current execution + if output.output: + historical_context.update( + { + f"current_{agent_name}_output": output.output + } + ) + + # Check for errors + if output.error: + success = False + error = f"Agent {agent_name} failed: {output.error}" + + # Try to recover using memory + if similar_executions: + recovery_output = self._attempt_recovery( + agent_name, task, similar_executions + ) + if recovery_output: + outputs[agent_name] = recovery_output + success = True + error = None + continue + break + + except Exception as agent_error: + logger.error( + f"Error executing agent {agent_name}: {str(agent_error)}" + ) + success = False + error = f"Agent {agent_name} failed: {str(agent_error)}" + break + + # Create result + result = SwarmOutput( + outputs=outputs, + execution_time=time.time() - start_time, + success=success, + error=error, + metadata={ + "task": task, + "used_optimal_sequence": optimal_sequence + is not None, + "similar_executions_found": len( + similar_executions + ), + "execution_order": execution_order, + "historical_context_used": bool( + historical_context + ), + }, + ) + + # Store execution in memory + await self._store_execution_async(task, result) + + return result + + except Exception as e: + logger.error(f"Swarm execution failed: {str(e)}") + return SwarmOutput( + outputs=outputs, + execution_time=time.time() - start_time, + success=False, + error=str(e), + metadata={"task": task}, + ) + + def run(self, task: str) -> SwarmOutput: + """Synchronous interface to execute the swarm.""" + return asyncio.run(self.execute(task)) + + def _extract_success_patterns( + self, similar_executions: List[Dict] + ) -> Dict: + """Extract success patterns from similar executions.""" + patterns = {} + successful_execs = [ + ex for ex in similar_executions if ex["success"] + ] + + if successful_execs: + patterns = { + "common_sequences": self._find_common_sequences( + successful_execs + ), + "avg_execution_time": sum( + ex["execution_time"] for ex in successful_execs + ) + / len(successful_execs), + "successful_strategies": self._extract_strategies( + successful_execs + ), + } + + return patterns + + def _attempt_recovery( + self, + failed_agent: str, + task: str, + similar_executions: List[Dict], + ) -> Optional[AgentOutput]: + """Attempt to recover from failure using memory.""" + for execution in similar_executions: + if ( + execution["success"] + and failed_agent in execution["outputs"] + ): + historical_output = execution["outputs"][failed_agent] + + return AgentOutput( + agent_name=failed_agent, + output=historical_output["output"], + execution_time=historical_output[ + "execution_time" + ], + metadata={ + "recovered_from_memory": True, + "original_task": execution["task"], + }, + ) + return None + + async def _store_execution_async( + self, task: str, result: SwarmOutput + ): + """Asynchronously store execution in memory.""" + try: + await asyncio.to_thread( + self.memory.store_execution, task, result + ) + except Exception as e: + logger.error( + f"Failed to store execution in memory: {str(e)}" + ) + + def add_agent(self, agent: Agent, dependencies: List[str] = None): + """Add a new agent to the swarm.""" + dependencies = dependencies or [] + self.agents[agent.agent_name] = agent + self.dependencies[agent.agent_name] = dependencies + self.graph.add_node(agent.agent_name, agent=agent) + + for dep in dependencies: + if dep not in self.agents: + raise ValueError(f"Dependency {dep} not found") + self.graph.add_edge(dep, agent.agent_name) + + self._validate_graph() + + +if __name__ == "__main__": + try: + # Create agents + data_collector = Agent( + agent_name="Market-Data-Collector", + model_name="gpt-4o-mini", + max_loops=1, + streaming_on=True, + ) + + trend_analyzer = Agent( + agent_name="Market-Trend-Analyzer", + model_name="gpt-4o-mini", + max_loops=1, + streaming_on=True, + ) + + report_generator = Agent( + agent_name="Investment-Report-Generator", + model_name="gpt-4o-mini", + max_loops=1, + streaming_on=True, + ) + + # Create swarm + swarm = GraphSwarm( + agents=[ + (data_collector, []), + (trend_analyzer, ["Market-Data-Collector"]), + (report_generator, ["Market-Trend-Analyzer"]), + ], + swarm_name="Market Analysis Intelligence Network", + ) + + # Run the swarm + result = swarm.run( + "Analyze current market trends for tech stocks and provide investment recommendations" + ) + + # Print results + print(f"Execution success: {result.success}") + print(f"Total time: {result.execution_time:.2f} seconds") + + for agent_name, output in result.outputs.items(): + print(f"\nAgent: {agent_name}") + print(f"Output: {output.output}") + if output.error: + print(f"Error: {output.error}") + except Exception as error: + logger.error(error) + raise error diff --git a/async_agents.py b/new_features_examples/async_agents.py similarity index 95% rename from async_agents.py rename to new_features_examples/async_agents.py index 0d9353db..8734cd8a 100644 --- a/async_agents.py +++ b/new_features_examples/async_agents.py @@ -7,7 +7,7 @@ from swarms import Agent from swarms.prompts.finance_agent_sys_prompt import ( FINANCIAL_AGENT_SYS_PROMPT, ) -from async_executor import HighSpeedExecutor +from new_features_examples.async_executor import HighSpeedExecutor load_dotenv() diff --git a/async_executor.py b/new_features_examples/async_executor.py similarity index 100% rename from async_executor.py rename to new_features_examples/async_executor.py diff --git a/concurrent_mix.py b/new_features_examples/concurrent_mix.py similarity index 100% rename from concurrent_mix.py rename to new_features_examples/concurrent_mix.py diff --git a/dict_to_table.py b/new_features_examples/dict_to_table.py similarity index 100% rename from dict_to_table.py rename to new_features_examples/dict_to_table.py diff --git a/ethchain_agent.py b/new_features_examples/ethchain_agent.py similarity index 100% rename from ethchain_agent.py rename to new_features_examples/ethchain_agent.py diff --git a/new_features_examples/microstructure.py b/new_features_examples/microstructure.py new file mode 100644 index 00000000..c13d2e3f --- /dev/null +++ b/new_features_examples/microstructure.py @@ -0,0 +1,1074 @@ +import os +import threading +import time +from collections import deque +from dataclasses import dataclass +from datetime import datetime +from queue import Queue +from typing import Any, Dict, List, Optional, Tuple + +import ccxt +import numpy as np +import pandas as pd +from dotenv import load_dotenv +from loguru import logger +from scipy import stats +from swarm_models import OpenAIChat + +from swarms import Agent + +logger.enable("") + + +@dataclass +class MarketSignal: + timestamp: datetime + signal_type: str + source: str + data: Dict[str, Any] + confidence: float + metadata: Dict[str, Any] + + +class MarketDataBuffer: + def __init__(self, max_size: int = 10000): + self.max_size = max_size + self.data = deque(maxlen=max_size) + self.lock = threading.Lock() + + def add(self, item: Any) -> None: + with self.lock: + self.data.append(item) + + def get_latest(self, n: int = None) -> List[Any]: + with self.lock: + if n is None: + return list(self.data) + return list(self.data)[-n:] + + +class SignalCSVWriter: + def __init__(self, output_dir: str = "market_data"): + self.output_dir = output_dir + self.ensure_output_dir() + self.files = {} + + def ensure_output_dir(self): + if not os.path.exists(self.output_dir): + os.makedirs(self.output_dir) + + def get_filename(self, signal_type: str, symbol: str) -> str: + date_str = datetime.now().strftime("%Y%m%d") + return ( + f"{self.output_dir}/{signal_type}_{symbol}_{date_str}.csv" + ) + + def write_order_book_signal(self, signal: MarketSignal): + symbol = signal.data["symbol"] + metrics = signal.data["metrics"] + filename = self.get_filename("order_book", symbol) + + # Create header if file doesn't exist + if not os.path.exists(filename): + header = [ + "timestamp", + "symbol", + "bid_volume", + "ask_volume", + "mid_price", + "bid_vwap", + "ask_vwap", + "spread", + "depth_imbalance", + "confidence", + ] + with open(filename, "w") as f: + f.write(",".join(header) + "\n") + + # Write data + data = [ + str(signal.timestamp), + symbol, + str(metrics["bid_volume"]), + str(metrics["ask_volume"]), + str(metrics["mid_price"]), + str(metrics["bid_vwap"]), + str(metrics["ask_vwap"]), + str(metrics["spread"]), + str(metrics["depth_imbalance"]), + str(signal.confidence), + ] + + with open(filename, "a") as f: + f.write(",".join(data) + "\n") + + def write_tick_signal(self, signal: MarketSignal): + symbol = signal.data["symbol"] + metrics = signal.data["metrics"] + filename = self.get_filename("tick_data", symbol) + + if not os.path.exists(filename): + header = [ + "timestamp", + "symbol", + "vwap", + "price_momentum", + "volume_mean", + "trade_intensity", + "kyle_lambda", + "roll_spread", + "confidence", + ] + with open(filename, "w") as f: + f.write(",".join(header) + "\n") + + data = [ + str(signal.timestamp), + symbol, + str(metrics["vwap"]), + str(metrics["price_momentum"]), + str(metrics["volume_mean"]), + str(metrics["trade_intensity"]), + str(metrics["kyle_lambda"]), + str(metrics["roll_spread"]), + str(signal.confidence), + ] + + with open(filename, "a") as f: + f.write(",".join(data) + "\n") + + def write_arbitrage_signal(self, signal: MarketSignal): + if ( + "best_opportunity" not in signal.data + or not signal.data["best_opportunity"] + ): + return + + symbol = signal.data["symbol"] + opp = signal.data["best_opportunity"] + filename = self.get_filename("arbitrage", symbol) + + if not os.path.exists(filename): + header = [ + "timestamp", + "symbol", + "buy_venue", + "sell_venue", + "spread", + "return", + "buy_price", + "sell_price", + "confidence", + ] + with open(filename, "w") as f: + f.write(",".join(header) + "\n") + + data = [ + str(signal.timestamp), + symbol, + opp["buy_venue"], + opp["sell_venue"], + str(opp["spread"]), + str(opp["return"]), + str(opp["buy_price"]), + str(opp["sell_price"]), + str(signal.confidence), + ] + + with open(filename, "a") as f: + f.write(",".join(data) + "\n") + + +class ExchangeManager: + def __init__(self): + self.available_exchanges = { + "kraken": ccxt.kraken, + "coinbase": ccxt.coinbase, + "kucoin": ccxt.kucoin, + "bitfinex": ccxt.bitfinex, + "gemini": ccxt.gemini, + } + self.active_exchanges = {} + self.test_exchanges() + + def test_exchanges(self): + """Test each exchange and keep only the accessible ones""" + for name, exchange_class in self.available_exchanges.items(): + try: + exchange = exchange_class() + exchange.load_markets() + self.active_exchanges[name] = exchange + logger.info(f"Successfully connected to {name}") + except Exception as e: + logger.warning(f"Could not connect to {name}: {e}") + + def get_primary_exchange(self) -> Optional[ccxt.Exchange]: + """Get the first available exchange""" + if not self.active_exchanges: + raise RuntimeError("No exchanges available") + return next(iter(self.active_exchanges.values())) + + def get_all_active_exchanges(self) -> Dict[str, ccxt.Exchange]: + """Get all active exchanges""" + return self.active_exchanges + + +class BaseMarketAgent(Agent): + def __init__( + self, + agent_name: str, + system_prompt: str, + api_key: str, + model_name: str = "gpt-4-0125-preview", + temperature: float = 0.1, + ): + model = OpenAIChat( + openai_api_key=api_key, + model_name=model_name, + temperature=temperature, + ) + super().__init__( + agent_name=agent_name, + system_prompt=system_prompt, + llm=model, + max_loops=1, + autosave=True, + dashboard=False, + verbose=True, + dynamic_temperature_enabled=True, + context_length=200000, + streaming_on=True, + output_type="str", + ) + self.signal_queue = Queue() + self.is_running = False + self.last_update = datetime.now() + self.update_interval = 1.0 # seconds + + def rate_limit_check(self) -> bool: + current_time = datetime.now() + if ( + current_time - self.last_update + ).total_seconds() < self.update_interval: + return False + self.last_update = current_time + return True + + +class OrderBookAgent(BaseMarketAgent): + def __init__(self, api_key: str): + system_prompt = """ + You are an Order Book Analysis Agent specialized in detecting institutional flows. + Monitor order book depth and changes to identify potential large trades and institutional activity. + Analyze patterns in order placement and cancellation rates. + """ + super().__init__("OrderBookAgent", system_prompt, api_key) + exchange_manager = ExchangeManager() + self.exchange = exchange_manager.get_primary_exchange() + self.order_book_buffer = MarketDataBuffer(max_size=100) + self.vwap_window = 20 + + def calculate_order_book_metrics( + self, order_book: Dict + ) -> Dict[str, float]: + bids = np.array(order_book["bids"]) + asks = np.array(order_book["asks"]) + + # Calculate key metrics + bid_volume = np.sum(bids[:, 1]) + ask_volume = np.sum(asks[:, 1]) + mid_price = (bids[0][0] + asks[0][0]) / 2 + + # Calculate VWAP + bid_vwap = ( + np.sum( + bids[: self.vwap_window, 0] + * bids[: self.vwap_window, 1] + ) + / bid_volume + if bid_volume > 0 + else 0 + ) + ask_vwap = ( + np.sum( + asks[: self.vwap_window, 0] + * asks[: self.vwap_window, 1] + ) + / ask_volume + if ask_volume > 0 + else 0 + ) + + # Calculate order book slope + bid_slope = np.polyfit( + range(len(bids[:10])), bids[:10, 0], 1 + )[0] + ask_slope = np.polyfit( + range(len(asks[:10])), asks[:10, 0], 1 + )[0] + + return { + "bid_volume": bid_volume, + "ask_volume": ask_volume, + "mid_price": mid_price, + "bid_vwap": bid_vwap, + "ask_vwap": ask_vwap, + "bid_slope": bid_slope, + "ask_slope": ask_slope, + "spread": asks[0][0] - bids[0][0], + "depth_imbalance": (bid_volume - ask_volume) + / (bid_volume + ask_volume), + } + + def detect_large_orders( + self, metrics: Dict[str, float], threshold: float = 2.0 + ) -> bool: + historical_books = self.order_book_buffer.get_latest(20) + if not historical_books: + return False + + # Calculate historical volume statistics + hist_volumes = [ + book["bid_volume"] + book["ask_volume"] + for book in historical_books + ] + volume_mean = np.mean(hist_volumes) + volume_std = np.std(hist_volumes) + + current_volume = metrics["bid_volume"] + metrics["ask_volume"] + z_score = (current_volume - volume_mean) / ( + volume_std if volume_std > 0 else 1 + ) + + return abs(z_score) > threshold + + def analyze_order_book(self, symbol: str) -> MarketSignal: + if not self.rate_limit_check(): + return None + + try: + order_book = self.exchange.fetch_order_book( + symbol, limit=100 + ) + metrics = self.calculate_order_book_metrics(order_book) + self.order_book_buffer.add(metrics) + + # Format data for LLM analysis + analysis_prompt = f""" + Analyze this order book for {symbol}: + Bid Volume: {metrics['bid_volume']} + Ask Volume: {metrics['ask_volume']} + Mid Price: {metrics['mid_price']} + Spread: {metrics['spread']} + Depth Imbalance: {metrics['depth_imbalance']} + + What patterns do you see? Is there evidence of institutional activity? + Are there any significant imbalances that could lead to price movement? + """ + + # Get LLM analysis + llm_analysis = self.run(analysis_prompt) + + # Original signal creation with added LLM analysis + return MarketSignal( + timestamp=datetime.now(), + signal_type="order_book_analysis", + source="OrderBookAgent", + data={ + "metrics": metrics, + "large_order_detected": self.detect_large_orders( + metrics + ), + "symbol": symbol, + "llm_analysis": llm_analysis, # Add LLM insights + }, + confidence=min( + abs(metrics["depth_imbalance"]) * 0.7 + + ( + 1.0 + if self.detect_large_orders(metrics) + else 0.0 + ) + * 0.3, + 1.0, + ), + metadata={ + "update_latency": ( + datetime.now() - self.last_update + ).total_seconds(), + "buffer_size": len( + self.order_book_buffer.get_latest() + ), + }, + ) + except Exception as e: + logger.error(f"Error in order book analysis: {str(e)}") + return None + + +class TickDataAgent(BaseMarketAgent): + def __init__(self, api_key: str): + system_prompt = """ + You are a Tick Data Analysis Agent specialized in analyzing high-frequency price movements. + Monitor tick-by-tick data for patterns indicating short-term price direction. + Analyze trade size distribution and execution speed. + """ + super().__init__("TickDataAgent", system_prompt, api_key) + self.tick_buffer = MarketDataBuffer(max_size=5000) + exchange_manager = ExchangeManager() + self.exchange = exchange_manager.get_primary_exchange() + + def calculate_tick_metrics( + self, ticks: List[Dict] + ) -> Dict[str, float]: + df = pd.DataFrame(ticks) + df["price"] = pd.to_numeric(df["price"]) + df["volume"] = pd.to_numeric(df["amount"]) + + # Calculate key metrics + metrics = {} + + # Volume-weighted average price (VWAP) + metrics["vwap"] = (df["price"] * df["volume"]).sum() / df[ + "volume" + ].sum() + + # Price momentum + metrics["price_momentum"] = df["price"].diff().mean() + + # Volume profile + metrics["volume_mean"] = df["volume"].mean() + metrics["volume_std"] = df["volume"].std() + + # Trade intensity + time_diff = ( + df["timestamp"].max() - df["timestamp"].min() + ) / 1000 # Convert to seconds + metrics["trade_intensity"] = ( + len(df) / time_diff if time_diff > 0 else 0 + ) + + # Microstructure indicators + metrics["kyle_lambda"] = self.calculate_kyle_lambda(df) + metrics["roll_spread"] = self.calculate_roll_spread(df) + + return metrics + + def calculate_kyle_lambda(self, df: pd.DataFrame) -> float: + """Calculate Kyle's Lambda (price impact coefficient)""" + try: + price_changes = df["price"].diff().dropna() + volume_changes = df["volume"].diff().dropna() + + if len(price_changes) > 1 and len(volume_changes) > 1: + slope, _, _, _, _ = stats.linregress( + volume_changes, price_changes + ) + return abs(slope) + except Exception as e: + logger.warning(f"Error calculating Kyle's Lambda: {e}") + return 0.0 + + def calculate_roll_spread(self, df: pd.DataFrame) -> float: + """Calculate Roll's implied spread""" + try: + price_changes = df["price"].diff().dropna() + if len(price_changes) > 1: + autocov = np.cov( + price_changes[:-1], price_changes[1:] + )[0][1] + return 2 * np.sqrt(-autocov) if autocov < 0 else 0.0 + except Exception as e: + logger.warning(f"Error calculating Roll spread: {e}") + return 0.0 + + def calculate_tick_metrics( + self, ticks: List[Dict] + ) -> Dict[str, float]: + try: + # Debug the incoming data structure + logger.info( + f"Raw tick data structure: {ticks[0] if ticks else 'No ticks'}" + ) + + # Convert trades to proper format + formatted_trades = [] + for trade in ticks: + formatted_trade = { + "price": float( + trade.get("price", trade.get("last", 0)) + ), # Handle different exchange formats + "amount": float( + trade.get( + "amount", + trade.get( + "size", trade.get("quantity", 0) + ), + ) + ), + "timestamp": trade.get( + "timestamp", int(time.time() * 1000) + ), + } + formatted_trades.append(formatted_trade) + + df = pd.DataFrame(formatted_trades) + + if df.empty: + logger.warning("No valid trades to analyze") + return { + "vwap": 0.0, + "price_momentum": 0.0, + "volume_mean": 0.0, + "volume_std": 0.0, + "trade_intensity": 0.0, + "kyle_lambda": 0.0, + "roll_spread": 0.0, + } + + # Calculate metrics with the properly formatted data + metrics = {} + metrics["vwap"] = ( + (df["price"] * df["amount"]).sum() + / df["amount"].sum() + if not df.empty + else 0 + ) + metrics["price_momentum"] = ( + df["price"].diff().mean() if len(df) > 1 else 0 + ) + metrics["volume_mean"] = df["amount"].mean() + metrics["volume_std"] = df["amount"].std() + + time_diff = ( + (df["timestamp"].max() - df["timestamp"].min()) / 1000 + if len(df) > 1 + else 1 + ) + metrics["trade_intensity"] = ( + len(df) / time_diff if time_diff > 0 else 0 + ) + + metrics["kyle_lambda"] = self.calculate_kyle_lambda(df) + metrics["roll_spread"] = self.calculate_roll_spread(df) + + logger.info(f"Calculated metrics: {metrics}") + return metrics + + except Exception as e: + logger.error( + f"Error in calculate_tick_metrics: {str(e)}", + exc_info=True, + ) + # Return default metrics on error + return { + "vwap": 0.0, + "price_momentum": 0.0, + "volume_mean": 0.0, + "volume_std": 0.0, + "trade_intensity": 0.0, + "kyle_lambda": 0.0, + "roll_spread": 0.0, + } + + def analyze_ticks(self, symbol: str) -> MarketSignal: + if not self.rate_limit_check(): + return None + + try: + # Fetch recent trades + trades = self.exchange.fetch_trades(symbol, limit=100) + + # Debug the raw trades data + logger.info(f"Fetched {len(trades)} trades for {symbol}") + if trades: + logger.info(f"Sample trade: {trades[0]}") + + self.tick_buffer.add(trades) + recent_ticks = self.tick_buffer.get_latest(1000) + metrics = self.calculate_tick_metrics(recent_ticks) + + # Only proceed with LLM analysis if we have valid metrics + if metrics["vwap"] > 0: + analysis_prompt = f""" + Analyze these trading patterns for {symbol}: + VWAP: {metrics['vwap']:.2f} + Price Momentum: {metrics['price_momentum']:.2f} + Trade Intensity: {metrics['trade_intensity']:.2f} + Kyle's Lambda: {metrics['kyle_lambda']:.2f} + + What does this tell us about: + 1. Current market sentiment + 2. Potential price direction + 3. Trading activity patterns + """ + llm_analysis = self.run(analysis_prompt) + else: + llm_analysis = "Insufficient data for analysis" + + return MarketSignal( + timestamp=datetime.now(), + signal_type="tick_analysis", + source="TickDataAgent", + data={ + "metrics": metrics, + "symbol": symbol, + "prediction": np.sign(metrics["price_momentum"]), + "llm_analysis": llm_analysis, + }, + confidence=min(metrics["trade_intensity"] / 100, 1.0) + * 0.4 + + min(metrics["kyle_lambda"], 1.0) * 0.6, + metadata={ + "update_latency": ( + datetime.now() - self.last_update + ).total_seconds(), + "buffer_size": len(self.tick_buffer.get_latest()), + }, + ) + + except Exception as e: + logger.error( + f"Error in tick analysis: {str(e)}", exc_info=True + ) + return None + + +class LatencyArbitrageAgent(BaseMarketAgent): + def __init__(self, api_key: str): + system_prompt = """ + You are a Latency Arbitrage Agent specialized in detecting price discrepancies across venues. + Monitor multiple exchanges for price differences exceeding transaction costs. + Calculate optimal trade sizes and routes. + """ + super().__init__( + "LatencyArbitrageAgent", system_prompt, api_key + ) + exchange_manager = ExchangeManager() + self.exchanges = exchange_manager.get_all_active_exchanges() + self.fee_structure = { + "kraken": 0.0026, # 0.26% taker fee + "coinbase": 0.006, # 0.6% taker fee + "kucoin": 0.001, # 0.1% taker fee + "bitfinex": 0.002, # 0.2% taker fee + "gemini": 0.003, # 0.3% taker fee + } + self.price_buffer = { + ex: MarketDataBuffer(max_size=100) + for ex in self.exchanges + } + + def calculate_effective_prices( + self, ticker: Dict, venue: str + ) -> Tuple[float, float]: + """Calculate effective prices including fees""" + fee = self.fee_structure[venue] + return ( + ticker["bid"] * (1 - fee), # Effective sell price + ticker["ask"] * (1 + fee), # Effective buy price + ) + + def calculate_arbitrage_metrics( + self, prices: Dict[str, Dict] + ) -> Dict: + opportunities = [] + + for venue1 in prices: + for venue2 in prices: + if venue1 != venue2: + sell_price, _ = self.calculate_effective_prices( + prices[venue1], venue1 + ) + _, buy_price = self.calculate_effective_prices( + prices[venue2], venue2 + ) + + spread = sell_price - buy_price + if spread > 0: + opportunities.append( + { + "sell_venue": venue1, + "buy_venue": venue2, + "spread": spread, + "return": spread / buy_price, + "buy_price": buy_price, + "sell_price": sell_price, + } + ) + + return { + "opportunities": opportunities, + "best_opportunity": ( + max(opportunities, key=lambda x: x["return"]) + if opportunities + else None + ), + } + + def find_arbitrage(self, symbol: str) -> MarketSignal: + """ + Find arbitrage opportunities across exchanges with LLM analysis + """ + if not self.rate_limit_check(): + return None + + try: + prices = {} + timestamps = {} + + for name, exchange in self.exchanges.items(): + try: + ticker = exchange.fetch_ticker(symbol) + prices[name] = { + "bid": ticker["bid"], + "ask": ticker["ask"], + } + timestamps[name] = ticker["timestamp"] + self.price_buffer[name].add(prices[name]) + except Exception as e: + logger.warning( + f"Error fetching {name} price: {e}" + ) + + if len(prices) < 2: + return None + + metrics = self.calculate_arbitrage_metrics(prices) + + if not metrics["best_opportunity"]: + return None + + # Calculate confidence based on spread and timing + opp = metrics["best_opportunity"] + timing_factor = 1.0 - min( + abs( + timestamps[opp["sell_venue"]] + - timestamps[opp["buy_venue"]] + ) + / 1000, + 1.0, + ) + spread_factor = min( + opp["return"] * 5, 1.0 + ) # Scale return to confidence + + confidence = timing_factor * 0.4 + spread_factor * 0.6 + + # Format price data for LLM analysis + price_summary = "\n".join( + [ + f"{venue}: Bid ${prices[venue]['bid']:.2f}, Ask ${prices[venue]['ask']:.2f}" + for venue in prices.keys() + ] + ) + + # Create detailed analysis prompt + analysis_prompt = f""" + Analyze this arbitrage opportunity for {symbol}: + + Current Prices: + {price_summary} + + Best Opportunity Found: + Buy Venue: {opp['buy_venue']} at ${opp['buy_price']:.2f} + Sell Venue: {opp['sell_venue']} at ${opp['sell_price']:.2f} + Spread: ${opp['spread']:.2f} + Expected Return: {opp['return']*100:.3f}% + Time Difference: {abs(timestamps[opp['sell_venue']] - timestamps[opp['buy_venue']])}ms + + Consider: + 1. Is this opportunity likely to be profitable after execution costs? + 2. What risks might prevent successful execution? + 3. What market conditions might have created this opportunity? + 4. How does the timing difference affect execution probability? + """ + + # Get LLM analysis + llm_analysis = self.run(analysis_prompt) + + # Create comprehensive signal + return MarketSignal( + timestamp=datetime.now(), + signal_type="arbitrage_opportunity", + source="LatencyArbitrageAgent", + data={ + "metrics": metrics, + "symbol": symbol, + "best_opportunity": metrics["best_opportunity"], + "all_prices": prices, + "llm_analysis": llm_analysis, + "timing": { + "time_difference_ms": abs( + timestamps[opp["sell_venue"]] + - timestamps[opp["buy_venue"]] + ), + "timestamps": timestamps, + }, + }, + confidence=confidence, + metadata={ + "update_latency": ( + datetime.now() - self.last_update + ).total_seconds(), + "timestamp_deltas": timestamps, + "venue_count": len(prices), + "execution_risk": 1.0 + - timing_factor, # Higher time difference = higher risk + }, + ) + + except Exception as e: + logger.error(f"Error in arbitrage analysis: {str(e)}") + return None + + +class SwarmCoordinator: + def __init__(self, api_key: str): + self.api_key = api_key + self.agents = { + "order_book": OrderBookAgent(api_key), + "tick_data": TickDataAgent(api_key), + "latency_arb": LatencyArbitrageAgent(api_key), + } + self.signal_processors = [] + self.signal_history = MarketDataBuffer(max_size=1000) + self.running = False + self.lock = threading.Lock() + self.csv_writer = SignalCSVWriter() + + def register_signal_processor(self, processor): + """Register a new signal processor function""" + with self.lock: + self.signal_processors.append(processor) + + def process_signals(self, signals: List[MarketSignal]): + """Process signals through all registered processors""" + if not signals: + return + + self.signal_history.add(signals) + + try: + for processor in self.signal_processors: + processor(signals) + except Exception as e: + logger.error(f"Error in signal processing: {e}") + + def aggregate_signals( + self, signals: List[MarketSignal] + ) -> Dict[str, Any]: + """Aggregate multiple signals into a combined market view""" + if not signals: + return {} + + self.signal_history.add(signals) + + aggregated = { + "timestamp": datetime.now(), + "symbols": set(), + "agent_signals": {}, + "combined_confidence": 0, + "market_state": {}, + } + + for signal in signals: + symbol = signal.data.get("symbol") + if symbol: + aggregated["symbols"].add(symbol) + + agent_type = signal.source + if agent_type not in aggregated["agent_signals"]: + aggregated["agent_signals"][agent_type] = [] + aggregated["agent_signals"][agent_type].append(signal) + + # Update market state based on signal type + if signal.signal_type == "order_book_analysis": + metrics = signal.data.get("metrics", {}) + aggregated["market_state"].update( + { + "order_book_imbalance": metrics.get( + "depth_imbalance" + ), + "spread": metrics.get("spread"), + "large_orders_detected": signal.data.get( + "large_order_detected" + ), + } + ) + elif signal.signal_type == "tick_analysis": + metrics = signal.data.get("metrics", {}) + aggregated["market_state"].update( + { + "price_momentum": metrics.get( + "price_momentum" + ), + "trade_intensity": metrics.get( + "trade_intensity" + ), + "kyle_lambda": metrics.get("kyle_lambda"), + } + ) + elif signal.signal_type == "arbitrage_opportunity": + opp = signal.data.get("best_opportunity") + if opp: + aggregated["market_state"].update( + { + "arbitrage_spread": opp.get("spread"), + "arbitrage_return": opp.get("return"), + } + ) + + # Calculate combined confidence as weighted average + confidences = [s.confidence for s in signals] + if confidences: + aggregated["combined_confidence"] = np.mean(confidences) + + return aggregated + + def start(self, symbols: List[str], interval: float = 1.0): + """Start the swarm monitoring system""" + if self.running: + logger.warning("Swarm is already running") + return + + self.running = True + + def agent_loop(agent, symbol): + while self.running: + try: + if isinstance(agent, OrderBookAgent): + signal = agent.analyze_order_book(symbol) + elif isinstance(agent, TickDataAgent): + signal = agent.analyze_ticks(symbol) + elif isinstance(agent, LatencyArbitrageAgent): + signal = agent.find_arbitrage(symbol) + + if signal: + agent.signal_queue.put(signal) + except Exception as e: + logger.error( + f"Error in {agent.agent_name} loop: {e}" + ) + + time.sleep(interval) + + def signal_collection_loop(): + while self.running: + try: + current_signals = [] + + # Collect signals from all agents + for agent in self.agents.values(): + while not agent.signal_queue.empty(): + signal = agent.signal_queue.get_nowait() + if signal: + current_signals.append(signal) + + if current_signals: + # Process current signals + self.process_signals(current_signals) + + # Aggregate and analyze + aggregated = self.aggregate_signals( + current_signals + ) + logger.info( + f"Aggregated market view: {aggregated}" + ) + + except Exception as e: + logger.error( + f"Error in signal collection loop: {e}" + ) + + time.sleep(interval) + + # Start agent threads + self.threads = [] + for symbol in symbols: + for agent in self.agents.values(): + thread = threading.Thread( + target=agent_loop, + args=(agent, symbol), + daemon=True, + ) + thread.start() + self.threads.append(thread) + + # Start signal collection thread + collection_thread = threading.Thread( + target=signal_collection_loop, daemon=True + ) + collection_thread.start() + self.threads.append(collection_thread) + + def stop(self): + """Stop the swarm monitoring system""" + self.running = False + for thread in self.threads: + thread.join(timeout=5.0) + logger.info("Swarm stopped") + + +def market_making_processor(signals: List[MarketSignal]): + """Enhanced signal processor with LLM analysis integration""" + for signal in signals: + if signal.confidence > 0.8: + if signal.signal_type == "arbitrage_opportunity": + opp = signal.data.get("best_opportunity") + if ( + opp and opp["return"] > 0.001 + ): # 0.1% return threshold + logger.info( + "\nSignificant arbitrage opportunity detected:" + ) + logger.info(f"Return: {opp['return']*100:.3f}%") + logger.info(f"Spread: ${opp['spread']:.2f}") + if "llm_analysis" in signal.data: + logger.info("\nLLM Analysis:") + logger.info(signal.data["llm_analysis"]) + + elif signal.signal_type == "order_book_analysis": + imbalance = signal.data["metrics"]["depth_imbalance"] + if abs(imbalance) > 0.3: + logger.info( + f"\nSignificant order book imbalance detected: {imbalance:.3f}" + ) + if "llm_analysis" in signal.data: + logger.info("\nLLM Analysis:") + logger.info(signal.data["llm_analysis"]) + + elif signal.signal_type == "tick_analysis": + momentum = signal.data["metrics"]["price_momentum"] + if abs(momentum) > 0: + logger.info( + f"\nSignificant price momentum detected: {momentum:.3f}" + ) + if "llm_analysis" in signal.data: + logger.info("\nLLM Analysis:") + logger.info(signal.data["llm_analysis"]) + + +load_dotenv() +api_key = os.getenv("OPENAI_API_KEY") + +coordinator = SwarmCoordinator(api_key) +coordinator.register_signal_processor(market_making_processor) + +symbols = ["BTC/USDT", "ETH/USDT"] + +logger.info( + "Starting market microstructure analysis with LLM integration..." +) +logger.info(f"Monitoring symbols: {symbols}") +logger.info( + f"CSV files will be written to: {os.path.abspath('market_data')}" +) + +try: + coordinator.start(symbols) + while True: + time.sleep(1) +except KeyboardInterrupt: + logger.info("Gracefully shutting down...") + coordinator.stop() diff --git a/multi_tool_usage_agent.py b/new_features_examples/multi_tool_usage_agent.py similarity index 100% rename from multi_tool_usage_agent.py rename to new_features_examples/multi_tool_usage_agent.py diff --git a/rearrange_test.py b/new_features_examples/rearrange_test.py similarity index 100% rename from rearrange_test.py rename to new_features_examples/rearrange_test.py diff --git a/no_llm_agent.py b/no_llm_agent.py new file mode 100644 index 00000000..2fcbb8f9 --- /dev/null +++ b/no_llm_agent.py @@ -0,0 +1,7 @@ +from swarms import Agent + +Agent( + agent_name="Stock-Analysis-Agent", + model_name="gpt-4o-mini", + max_loops=1, +).run("What are 5 hft algorithms") diff --git a/pulsar_swarm.py b/pulsar_swarm.py new file mode 100644 index 00000000..2d8961f7 --- /dev/null +++ b/pulsar_swarm.py @@ -0,0 +1,276 @@ +import asyncio +import pulsar + +from pulsar import ConsumerType +from loguru import logger +from swarms import Agent +from typing import List, Dict, Any +import json + + +class ScalableAsyncAgentSwarm: + """ + A scalable, asynchronous swarm of agents leveraging Apache Pulsar for inter-agent communication. + Provides load balancing, health monitoring, dead letter queues, and centralized logging. + """ + + def __init__( + self, + pulsar_url: str, + topic: str, + dlq_topic: str, + agents_config: List[Dict[str, Any]], + ): + """ + Initializes the async swarm with agents. + + Args: + pulsar_url (str): The URL of the Apache Pulsar broker. + topic (str): The main topic for task distribution. + dlq_topic (str): The Dead Letter Queue topic for failed messages. + agents_config (List[Dict[str, Any]]): List of agent configurations with `name`, `description`, and `model_name`. + """ + self.pulsar_url = pulsar_url + self.topic = topic + self.dlq_topic = dlq_topic + self.agents_config = agents_config + self.client = pulsar.Client(pulsar_url) + self.consumer = self.client.subscribe( + topic, + subscription_name="swarm-task-sub", + consumer_type=ConsumerType.Shared, + ) + self.dlq_producer = self.client.create_producer(dlq_topic) + self.response_logger = [] + self.agents = [ + self.create_agent(config) for config in agents_config + ] + self.agent_index = 0 + + logger.info( + "Swarm initialized with agents: {}", + [agent["name"] for agent in agents_config], + ) + + def create_agent( + self, agent_config: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Creates a new agent configuration with asynchronous capabilities. + + Args: + agent_config (Dict[str, Any]): Configuration dictionary with agent details. + + Returns: + Dict[str, Any]: A dictionary containing agent metadata and functionality. + """ + agent_name = agent_config["name"] + description = agent_config["description"] + model_name = agent_config.get("model_name", "gpt-4o-mini") + + class AsyncAgent: + """ + An asynchronous agent that processes tasks and communicates via Apache Pulsar. + """ + + def __init__( + self, name: str, description: str, model_name: str + ): + self.name = name + self.description = description + self.agent = Agent( + agent_name=name, + model_name=model_name, + max_loops="auto", + interactive=True, + streaming_on=True, + ) + logger.info( + f"Initialized agent '{name}' - {description}" + ) + + async def process_task( + self, message: str + ) -> Dict[str, Any]: + """ + Processes a single task using the agent. + + Args: + message (str): The task message. + + Returns: + Dict[str, Any]: JSON-formatted response. + """ + try: + logger.info( + f"Agent {self.name} processing task: {message}" + ) + response = await asyncio.to_thread( + self.agent.run, message + ) + logger.info(f"Agent {self.name} completed task.") + return { + "agent_name": self.name, + "response": response, + } + except Exception as e: + logger.error( + f"Agent {self.name} encountered an error: {e}" + ) + return {"agent_name": self.name, "error": str(e)} + + return { + "name": agent_name, + "instance": AsyncAgent( + agent_name, description, model_name + ), + } + + async def distribute_task(self, message: str): + """ + Distributes a task to the next available agent using round-robin. + + Args: + message (str): The task message. + """ + agent = self.agents[self.agent_index] + self.agent_index = (self.agent_index + 1) % len(self.agents) + + try: + response = await agent["instance"].process_task(message) + self.log_response(response) + except Exception as e: + logger.error( + f"Error processing task by agent {agent['name']}: {e}" + ) + self.send_to_dlq(message) + + async def monitor_health(self): + """ + Periodically monitors the health of agents. + """ + while True: + logger.info("Performing health check for all agents.") + for agent in self.agents: + logger.info(f"Agent {agent['name']} is online.") + await asyncio.sleep(10) + + def send_to_dlq(self, message: str): + """ + Sends a failed message to the Dead Letter Queue (DLQ). + + Args: + message (str): The message to send to the DLQ. + """ + try: + self.dlq_producer.send(message.encode("utf-8")) + logger.info("Message sent to Dead Letter Queue.") + except Exception as e: + logger.error(f"Failed to send message to DLQ: {e}") + + def log_response(self, response: Dict[str, Any]): + """ + Logs the response to a centralized list for later analysis. + + Args: + response (Dict[str, Any]): The agent's response. + """ + self.response_logger.append(response) + logger.info(f"Response logged: {response}") + + async def listen_and_distribute(self): + """ + Listens to the main Pulsar topic and distributes tasks to agents. + """ + while True: + msg = self.consumer.receive() + try: + message = msg.data().decode("utf-8") + logger.info(f"Received task: {message}") + await self.distribute_task(message) + self.consumer.acknowledge(msg) + except Exception as e: + logger.error(f"Error processing message: {e}") + self.send_to_dlq(msg.data().decode("utf-8")) + self.consumer.negative_acknowledge(msg) + + async def run(self): + """ + Runs the swarm asynchronously with health monitoring and task distribution. + """ + logger.info("Starting the async swarm...") + task_listener = asyncio.create_task( + self.listen_and_distribute() + ) + health_monitor = asyncio.create_task(self.monitor_health()) + await asyncio.gather(task_listener, health_monitor) + + def shutdown(self): + """ + Safely shuts down the swarm and logs all responses. + """ + logger.info("Shutting down the swarm...") + self.client.close() + with open("responses.json", "w") as f: + json.dump(self.response_logger, f, indent=4) + logger.info("Responses saved to 'responses.json'.") + + +# from scalable_agent_swarm import ScalableAsyncAgentSwarm # Assuming your swarm class is saved here + +if __name__ == "__main__": + # Example Configuration + PULSAR_URL = "pulsar://localhost:6650" + TOPIC = "stock-analysis" + DLQ_TOPIC = "stock-analysis-dlq" + + # Agents configuration + AGENTS_CONFIG = [ + { + "name": "Stock-Analysis-Agent-1", + "description": "Analyzes stock trends.", + "model_name": "gpt-4o-mini", + }, + { + "name": "Stock-News-Agent", + "description": "Summarizes stock news.", + "model_name": "gpt-4o-mini", + }, + { + "name": "Tech-Trends-Agent", + "description": "Tracks tech sector trends.", + "model_name": "gpt-4o-mini", + }, + ] + + # Tasks to send + TASKS = [ + "Analyze the trend for tech stocks in Q4 2024", + "Summarize the latest news on the S&P 500", + "Identify the top-performing sectors in the stock market", + "Provide a forecast for AI-related stocks for 2025", + ] + + # Initialize and run the swarm + swarm = ScalableAsyncAgentSwarm( + PULSAR_URL, TOPIC, DLQ_TOPIC, AGENTS_CONFIG + ) + try: + # Run the swarm in the background + swarm_task = asyncio.create_task(swarm.run()) + + # Send tasks to the topic + client = pulsar.Client(PULSAR_URL) + producer = client.create_producer(TOPIC) + + for task in TASKS: + producer.send(task.encode("utf-8")) + print(f"Sent task: {task}") + + producer.close() + client.close() + + # Keep the swarm running + asyncio.run(swarm_task) + except KeyboardInterrupt: + swarm.shutdown() diff --git a/pyproject.toml b/pyproject.toml index 51bb898f..5102f0d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "swarms" -version = "6.3.7" +version = "6.4.7" description = "Swarms - Pytorch" license = "MIT" authors = ["Kye Gomez "] diff --git a/swarms/agents/auto_generate_swarm_config.py b/swarms/agents/auto_generate_swarm_config.py new file mode 100644 index 00000000..febb85e3 --- /dev/null +++ b/swarms/agents/auto_generate_swarm_config.py @@ -0,0 +1,253 @@ +import re + +from dotenv import load_dotenv +from tenacity import retry, stop_after_attempt, wait_exponential + +from swarms import Agent +from swarms.agents.create_agents_from_yaml import ( + create_agents_from_yaml, +) +from swarms.utils.formatter import formatter +from swarms.utils.litellm import LiteLLM + +load_dotenv() + + +def prepare_yaml_for_parsing(raw_yaml: str) -> str: + """ + Prepares raw YAML content by fixing spacing and formatting issues. + + Args: + raw_yaml (str): The raw YAML content extracted from Markdown. + + Returns: + str: The cleaned YAML content ready for parsing. + """ + # Fix sequence items that are improperly placed on the same line as their key + fixed_yaml = re.sub( + r"(\b\w+\b):\s*-\s*", r"\1:\n - ", raw_yaml + ) # Fix "key: - value" to "key:\n - value" + + # Ensure proper spacing after colons + fixed_yaml = re.sub( + r"(\S):(\S)", r"\1: \2", fixed_yaml + ) # Ensure space after colons + + # Remove trailing spaces before newlines + fixed_yaml = re.sub(r"\s+\n", "\n", fixed_yaml) + + # Replace non-breaking spaces (if any) with regular spaces + fixed_yaml = fixed_yaml.replace("\xa0", " ") + + return fixed_yaml.strip() + + +def parse_yaml_from_swarm_markdown(markdown_text: str) -> dict: + """ + Extracts and prepares YAML content from a Markdown-style 'Auto-Swarm-Builder' block and parses it. + + Args: + markdown_text (str): The Markdown text containing the YAML inside 'Auto-Swarm-Builder' block. + + Returns: + dict: A parsed Python dictionary of the YAML content. + """ + # Match the 'Auto-Swarm-Builder' block with YAML inside triple backticks + pattern = r"```yaml\s*\n(.*?)```" + match = re.search(pattern, markdown_text, re.DOTALL) + + if not match: + raise ValueError( + "No YAML content found in the 'Auto-Swarm-Builder' block." + ) + + raw_yaml = match.group(1).strip() + + # Preprocess and normalize the YAML content + normalized_yaml = prepare_yaml_for_parsing(raw_yaml) + + return normalized_yaml + + +AUTO_GEN_PROMPT = """ +You are a specialized agent responsible for creating YAML configuration files for multi-agent swarms. Your role is to generate well-structured YAML that defines both individual agents and swarm architectures based on user requirements. +Output only the yaml nothing else. You will be penalized for making mistakes + +GUIDELINES: +1. Each YAML file must contain an `agents` section with at least one agent configuration +2. Each agent configuration requires the following mandatory fields: + - agent_name (string) + - system_prompt (string) + +3. Optional agent fields include: + - max_loops (integer) + - autosave (boolean) + - dashboard (boolean) + - verbose (boolean) + - dynamic_temperature_enabled (boolean) + - saved_state_path (string) + - user_name (string) + - retry_attempts (integer) + - context_length (integer) + - return_step_meta (boolean) + - output_type (string) + - task (string) + +4. When a swarm is needed, include a `swarm_architecture` section with: + Mandatory fields: + - name (string) + - swarm_type (string: "ConcurrentWorkflow" or "SequentialWorkflow") [AgentRearrange, MixtureOfAgents, SpreadSheetSwarm, SequentialWorkflow, ConcurrentWorkflow] + + Optional fields: + - description (string) + - max_loops (integer) + - task (string) + +TEMPLATE STRUCTURE: +```yaml +agents: + - agent_name: "Agent-1-Name" + system_prompt: "Detailed system prompt here" + max_loops: 1 + # [additional optional fields] + + - agent_name: "Agent-2-Name" + system_prompt: "Detailed system prompt here" + # [additional optional fields] + +swarm_architecture: + name: "Swarm-Name" + description: "Swarm purpose and goals" + swarm_type: "ConcurrentWorkflow" + max_loops: 5 + task: "Main swarm task description" +``` + +VALIDATION RULES: +1. All agent names must be unique +2. System prompts must be clear and specific to the agent's role +3. Integer values must be positive +4. Boolean values must be true or false (lowercase) +5. File paths should use forward slashes +6. Tasks should be specific and aligned with the agent/swarm purpose + +When generating a YAML configuration: +1. Ask for specific requirements about the agents and swarm needed +2. Determine if a swarm architecture is necessary based on the task complexity +3. Generate appropriate system prompts for each agent based on their roles +4. Include relevant optional fields based on the use case +5. Validate the configuration against all rules before returning + +Example valid YAML configurations are provided below. Use these as references for structure and formatting: + +```yaml + + +agents: + - agent_name: "Data-Analysis-Agent" + system_prompt: "You are a specialized data analysis agent focused on processing and interpreting financial data. Provide clear, actionable insights based on the data provided." + max_loops: 3 + autosave: true + verbose: true + context_length: 100000 + output_type: "json" + task: "Analyze quarterly financial reports and identify trends" + +# Multi-Agent Swarm Example +agents: + - agent_name: "Research-Agent" + system_prompt: "You are a research agent specialized in gathering and summarizing scientific publications. Focus on peer-reviewed sources and provide comprehensive summaries." + max_loops: 2 + context_length: 150000 + output_type: "str" + + - agent_name: "Analysis-Agent" + system_prompt: "You are an analysis agent that processes research summaries and identifies key patterns and insights. Provide detailed analytical reports." + max_loops: 3 + context_length: 200000 + output_type: "json" + +swarm_architecture: + name: "Research-Analysis-Swarm" + description: "A swarm for comprehensive research analysis and insight generation" + swarm_type: "SequentialWorkflow" + max_loops: 5 + task: "Research and analyze recent developments in quantum computing" + +``` +""" + + +def generate_swarm_config( + task: str, + file_name: str = "swarm_config_output.yaml", + model_name: str = "gpt-4o", + *args, + **kwargs, +): + """ + Generates a swarm configuration based on the provided task and model name. + + This function attempts to generate a swarm configuration by running an agent with the specified task and model name. + It then parses the output into YAML format and creates agents based on the parsed YAML content. + + Args: + task (str): The task to be performed by the swarm. + file_name (str, optional): The file name for the output YAML configuration. Defaults to "swarm_config_output.yaml". + model_name (str, optional): The name of the model to use for the agent. Defaults to "gpt-4o". + *args: Additional positional arguments to be passed to the agent's run method. + **kwargs: Additional keyword arguments to be passed to the agent's run method. + + Returns: + Any: The output of the swarm configuration generation process. This can be a SwarmRouter instance or an error message. + """ + formatter.print_panel( + "Auto Generating Swarm...", "Auto Swarm Builder" + ) + + @retry( + stop=stop_after_attempt(3), + wait=wait_exponential(min=4, max=10), + ) + def attempt_generate_swarm_config(): + try: + model = LiteLLM(model_name=model_name) + + # Initialize the agent + agent = Agent( + agent_name="Auto-Swarm-Builder", + system_prompt=AUTO_GEN_PROMPT, + llm=model, + max_loops=1, + dynamic_temperature_enabled=True, + saved_state_path="swarm_builder.json", + user_name="swarms_corp", + output_type="str", + ) + + # Generate output from the agent + raw_output = agent.run(task, *args, **kwargs) + yaml_content = parse_yaml_from_swarm_markdown(raw_output) + print(yaml_content) + + # Create agents from the YAML file + output = create_agents_from_yaml( + yaml_string=yaml_content, + return_type="run_swarm", + ) + + formatter.print_panel( + "Swarm configuration generated successfully.", + "Success", + ) + + return output + + except Exception as e: + formatter.print_panel( + f"Error generating swarm configuration: {str(e)}", + "Error", + ) + raise + + return attempt_generate_swarm_config() diff --git a/swarms/agents/create_agents_from_yaml.py b/swarms/agents/create_agents_from_yaml.py index 7e6e056b..e92d1923 100644 --- a/swarms/agents/create_agents_from_yaml.py +++ b/swarms/agents/create_agents_from_yaml.py @@ -1,22 +1,168 @@ import os -from typing import Any, Callable, Dict, List, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Union import yaml +from tenacity import ( + retry, + stop_after_attempt, + wait_exponential, + retry_if_exception_type, +) +from pydantic import ( + BaseModel, + Field, + field_validator, +) from swarms.utils.loguru_logger import initialize_logger - from swarms.structs.agent import Agent from swarms.structs.swarm_router import SwarmRouter - +from swarms.utils.litellm import LiteLLM logger = initialize_logger(log_folder="create_agents_from_yaml") +class AgentConfig(BaseModel): + agent_name: str + system_prompt: str + model_name: Optional[str] = None + max_loops: int = Field(default=1, ge=1) + autosave: bool = True + dashboard: bool = False + verbose: bool = False + dynamic_temperature_enabled: bool = False + saved_state_path: Optional[str] = None + user_name: str = "default_user" + retry_attempts: int = Field(default=3, ge=1) + context_length: int = Field(default=100000, ge=1000) + return_step_meta: bool = False + output_type: str = "str" + auto_generate_prompt: bool = False + artifacts_on: bool = False + artifacts_file_extension: str = ".md" + artifacts_output_path: str = "" + + @field_validator("system_prompt") + @classmethod + def validate_system_prompt(cls, v): + if not v or not isinstance(v, str) or len(v.strip()) == 0: + raise ValueError( + "System prompt must be a non-empty string" + ) + return v + + +class SwarmConfig(BaseModel): + name: str + description: str + max_loops: int = Field(default=1, ge=1) + swarm_type: str + task: Optional[str] = None + flow: Optional[Dict] = None + autosave: bool = True + return_json: bool = False + rules: str = "" + + @field_validator("swarm_type") + @classmethod + def validate_swarm_type(cls, v): + valid_types = { + "SequentialWorkflow", + "ConcurrentWorkflow", + "AgentRearrange", + "MixtureOfAgents", + "auto", + } + if v not in valid_types: + raise ValueError( + f"Swarm type must be one of: {valid_types}" + ) + return v + + +class YAMLConfig(BaseModel): + agents: List[AgentConfig] = Field(..., min_length=1) + swarm_architecture: Optional[SwarmConfig] = None + + model_config = { + "extra": "forbid" # Prevent additional fields not in the model + } + + +def load_yaml_safely( + yaml_file: str = None, yaml_string: str = None +) -> Dict: + """Safely load and validate YAML configuration using Pydantic.""" + try: + if yaml_string: + config_dict = yaml.safe_load(yaml_string) + elif yaml_file: + if not os.path.exists(yaml_file): + raise FileNotFoundError( + f"YAML file {yaml_file} not found." + ) + with open(yaml_file, "r") as file: + config_dict = yaml.safe_load(file) + else: + raise ValueError( + "Either yaml_file or yaml_string must be provided" + ) + + # Validate using Pydantic + YAMLConfig(**config_dict) + return config_dict + except yaml.YAMLError as e: + raise ValueError(f"Error parsing YAML: {str(e)}") + except Exception as e: + raise ValueError(f"Error validating configuration: {str(e)}") + + +@retry( + stop=stop_after_attempt(3), + wait=wait_exponential(multiplier=1, min=4, max=10), + retry=retry_if_exception_type((ConnectionError, TimeoutError)), + before_sleep=lambda retry_state: logger.info( + f"Retrying after error: {retry_state.outcome.exception()}" + ), +) +def create_agent_with_retry( + agent_config: Dict, model: LiteLLM +) -> Agent: + """Create an agent with retry logic for handling transient failures.""" + try: + validated_config = AgentConfig(**agent_config) + agent = Agent( + agent_name=validated_config.agent_name, + system_prompt=validated_config.system_prompt, + llm=model, + max_loops=validated_config.max_loops, + autosave=validated_config.autosave, + dashboard=validated_config.dashboard, + verbose=validated_config.verbose, + dynamic_temperature_enabled=validated_config.dynamic_temperature_enabled, + saved_state_path=validated_config.saved_state_path, + user_name=validated_config.user_name, + retry_attempts=validated_config.retry_attempts, + context_length=validated_config.context_length, + return_step_meta=validated_config.return_step_meta, + output_type=validated_config.output_type, + auto_generate_prompt=validated_config.auto_generate_prompt, + artifacts_on=validated_config.artifacts_on, + artifacts_file_extension=validated_config.artifacts_file_extension, + artifacts_output_path=validated_config.artifacts_output_path, + ) + return agent + except Exception as e: + logger.error( + f"Error creating agent {agent_config.get('agent_name', 'unknown')}: {str(e)}" + ) + raise + + def create_agents_from_yaml( model: Callable = None, yaml_file: str = "agents.yaml", + yaml_string: str = None, return_type: str = "auto", - *args, - **kwargs, ) -> Union[ SwarmRouter, Agent, @@ -25,171 +171,99 @@ def create_agents_from_yaml( List[Dict[str, Any]], ]: """ - Create agents and/or SwarmRouter based on configurations defined in a YAML file. - - This function dynamically creates agents and a SwarmRouter (if specified) based on the - configuration in the YAML file. It adapts its behavior based on the presence of a - swarm architecture and the number of agents defined. - - Args: - model (Callable): The language model to be used by the agents. - yaml_file (str): Path to the YAML file containing agent and swarm configurations. - return_type (str): Determines the return value. Options are: - "auto" (default): Automatically determine the most appropriate return type. - "swarm": Return SwarmRouter if present, otherwise a single agent or list of agents. - "agents": Return a list of agents (or a single agent if only one is defined). - "both": Return both SwarmRouter (or single agent) and list of agents. - "tasks": Return task results if any tasks were executed. - "run_swarm": Run the swarm and return its output. - *args: Additional positional arguments for agent or SwarmRouter customization. - **kwargs: Additional keyword arguments for agent or SwarmRouter customization. - - Returns: - Union[SwarmRouter, Agent, List[Agent], Tuple[Union[SwarmRouter, Agent], List[Agent]], List[Dict[str, Any]]]: - The return type depends on the 'return_type' argument and the configuration in the YAML file. - - Raises: - FileNotFoundError: If the specified YAML file is not found. - ValueError: If the YAML configuration is invalid or if an invalid return_type is specified. + Create agents and/or SwarmRouter based on configurations defined in a YAML file or string. """ - try: - logger.info( - f"Checking if the YAML file {yaml_file} exists..." - ) - - if not os.path.exists(yaml_file): - logger.error(f"YAML file {yaml_file} not found.") - raise FileNotFoundError( - f"YAML file {yaml_file} not found." - ) - - logger.info(f"Loading YAML file {yaml_file}") - with open(yaml_file, "r") as file: - config = yaml.safe_load(file) - - if "agents" not in config: - logger.error( - "The YAML configuration does not contain 'agents'." - ) - raise ValueError( - "The YAML configuration does not contain 'agents'." - ) + agents = [] + task_results = [] + swarm_router = None - agents = [] - task_results = [] + try: + # Load and validate configuration + config = load_yaml_safely(yaml_file, yaml_string) - # Create agents + # Create agents with retry logic for agent_config in config["agents"]: logger.info( f"Creating agent: {agent_config['agent_name']}" ) - if "system_prompt" not in agent_config: - logger.error( - f"System prompt is missing for agent: {agent_config['agent_name']}" - ) - raise ValueError( - f"System prompt is missing for agent: {agent_config['agent_name']}" + if "model_name" in agent_config: + model_instance = LiteLLM( + model_name=agent_config["model_name"] ) + else: + model_name = "gpt-4o" + model_instance = LiteLLM(model_name=model_name) - agent = Agent( - agent_name=agent_config["agent_name"], - system_prompt=agent_config["system_prompt"], - llm=model, - max_loops=agent_config.get("max_loops", 1), - autosave=agent_config.get("autosave", True), - dashboard=agent_config.get("dashboard", False), - verbose=agent_config.get("verbose", False), - dynamic_temperature_enabled=agent_config.get( - "dynamic_temperature_enabled", False - ), - saved_state_path=agent_config.get("saved_state_path"), - user_name=agent_config.get( - "user_name", "default_user" - ), - retry_attempts=agent_config.get("retry_attempts", 1), - context_length=agent_config.get( - "context_length", 100000 - ), - return_step_meta=agent_config.get( - "return_step_meta", False - ), - output_type=agent_config.get("output_type", "str"), - auto_generate_prompt=agent_config.get( - "auto_generate_prompt", "False" - ), - artifacts_on=agent_config.get( - "artifacts_on", "False" - ), - artifacts_file_extension=agent_config.get( - "artifacts_file_extension", ".md" - ), - artifacts_output_path=agent_config.get( - "artifacts_output_path", "" - ), - *args, - **kwargs, + agent = create_agent_with_retry( + agent_config, model_instance ) - logger.info( f"Agent {agent_config['agent_name']} created successfully." ) agents.append(agent) - # Create SwarmRouter if swarm_architecture is present - swarm_router = None + # Create SwarmRouter if specified if "swarm_architecture" in config: - swarm_config = config["swarm_architecture"] - swarm_router = SwarmRouter( - name=swarm_config["name"], - description=swarm_config["description"], - max_loops=swarm_config["max_loops"], - agents=agents, - swarm_type=swarm_config["swarm_type"], - task=swarm_config.get("task"), - flow=swarm_config.get("flow"), - autosave=swarm_config.get("autosave"), - return_json=swarm_config.get("return_json"), - rules=swarm_config.get("rules", "") * args, - **kwargs, - ) - logger.info( - f"SwarmRouter '{swarm_config['name']}' created successfully." + try: + swarm_config = SwarmConfig( + **config["swarm_architecture"] + ) + swarm_router = SwarmRouter( + name=swarm_config.name, + description=swarm_config.description, + max_loops=swarm_config.max_loops, + agents=agents, + swarm_type=swarm_config.swarm_type, + task=swarm_config.task, + flow=swarm_config.flow, + autosave=swarm_config.autosave, + return_json=swarm_config.return_json, + rules=swarm_config.rules, + ) + logger.info( + f"SwarmRouter '{swarm_config.name}' created successfully." + ) + except Exception as e: + logger.error(f"Error creating SwarmRouter: {str(e)}") + raise ValueError( + f"Failed to create SwarmRouter: {str(e)}" + ) + + # Handle return types with improved error checking + valid_return_types = { + "auto", + "swarm", + "agents", + "both", + "tasks", + "run_swarm", + } + if return_type not in valid_return_types: + raise ValueError( + f"Invalid return_type. Must be one of: {valid_return_types}" ) - # Define function to run SwarmRouter - def run_swarm_router( - task: str = ( - swarm_config.get("task") - if "swarm_architecture" in config - else None - ), - ): - if swarm_router: - try: - output = swarm_router.run(task) - print(output) - logger.info( - f"Output for SwarmRouter '{swarm_config['name']}': {output}" - ) - return output - except Exception as e: - logger.error( - f"Error running task for SwarmRouter '{swarm_config['name']}': {e}" - ) - raise e - else: - logger.error("SwarmRouter not created.") - raise ValueError("SwarmRouter not created.") + if return_type == "run_swarm" or "swarm": + if not swarm_router: + raise ValueError( + "Cannot run swarm: SwarmRouter not created." + ) + try: + return swarm_router.run( + config["swarm_architecture"]["task"] + ) + except Exception as e: + logger.error(f"Error running SwarmRouter: {str(e)}") + raise - # Handle return types + # Return appropriate type based on configuration if return_type == "auto": - if swarm_router: - return swarm_router - elif len(agents) == 1: - return agents[0] - else: - return agents + return ( + swarm_router + if swarm_router + else (agents[0] if len(agents) == 1 else agents) + ) elif return_type == "swarm": return ( swarm_router @@ -205,24 +279,10 @@ def create_agents_from_yaml( else agents[0] if len(agents) == 1 else agents ), agents elif return_type == "tasks": - if not task_results: - logger.warning( - "No tasks were executed. Returning empty list." - ) return task_results - elif return_type == "run_swarm": - if swarm_router: - return run_swarm_router() - else: - logger.error( - "Cannot run swarm: SwarmRouter not created." - ) - raise ValueError( - "Cannot run swarm: SwarmRouter not created." - ) - else: - logger.error(f"Invalid return_type: {return_type}") - raise ValueError(f"Invalid return_type: {return_type}") + except Exception as e: - logger.error(f"An error occurred: {e}") - raise e + logger.error( + f"Critical error in create_agents_from_yaml: {str(e)}" + ) + raise diff --git a/swarms/cli/main.py b/swarms/cli/main.py index 738deec6..5abe8b58 100644 --- a/swarms/cli/main.py +++ b/swarms/cli/main.py @@ -1,244 +1,348 @@ import argparse import os +import subprocess import time +import webbrowser from rich.console import Console +from rich.panel import Panel +from rich.progress import Progress, SpinnerColumn, TextColumn +from rich.table import Table from rich.text import Text -from swarms.cli.onboarding_process import OnboardingProcess + +from swarms.agents.auto_generate_swarm_config import ( + generate_swarm_config, +) from swarms.agents.create_agents_from_yaml import ( create_agents_from_yaml, ) -import subprocess +from swarms.cli.onboarding_process import OnboardingProcess +from swarms.utils.formatter import formatter +# Initialize console with custom styling console = Console() -ASCII_ART = """ - _________ - / _____/_ _ _______ _______ _____ ______ - \_____ \\ \/ \/ /\__ \\_ __ \/ \ / ___/ - / \\ / / __ \| | \/ Y Y \\___ \ -/_______ / \/\_/ (____ /__| |__|_| /____ > - \/ \/ \/ \/ +class SwarmCLIError(Exception): + """Custom exception for Swarm CLI errors""" + + pass + +# Color scheme +COLORS = { + "primary": "red", + "secondary": "#FF6B6B", + "accent": "#4A90E2", + "success": "#2ECC71", + "warning": "#F1C40F", + "error": "#E74C3C", + "text": "#FFFFFF", +} + +ASCII_ART = """ + ▄████████ ▄█ █▄ ▄████████ ▄████████ ▄▄▄▄███▄▄▄▄ ▄████████ + ███ ███ ███ ███ ███ ███ ███ ███ ▄██▀▀▀███▀▀▀██▄ ███ ███ + ███ █▀ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ █▀ + ███ ███ ███ ███ ███ ▄███▄▄▄▄██▀ ███ ███ ███ ███ +▀███████████ ███ ███ ▀███████████ ▀▀███▀▀▀▀▀ ███ ███ ███ ▀███████████ + ███ ███ ███ ███ ███ ▀███████████ ███ ███ ███ ███ + ▄█ ███ ███ ▄█▄ ███ ███ ███ ███ ███ ███ ███ ███ ▄█ ███ + ▄████████▀ ▀███▀███▀ ███ █▀ ███ ███ ▀█ ███ █▀ ▄████████▀ + ███ ███ """ -# Function to display the ASCII art in red +def create_spinner(text: str) -> Progress: + """Create a custom spinner with the given text.""" + return Progress( + SpinnerColumn(style=COLORS["primary"]), + TextColumn("[{task.description}]", style=COLORS["text"]), + console=console, + ) + + def show_ascii_art(): - text = Text(ASCII_ART, style="bold cyan") - console.print(text) + """Display the ASCII art with a glowing effect.""" + panel = Panel( + Text(ASCII_ART, style=f"bold {COLORS['primary']}"), + border_style=COLORS["secondary"], + title="[bold]Welcome to Swarms[/bold]", + subtitle="[dim]Power to the Swarms[/dim]", + ) + console.print(panel) -# Help command -def show_help(): - console.print( - """ - [bold cyan]Swarms CLI - Help[/bold cyan] - - [bold magenta]Commands:[/bold magenta] - [bold white]onboarding[/bold white] : Starts the onboarding process - [bold white]help[/bold white] : Shows this help message - [bold white]get-api-key[/bold white] : Retrieves your API key from the platform - [bold white]check-login[/bold white] : Checks if you're logged in and starts the cache - [bold white]read-docs[/bold white] : Redirects you to swarms cloud documentation! - [bold white]run-agents[/bold white] : Run your Agents from your specified yaml file. Specify the yaml file with path the `--yaml-file` arg. Example: `--yaml-file agents.yaml` - [bold white]generate-prompt[/bold white] : Generate a prompt through automated prompt engineering. Requires an OPENAI Key in your `.env` Example: --prompt "Generate a prompt for an agent to analyze legal docs" - [bold white]auto-upgrade[/bold white] : Automatically upgrades Swarms to the latest version - [bold white]book-call[/bold white] : Book a strategy session with our team to discuss your use case and get personalized guidance - - For more details, visit: https://docs.swarms.world - """ +def create_command_table() -> Table: + """Create a beautifully formatted table of commands.""" + table = Table( + show_header=True, + header_style=f"bold {COLORS['primary']}", + border_style=COLORS["secondary"], + title="Available Commands", + padding=(0, 2), ) - # [bold white]add-agent[/bold white] : Add an agent to the marketplace under your name. Must have a Dockerfile + your agent.yaml to publish. Learn more Here: https://docs.swarms.world/en/latest/swarms_cloud/vision/ + table.add_column("Command", style="bold white") + table.add_column("Description", style="dim white") + commands = [ + ("onboarding", "Start the interactive onboarding process"), + ("help", "Display this help message"), + ("get-api-key", "Retrieve your API key from the platform"), + ("check-login", "Verify login status and initialize cache"), + ("run-agents", "Execute agents from your YAML configuration"), + ("auto-upgrade", "Update Swarms to the latest version"), + ("book-call", "Schedule a strategy session with our team"), + ("autoswarm", "Generate and execute an autonomous swarm"), + ] -# Fetch API key from platform -def get_api_key(): + for cmd, desc in commands: + table.add_row(cmd, desc) + + return table + + +def show_help(): + """Display a beautifully formatted help message.""" console.print( - "[bold yellow]Opening the API key retrieval page...[/bold yellow]" + "\n[bold]Swarms CLI - Command Reference[/bold]\n", + style=COLORS["primary"], ) - # Simulating API key retrieval process by opening the website - import webbrowser - - webbrowser.open("https://swarms.world/platform/api-keys") - time.sleep(2) + console.print(create_command_table()) console.print( - "[bold green]Your API key is available on the dashboard.[/bold green]" + "\n[dim]For detailed documentation, visit: https://docs.swarms.world[/dim]" ) -# Redirect to docs -def redirect_to_docs(): - console.print( - "[bold yellow]Opening the Docs page...[/bold yellow]" +def show_error(message: str, help_text: str = None): + """Display error message in a formatted panel""" + error_panel = Panel( + f"[bold red]{message}[/bold red]", + title="Error", + border_style="red", ) - # Simulating API key retrieval process by opening the website - import webbrowser + console.print(error_panel) - webbrowser.open("https://docs.swarms.world") - time.sleep(2) + if help_text: + console.print(f"\n[yellow]ℹ️ {help_text}[/yellow]") -# Redirect to docs -def redirect_to_call(): +def execute_with_spinner(action: callable, text: str) -> None: + """Execute an action with a spinner animation.""" + with create_spinner(text) as progress: + task = progress.add_task(text, total=None) + result = action() + progress.remove_task(task) + return result + + +def get_api_key(): + """Retrieve API key with visual feedback.""" + with create_spinner("Opening API key portal...") as progress: + task = progress.add_task("Opening browser...") + webbrowser.open("https://swarms.world/platform/api-keys") + time.sleep(1) + progress.remove_task(task) console.print( - "[bold yellow]Opening the Call page...[/bold yellow]" + f"\n[{COLORS['success']}]✓ API key page opened in your browser[/{COLORS['success']}]" ) - # Simulating API key retrieval process by opening the website - import webbrowser - webbrowser.open("https://cal.com/swarms/swarms-strategy-session") - time.sleep(2) - -# Check and start cache (login system simulation) def check_login(): + """Verify login status with enhanced visual feedback.""" cache_file = "cache.txt" if os.path.exists(cache_file): with open(cache_file, "r") as f: - cache_content = f.read() - if cache_content == "logged_in": + if f.read() == "logged_in": + console.print( + f"[{COLORS['success']}]✓ Authentication verified[/{COLORS['success']}]" + ) + return True + + with create_spinner("Authenticating...") as progress: + task = progress.add_task("Initializing session...") + time.sleep(1) + with open(cache_file, "w") as f: + f.write("logged_in") + progress.remove_task(task) + + console.print( + f"[{COLORS['success']}]✓ Login successful![/{COLORS['success']}]" + ) + return True + + +def run_autoswarm(task: str, model: str): + """Run autoswarm with enhanced error handling""" + try: + console.print( + "[yellow]Initializing autoswarm configuration...[/yellow]" + ) + + # Set LiteLLM verbose mode for debugging + import litellm + + litellm.set_verbose = True + + # Validate inputs + if not task or task.strip() == "": + raise SwarmCLIError("Task cannot be empty") + + if not model or model.strip() == "": + raise SwarmCLIError("Model name cannot be empty") + + # Attempt to generate swarm configuration + console.print( + f"[yellow]Generating swarm for task: {task}[/yellow]" + ) + result = generate_swarm_config(task=task, model=model) + + if result: console.print( - "[bold green]You are already logged in.[/bold green]" + "[green]✓ Swarm configuration generated successfully![/green]" ) else: - console.print( - "[bold red]You are not logged in.[/bold red]" + raise SwarmCLIError( + "Failed to generate swarm configuration" + ) + + except Exception as e: + if "No YAML content found" in str(e): + show_error( + "Failed to generate YAML configuration", + "This might be due to an API key issue or invalid model configuration.\n" + + "1. Check if your OpenAI API key is set correctly\n" + + "2. Verify the model name is valid\n" + + "3. Try running with --model gpt-4", + ) + else: + show_error( + f"Error during autoswarm execution: {str(e)}", + "For debugging, try:\n" + + "1. Check your API keys are set correctly\n" + + "2. Verify your network connection\n" + + "3. Try a different model", ) - else: - console.print("[bold yellow]Logging in...[/bold yellow]") - time.sleep(2) - with open(cache_file, "w") as f: - f.write("logged_in") - console.print("[bold green]Login successful![/bold green]") def check_and_upgrade_version(): - console.print( - "[bold yellow]Checking for Swarms updates...[/bold yellow]" - ) - try: - # Check for updates using pip + """Check for updates with visual progress.""" + + def check_update(): result = subprocess.run( ["pip", "list", "--outdated", "--format=freeze"], capture_output=True, text=True, ) - outdated_packages = result.stdout.splitlines() + return result.stdout.splitlines() - # Check if Swarms is outdated - for package in outdated_packages: - if package.startswith("swarms=="): - console.print( - "[bold magenta]New version available! Upgrading...[/bold magenta]" + outdated = execute_with_spinner( + check_update, "Checking for updates..." + ) + + for package in outdated: + if package.startswith("swarms=="): + console.print( + f"[{COLORS['warning']}]↑ Update available![/{COLORS['warning']}]" + ) + with create_spinner("Upgrading Swarms...") as progress: + task = progress.add_task( + "Installing latest version..." ) subprocess.run( ["pip", "install", "--upgrade", "swarms"], check=True, ) - console.print( - "[bold green]Swarms upgraded successfully![/bold green]" - ) - return + progress.remove_task(task) + console.print( + f"[{COLORS['success']}]✓ Swarms upgraded successfully![/{COLORS['success']}]" + ) + return - console.print( - "[bold green]Swarms is up-to-date.[/bold green]" - ) - except Exception as e: - console.print( - f"[bold red]Error checking for updates: {e}[/bold red]" - ) + console.print( + f"[{COLORS['success']}]✓ Swarms is up to date![/{COLORS['success']}]" + ) -# Main CLI handler def main(): - parser = argparse.ArgumentParser(description="Swarms Cloud CLI") - - # Adding arguments for different commands - parser.add_argument( - "command", - choices=[ - "onboarding", - "help", - "get-api-key", - "check-login", - "run-agents", - "generate-prompt", # Added new command for generating prompts - "auto-upgrade", # Added new command for auto-upgrade, - "book-call", - ], - help="Command to run", - ) - parser.add_argument( - "--yaml-file", - type=str, - default="agents.yaml", - help="Specify the YAML file for running agents", - ) - parser.add_argument( - "--prompt", - type=str, - help="Specify the task for generating a prompt", - ) - parser.add_argument( - "--num-loops", - type=int, - default=1, - help="Specify the number of loops for generating a prompt", - ) - parser.add_argument( - "--autosave", - action="store_true", - help="Enable autosave for the prompt generator", - ) - parser.add_argument( - "--save-to-yaml", - action="store_true", - help="Save the generated prompt to a YAML file", - ) + try: - args = parser.parse_args() - - show_ascii_art() - - # Determine which command to run - if args.command == "onboarding": - OnboardingProcess().run() - elif args.command == "help": - show_help() - elif args.command == "get-api-key": - get_api_key() - elif args.command == "check-login": - check_login() - elif args.command == "run-agents": - create_agents_from_yaml( - yaml_file=args.yaml_file, return_type="tasks" + show_ascii_art() + + parser = argparse.ArgumentParser( + description="Swarms Cloud CLI" ) - # elif args.command == "generate-prompt": - # if ( - # args.prompt - # ): # Corrected from args.prompt_task to args.prompt - # generate_prompt( - # num_loops=args.num_loops, - # autosave=args.autosave, - # save_to_yaml=args.save_to_yaml, - # prompt=args.prompt, # Corrected from args.prompt_task to args.prompt - # ) - # else: - # console.print( - # "[bold red]Please specify a task for generating a prompt using '--prompt'.[/bold red]" - # ) - elif args.command == "auto-upgrade": - check_and_upgrade_version() - elif args.command == "book-call": - redirect_to_call() - else: - console.print( - "[bold red]Unknown command! Type 'help' for usage.[/bold red]" + parser.add_argument( + "command", + choices=[ + "onboarding", + "help", + "get-api-key", + "check-login", + "run-agents", + "auto-upgrade", + "book-call", + "autoswarm", + ], + help="Command to execute", + ) + parser.add_argument( + "--yaml-file", + type=str, + default="agents.yaml", + help="YAML configuration file path", + ) + parser.add_argument( + "--task", type=str, help="Task for autoswarm" + ) + parser.add_argument( + "--model", + type=str, + default="gpt-4", + help="Model for autoswarm", + ) + + args = parser.parse_args() + + try: + if args.command == "onboarding": + OnboardingProcess().run() + elif args.command == "help": + show_help() + elif args.command == "get-api-key": + get_api_key() + elif args.command == "check-login": + check_login() + elif args.command == "run-agents": + create_agents_from_yaml( + yaml_file=args.yaml_file, return_type="tasks" + ) + elif args.command == "auto-upgrade": + check_and_upgrade_version() + elif args.command == "book-call": + webbrowser.open( + "https://cal.com/swarms/swarms-strategy-session" + ) + elif args.command == "autoswarm": + if not args.task: + show_error( + "Missing required argument: --task", + "Example usage: python cli.py autoswarm --task 'analyze this data' --model gpt-4", + ) + exit(1) + run_autoswarm(args.task, args.model) + except Exception as e: + console.print( + f"[{COLORS['error']}]Error: {str(e)}[/{COLORS['error']}]" + ) + return + except Exception as error: + formatter.print_panel( + f"Error detected: {error} check your args" ) + raise error if __name__ == "__main__": diff --git a/swarms/cli/onboarding_process.py b/swarms/cli/onboarding_process.py index 71c063c2..edac1168 100644 --- a/swarms/cli/onboarding_process.py +++ b/swarms/cli/onboarding_process.py @@ -87,19 +87,6 @@ class OnboardingProcess: try: combined_data = {**self.user_data, **self.system_data} log_agent_data(combined_data) - # threading.Thread(target=log_agent_data(combined_data)).start() - # with open(self.auto_save_path, "w") as f: - # json.dump(combined_data, f, indent=4) - # # logger.info( - # # "User and system data successfully saved to {}", - # # self.auto_save_path, - # # ) - # with open(self.cache_save_path, "w") as f: - # json.dump(combined_data, f, indent=4) - # logger.info( - # "User and system data successfully cached in {}", - # self.cache_save_path, - # ) return # Exit the function if saving was successful except Exception as e: logger.error( diff --git a/swarms/structs/agent.py b/swarms/structs/agent.py index d1f3a745..ca2ac120 100644 --- a/swarms/structs/agent.py +++ b/swarms/structs/agent.py @@ -338,6 +338,8 @@ class Agent: scheduled_run_date: Optional[datetime] = None, do_not_use_cluster_ops: bool = True, all_gpus: bool = False, + model_name: str = None, + llm_args: dict = None, *args, **kwargs, ): @@ -453,6 +455,8 @@ class Agent: self.scheduled_run_date = scheduled_run_date self.do_not_use_cluster_ops = do_not_use_cluster_ops self.all_gpus = all_gpus + self.model_name = model_name + self.llm_args = llm_args # Initialize the short term memory self.short_memory = Conversation( @@ -589,6 +593,21 @@ class Agent: # Telemetry Processor to log agent data threading.Thread(target=self.log_agent_data).start() + threading.Thread(target=self.llm_handling()) + + def llm_handling(self): + + if self.llm is None: + from swarms.utils.litellm import LiteLLM + + if self.llm_args is not None: + self.llm = LiteLLM( + model_name=self.model_name, **self.llm_args + ) + + else: + self.llm = LiteLLM(model_name=self.model_name) + def check_if_no_prompt_then_autogenerate(self, task: str = None): """ Checks if auto_generate_prompt is enabled and generates a prompt by combining agent name, description and system prompt if available. @@ -951,7 +970,7 @@ class Agent: if self.interactive: logger.info("Interactive mode enabled.") - user_input = formatter.print_panel(input("You: ")) + user_input = input("You: ") # User-defined exit command if ( @@ -1015,6 +1034,11 @@ class Agent: self.artifacts_file_extension, ) + try: + self.log_agent_data() + except Exception: + pass + # More flexible output types if ( self.output_type == "string" @@ -1050,8 +1074,16 @@ class Agent: ) except Exception as error: + self.log_agent_data() + logger.info( + f"Error running agent: {error} optimize your input parameters" + ) + raise error + + except KeyboardInterrupt as error: + self.log_agent_data() logger.info( - f"Error running agent: {error} optimize your input parameter" + f"Error running agent: {error} optimize your input parameters" ) raise error diff --git a/groupchat_new.py b/swarms/structs/groupchat_new.py similarity index 100% rename from groupchat_new.py rename to swarms/structs/groupchat_new.py diff --git a/swarms/utils/litellm.py b/swarms/utils/litellm.py new file mode 100644 index 00000000..5bdd208d --- /dev/null +++ b/swarms/utils/litellm.py @@ -0,0 +1,105 @@ +try: + from litellm import completion +except ImportError: + import subprocess + + subprocess.check_call(["pip", "install", "litellm"]) + import litellm + from litellm import completion + + litellm.set_verbose = True + + +class LiteLLM: + """ + This class represents a LiteLLM. + It is used to interact with the LLM model for various tasks. + """ + + def __init__( + self, + model_name: str = "gpt-4o", + system_prompt: str = None, + stream: bool = False, + temperature: float = 0.5, + max_tokens: int = 4000, + ): + """ + Initialize the LiteLLM with the given parameters. + + Args: + model_name (str, optional): The name of the model to use. Defaults to "gpt-4o". + system_prompt (str, optional): The system prompt to use. Defaults to None. + stream (bool, optional): Whether to stream the output. Defaults to False. + temperature (float, optional): The temperature for the model. Defaults to 0.5. + max_tokens (int, optional): The maximum number of tokens to generate. Defaults to 4000. + """ + self.model_name = model_name + self.system_prompt = system_prompt + self.stream = stream + self.temperature = temperature + self.max_tokens = max_tokens + + def _prepare_messages(self, task: str) -> list: + """ + Prepare the messages for the given task. + + Args: + task (str): The task to prepare messages for. + + Returns: + list: A list of messages prepared for the task. + """ + messages = [] + + if self.system_prompt: # Check if system_prompt is not None + messages.append( + {"role": "system", "content": self.system_prompt} + ) + + messages.append({"role": "user", "content": task}) + + return messages + + def run(self, task: str, *args, **kwargs): + """ + Run the LLM model for the given task. + + Args: + task (str): The task to run the model for. + *args: Additional positional arguments to pass to the model. + **kwargs: Additional keyword arguments to pass to the model. + + Returns: + str: The content of the response from the model. + """ + messages = self._prepare_messages(task) + + response = completion( + model=self.model_name, + messages=messages, + stream=self.stream, + temperature=self.temperature, + # max_completion_tokens=self.max_tokens, + max_tokens=self.max_tokens, + *args, + **kwargs, + ) + content = response.choices[ + 0 + ].message.content # Accessing the content + return content + + def __call__(self, task: str, *args, **kwargs): + """ + Call the LLM model for the given task. + + Args: + task (str): The task to run the model for. + *args: Additional positional arguments to pass to the model. + **kwargs: Additional keyword arguments to pass to the model. + + Returns: + str: The content of the response from the model. + """ + return self.run(task, *args, **kwargs) diff --git a/swarms/utils/parse_code.py b/swarms/utils/parse_code.py index f295340c..c962c5d8 100644 --- a/swarms/utils/parse_code.py +++ b/swarms/utils/parse_code.py @@ -1,50 +1,64 @@ import re -def extract_code_from_markdown(markdown_content: str) -> str: +def extract_code_blocks_with_language(markdown_text: str): """ - Extracts code blocks from a Markdown string and returns them as a single string. + Extracts all code blocks from Markdown text along with their languages. Args: - - markdown_content (str): The Markdown content as a string. + markdown_text (str): The input Markdown text. Returns: - - str: A single string containing all the code blocks separated by newlines. + list[dict]: A list of dictionaries, each containing: + - 'language': The detected language (or 'plaintext' if none specified). + - 'content': The content of the code block. """ - # Regular expression for fenced code blocks with optional language specifier - pattern = r"```(?:\w+\n)?(.*?)```" + # Regex pattern to match code blocks and optional language specifiers + pattern = r"```(\w+)?\n(.*?)```" - # Check if markdown_content is a string - if not isinstance(markdown_content, str): - raise TypeError("markdown_content must be a string") + # Find all matches (language and content) + matches = re.findall(pattern, markdown_text, re.DOTALL) - # Find all matches of the pattern - matches = re.finditer(pattern, markdown_content, re.DOTALL) - - # Extract the content inside the backticks + # Parse results code_blocks = [] - for match in matches: - code_block = match.group(1).strip() - # Remove any leading or trailing whitespace from the code block - code_block = code_block.strip() - # Remove any empty lines from the code block - code_block = "\n".join( - [line for line in code_block.split("\n") if line.strip()] + for language, content in matches: + language = ( + language.strip() if language else "plaintext" + ) # Default to 'plaintext' + code_blocks.append( + {"language": language, "content": content.strip()} ) - code_blocks.append(code_block) - # Concatenate all code blocks separated by newlines - if code_blocks: - return "\n\n".join(code_blocks) - else: - return "" + return code_blocks + + +def extract_code_from_markdown( + markdown_text: str, language: str = None +): + """ + Extracts content of code blocks for a specific language or all blocks if no language specified. + Args: + markdown_text (str): The input Markdown text. + language (str, optional): The language to filter by (e.g., 'yaml', 'python'). -# example = """ -# hello im an agent -# ```bash -# pip install swarms -# ``` -# """ + Returns: + str: The concatenated content of matched code blocks or an empty string if none found. + """ + # Get all code blocks with detected languages + code_blocks = extract_code_blocks_with_language(markdown_text) + + # Filter by language if specified + if language: + code_blocks = [ + block["content"] + for block in code_blocks + if block["language"] == language + ] + else: + code_blocks = [ + block["content"] for block in code_blocks + ] # Include all blocks -# print(extract_code_from_markdown(example)) # Output: { "type": "function", "function": { "name": "fetch_financial_news", "parameters": { "query": "Nvidia news", "num_articles": 5 } } } + # Return concatenated content + return "\n\n".join(code_blocks) if code_blocks else "" diff --git a/test.py b/test.py deleted file mode 100644 index ce12ec1c..00000000 --- a/test.py +++ /dev/null @@ -1,292 +0,0 @@ -import torch -import torch.nn as nn -import torch.distributed as dist -from dataclasses import dataclass -from typing import Optional, Tuple, Union -from loguru import logger -import math - - -@dataclass -class StarAttentionConfig: - """Configuration for StarAttention module. - - Attributes: - hidden_size: Dimension of the model's hidden states - num_attention_heads: Number of attention heads - num_hosts: Number of hosts in the distributed system - block_size: Size of each context block - anchor_size: Size of the anchor block - dropout_prob: Dropout probability (default: 0.1) - layer_norm_eps: Layer normalization epsilon (default: 1e-12) - """ - - hidden_size: int - num_attention_heads: int - num_hosts: int - block_size: int - anchor_size: int - dropout_prob: float = 0.1 - layer_norm_eps: float = 1e-12 - - -class StarAttention(nn.Module): - """ - Implementation of Star Attention mechanism for distributed inference. - - The module implements a two-phase attention mechanism: - 1. Local Context Encoding with Anchor Blocks - 2. Query Encoding and Output Generation with Global Attention - """ - - def __init__(self, config: StarAttentionConfig): - super().__init__() - - if config.hidden_size % config.num_attention_heads != 0: - raise ValueError( - f"Hidden size {config.hidden_size} not divisible by number of attention " - f"heads {config.num_attention_heads}" - ) - - self.config = config - self.head_dim = ( - config.hidden_size // config.num_attention_heads - ) - - # Initialize components - self.query = nn.Linear(config.hidden_size, config.hidden_size) - self.key = nn.Linear(config.hidden_size, config.hidden_size) - self.value = nn.Linear(config.hidden_size, config.hidden_size) - - self.dropout = nn.Dropout(config.dropout_prob) - self.layer_norm = nn.LayerNorm( - config.hidden_size, eps=config.layer_norm_eps - ) - - # KV cache for storing computed key/value pairs - self.kv_cache = {} - - logger.info( - f"Initialized StarAttention with config: {config}" - ) - - def _split_heads( - self, tensor: torch.Tensor, num_heads: int - ) -> torch.Tensor: - """Split the last dimension into (num_heads, head_dim).""" - batch_size, seq_len, _ = tensor.size() - tensor = tensor.view( - batch_size, seq_len, num_heads, self.head_dim - ) - # Transpose to (batch_size, num_heads, seq_len, head_dim) - return tensor.transpose(1, 2) - - def _merge_heads(self, tensor: torch.Tensor) -> torch.Tensor: - """Merge the head dimension back into hidden_size.""" - batch_size, _, seq_len, _ = tensor.size() - tensor = tensor.transpose(1, 2) - return tensor.reshape( - batch_size, seq_len, self.config.hidden_size - ) - - def _compute_attention_scores( - self, - query: torch.Tensor, - key: torch.Tensor, - value: torch.Tensor, - mask: Optional[torch.Tensor] = None, - ) -> Tuple[torch.Tensor, torch.Tensor]: - """Compute attention scores and weighted values.""" - # Scale dot-product attention - scores = torch.matmul( - query, key.transpose(-2, -1) - ) / math.sqrt(self.head_dim) - - if mask is not None: - scores = scores.masked_fill(mask == 0, float("-inf")) - - # Online softmax computation - attention_probs = torch.nn.functional.softmax(scores, dim=-1) - attention_probs = self.dropout(attention_probs) - - context = torch.matmul(attention_probs, value) - - return context, attention_probs - - def phase1_local_context_encoding( - self, - input_ids: torch.Tensor, - host_id: int, - device: Union[str, torch.device] = "cuda", - ) -> None: - """ - Phase 1: Local Context Encoding with Anchor Blocks - - Args: - input_ids: Input tensor of shape (batch_size, seq_len) - host_id: ID of the current host - device: Device to run computations on - """ - logger.debug(f"Starting Phase 1 on host {host_id}") - - # Calculate block assignments - block_start = host_id * self.config.block_size - block_end = block_start + self.config.block_size - - # Get local block - local_block = input_ids[:, block_start:block_end].to(device) - - # Get anchor block (first block) - anchor_block = input_ids[:, : self.config.anchor_size].to( - device - ) - - # Compute KV pairs for local block - local_hidden = self.layer_norm(local_block) - local_key = self._split_heads( - self.key(local_hidden), self.config.num_attention_heads - ) - local_value = self._split_heads( - self.value(local_hidden), self.config.num_attention_heads - ) - - # Store in KV cache - self.kv_cache[host_id] = { - "key": local_key, - "value": local_value, - "anchor_key": ( - None - if host_id == 0 - else self._split_heads( - self.key(self.layer_norm(anchor_block)), - self.config.num_attention_heads, - ) - ), - } - - logger.debug( - f"Phase 1 complete on host {host_id}. KV cache shapes - " - f"key: {local_key.shape}, value: {local_value.shape}" - ) - - def phase2_query_encoding( - self, - query_input: torch.Tensor, - host_id: int, - is_query_host: bool, - device: Union[str, torch.device] = "cuda", - ) -> Optional[torch.Tensor]: - """ - Phase 2: Query Encoding and Output Generation - - Args: - query_input: Query tensor of shape (batch_size, seq_len, hidden_size) - host_id: ID of the current host - is_query_host: Whether this host is the query host - device: Device to run computations on - - Returns: - Output tensor if this is the query host, None otherwise - """ - logger.debug(f"Starting Phase 2 on host {host_id}") - - # Transform query - query_hidden = self.layer_norm(query_input) - query = self._split_heads( - self.query(query_hidden), self.config.num_attention_heads - ) - - # Compute local attention scores - local_context, local_probs = self._compute_attention_scores( - query, - self.kv_cache[host_id]["key"], - self.kv_cache[host_id]["value"], - ) - - if not is_query_host: - # Non-query hosts send their local attention statistics - dist.send(local_probs, dst=self.config.num_hosts - 1) - return None - - # Query host aggregates attention from all hosts - all_attention_probs = [local_probs] - for src_rank in range(self.config.num_hosts - 1): - probs = torch.empty_like(local_probs) - dist.recv(probs, src=src_rank) - all_attention_probs.append(probs) - - # Compute global attention - torch.mean(torch.stack(all_attention_probs), dim=0) - - # Final output computation - output = self._merge_heads(local_context) - output = self.dropout(output) - - logger.debug( - f"Phase 2 complete on host {host_id}. Output shape: {output.shape}" - ) - - return output - - def forward( - self, - input_ids: torch.Tensor, - query_input: torch.Tensor, - host_id: int, - is_query_host: bool, - device: Union[str, torch.device] = "cuda", - ) -> Optional[torch.Tensor]: - """ - Forward pass of the StarAttention module. - - Args: - input_ids: Input tensor of shape (batch_size, seq_len) - query_input: Query tensor of shape (batch_size, seq_len, hidden_size) - host_id: ID of the current host - is_query_host: Whether this host is the query host - device: Device to run computations on - - Returns: - Output tensor if this is the query host, None otherwise - """ - # Phase 1: Local Context Encoding - self.phase1_local_context_encoding(input_ids, host_id, device) - - # Phase 2: Query Encoding and Output Generation - return self.phase2_query_encoding( - query_input, host_id, is_query_host, device - ) - - -# Example forward pass -config = StarAttentionConfig( - hidden_size=768, - num_attention_heads=12, - num_hosts=3, - block_size=512, - anchor_size=128, -) - -# Initialize model -model = StarAttention(config) - -# Example input tensors -batch_size = 4 -seq_len = 512 -input_ids = torch.randint( - 0, 1000, (batch_size, seq_len) -) # Random input IDs -query_input = torch.randn( - batch_size, seq_len, config.hidden_size -) # Random query input - -# Example forward pass for query host (host_id = 2) -output = model( - input_ids=input_ids, - query_input=query_input, - host_id=2, - is_query_host=True, - device="cpu", -) - -print(output) From f82a57d70ec8dbb6f56160f7d36656012657161a Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 29 Nov 2024 10:57:26 -0800 Subject: [PATCH 07/13] [CLEANUP] --- graph_swarm.py | 1 - swarms/utils/any_to_str.py | 74 ++++----- swarms/utils/calculate_func_metrics.py | 1 - swarms/utils/callable_name.py | 203 ------------------------- swarms/utils/dict_to_table.py | 0 swarms/utils/pdf_to_text.py | 10 +- swarms/utils/remove_json_whitespace.py | 51 ------- 7 files changed, 41 insertions(+), 299 deletions(-) delete mode 100644 swarms/utils/callable_name.py delete mode 100644 swarms/utils/dict_to_table.py delete mode 100644 swarms/utils/remove_json_whitespace.py diff --git a/graph_swarm.py b/graph_swarm.py index 31842225..82cef523 100644 --- a/graph_swarm.py +++ b/graph_swarm.py @@ -202,7 +202,6 @@ class GraphSwarm: self.memory_collection = memory_collection self.memory = SwarmMemory(collection_name=memory_collection) - if agents: self.initialize_agents(agents) diff --git a/swarms/utils/any_to_str.py b/swarms/utils/any_to_str.py index 125e233e..2b0e3809 100644 --- a/swarms/utils/any_to_str.py +++ b/swarms/utils/any_to_str.py @@ -63,40 +63,40 @@ def any_to_str(data: Union[str, Dict, List, Tuple, Any]) -> str: return f"Error converting data: {str(e)}" -def main(): - # Example 1: Dictionary - print("Dictionary:") - print( - any_to_str( - { - "name": "John", - "age": 30, - "hobbies": ["reading", "hiking"], - } - ) - ) - - print("\nNested Dictionary:") - print( - any_to_str( - { - "user": { - "id": 123, - "details": {"city": "New York", "active": True}, - }, - "data": [1, 2, 3], - } - ) - ) - - print("\nList and Tuple:") - print(any_to_str([1, "text", None, (1, 2)])) - print(any_to_str((True, False, None))) - - print("\nEmpty Collections:") - print(any_to_str([])) - print(any_to_str({})) - - -if __name__ == "__main__": - main() +# def main(): +# # Example 1: Dictionary +# print("Dictionary:") +# print( +# any_to_str( +# { +# "name": "John", +# "age": 30, +# "hobbies": ["reading", "hiking"], +# } +# ) +# ) + +# print("\nNested Dictionary:") +# print( +# any_to_str( +# { +# "user": { +# "id": 123, +# "details": {"city": "New York", "active": True}, +# }, +# "data": [1, 2, 3], +# } +# ) +# ) + +# print("\nList and Tuple:") +# print(any_to_str([1, "text", None, (1, 2)])) +# print(any_to_str((True, False, None))) + +# print("\nEmpty Collections:") +# print(any_to_str([])) +# print(any_to_str({})) + + +# if __name__ == "__main__": +# main() diff --git a/swarms/utils/calculate_func_metrics.py b/swarms/utils/calculate_func_metrics.py index bfb8a528..795e7bb2 100644 --- a/swarms/utils/calculate_func_metrics.py +++ b/swarms/utils/calculate_func_metrics.py @@ -4,7 +4,6 @@ from functools import wraps from typing import Any, Callable import psutil -from loguru import logger from pydantic import BaseModel from swarms.utils.loguru_logger import initialize_logger diff --git a/swarms/utils/callable_name.py b/swarms/utils/callable_name.py deleted file mode 100644 index 9a0b037f..00000000 --- a/swarms/utils/callable_name.py +++ /dev/null @@ -1,203 +0,0 @@ -from typing import Any -import inspect -from functools import partial -import logging - - -class NameResolver: - """Utility class for resolving names of various objects""" - - @staticmethod - def get_name(obj: Any, default: str = "unnamed_callable") -> str: - """ - Get the name of any object with multiple fallback strategies. - - Args: - obj: The object to get the name from - default: Default name if all strategies fail - - Returns: - str: The resolved name - """ - strategies = [ - # Try getting __name__ attribute - lambda x: getattr(x, "__name__", None), - # Try getting class name - lambda x: ( - x.__class__.__name__ - if hasattr(x, "__class__") - else None - ), - # Try getting function name if it's a partial - lambda x: ( - x.func.__name__ if isinstance(x, partial) else None - ), - # Try getting the name from the class's type - lambda x: type(x).__name__, - # Try getting qualname - lambda x: getattr(x, "__qualname__", None), - # Try getting the module and class name - lambda x: ( - f"{x.__module__}.{x.__class__.__name__}" - if hasattr(x, "__module__") - else None - ), - # For async functions - lambda x: ( - x.__name__ if inspect.iscoroutinefunction(x) else None - ), - # For classes with custom __str__ - lambda x: ( - str(x) - if hasattr(x, "__str__") - and x.__str__ != object.__str__ - else None - ), - # For wrapped functions - lambda x: ( - getattr(x, "__wrapped__", None).__name__ - if hasattr(x, "__wrapped__") - else None - ), - ] - - # Try each strategy - for strategy in strategies: - try: - name = strategy(obj) - if name and isinstance(name, str): - return name.replace(" ", "_").replace("-", "_") - except Exception: - continue - - # Return default if all strategies fail - return default - - @staticmethod - def get_callable_details(obj: Any) -> dict: - """ - Get detailed information about a callable object. - - Returns: - dict: Dictionary containing: - - name: The resolved name - - type: The type of callable - - signature: The signature if available - - module: The module name if available - - doc: The docstring if available - """ - details = { - "name": NameResolver.get_name(obj), - "type": "unknown", - "signature": None, - "module": getattr(obj, "__module__", "unknown"), - "doc": inspect.getdoc(obj) - or "No documentation available", - } - - # Determine the type - if inspect.isclass(obj): - details["type"] = "class" - elif inspect.iscoroutinefunction(obj): - details["type"] = "async_function" - elif inspect.isfunction(obj): - details["type"] = "function" - elif isinstance(obj, partial): - details["type"] = "partial" - elif callable(obj): - details["type"] = "callable" - - # Try to get signature - try: - details["signature"] = str(inspect.signature(obj)) - except (ValueError, TypeError): - details["signature"] = "Unknown signature" - - return details - - @classmethod - def get_safe_name(cls, obj: Any, max_retries: int = 3) -> str: - """ - Safely get a name with retries and validation. - - Args: - obj: Object to get name from - max_retries: Maximum number of retry attempts - - Returns: - str: A valid name string - """ - retries = 0 - last_error = None - - while retries < max_retries: - try: - name = cls.get_name(obj) - - # Validate and clean the name - if name: - # Remove invalid characters - clean_name = "".join( - c - for c in name - if c.isalnum() or c in ["_", "."] - ) - - # Ensure it starts with a letter or underscore - if ( - not clean_name[0].isalpha() - and clean_name[0] != "_" - ): - clean_name = f"_{clean_name}" - - return clean_name - - except Exception as e: - last_error = e - retries += 1 - - # If all retries failed, generate a unique fallback name - import uuid - - fallback = f"callable_{uuid.uuid4().hex[:8]}" - logging.warning( - f"Failed to get name after {max_retries} retries. Using fallback: {fallback}. " - f"Last error: {str(last_error)}" - ) - return fallback - - -# # Example usage -# if __name__ == "__main__": -# def test_resolver(): -# # Test cases -# class TestClass: -# def method(self): -# pass - -# async def async_func(): -# pass - -# test_cases = [ -# TestClass, # Class -# TestClass(), # Instance -# async_func, # Async function -# lambda x: x, # Lambda -# partial(print, end=""), # Partial -# TestClass.method, # Method -# print, # Built-in function -# str, # Built-in class -# ] - -# resolver = NameResolver() - -# print("\nName Resolution Results:") -# print("-" * 50) -# for obj in test_cases: -# details = resolver.get_callable_details(obj) -# safe_name = resolver.get_safe_name(obj) -# print(f"\nObject: {obj}") -# print(f"Safe Name: {safe_name}") -# print(f"Details: {details}") - -# test_resolver() diff --git a/swarms/utils/dict_to_table.py b/swarms/utils/dict_to_table.py deleted file mode 100644 index e69de29b..00000000 diff --git a/swarms/utils/pdf_to_text.py b/swarms/utils/pdf_to_text.py index 90711691..8df8e065 100644 --- a/swarms/utils/pdf_to_text.py +++ b/swarms/utils/pdf_to_text.py @@ -1,14 +1,12 @@ -import sys from swarms.utils.try_except_wrapper import try_except_wrapper try: import pypdf except ImportError: - print( - "pypdf not installed. Please install it using: pip install" - " pypdf" - ) - sys.exit(1) + import subprocess + + subprocess.check_call(["python", "-m", "pip", "install", "pypdf"]) + import pypdf @try_except_wrapper diff --git a/swarms/utils/remove_json_whitespace.py b/swarms/utils/remove_json_whitespace.py deleted file mode 100644 index 0a043e7c..00000000 --- a/swarms/utils/remove_json_whitespace.py +++ /dev/null @@ -1,51 +0,0 @@ -import json - -import yaml - - -def remove_whitespace_from_json(json_string: str) -> str: - """ - Removes unnecessary whitespace from a JSON string. - - This function parses the JSON string into a Python object and then - serializes it back into a JSON string without unnecessary whitespace. - - Args: - json_string (str): The JSON string. - - Returns: - str: The JSON string with whitespace removed. - """ - parsed = json.loads(json_string) - return json.dumps(parsed, separators=(",", ":")) - - -# # Example usage for JSON -# json_string = '{"field1": 123, "field2": "example text"}' -# print(remove_whitespace_from_json(json_string)) - - -def remove_whitespace_from_yaml(yaml_string: str) -> str: - """ - Removes unnecessary whitespace from a YAML string. - - This function parses the YAML string into a Python object and then - serializes it back into a YAML string with minimized whitespace. - Note: This might change the representation style of YAML data. - - Args: - yaml_string (str): The YAML string. - - Returns: - str: The YAML string with whitespace reduced. - """ - parsed = yaml.safe_load(yaml_string) - return yaml.dump(parsed, default_flow_style=True) - - -# # Example usage for YAML -# yaml_string = """ -# field1: 123 -# field2: example text -# """ -# print(remove_whitespace_from_yaml(yaml_string)) From b51840c12dc027984ab45edfb057b393f0e0196d Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 29 Nov 2024 11:04:22 -0800 Subject: [PATCH 08/13] [REMOVE ARTIFACTS FROM SWARMS.ARTIFACTS] --- requirements.txt | 2 +- swarms/artifacts/__init__.py | 5 +- swarms/artifacts/base_artifact.py | 77 ------------------------------- swarms/artifacts/text_artifact.py | 58 ----------------------- 4 files changed, 2 insertions(+), 140 deletions(-) delete mode 100644 swarms/artifacts/base_artifact.py delete mode 100644 swarms/artifacts/text_artifact.py diff --git a/requirements.txt b/requirements.txt index ca9fdcdd..e5375a0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ asyncio>=3.4.3,<4.0 toml pypdf==4.3.1 ratelimit==2.2.1 -loguru==0.7.2 +loguru pydantic==2.8.2 tenacity rich diff --git a/swarms/artifacts/__init__.py b/swarms/artifacts/__init__.py index 448d6101..d52375b4 100644 --- a/swarms/artifacts/__init__.py +++ b/swarms/artifacts/__init__.py @@ -1,9 +1,6 @@ -from swarms.artifacts.base_artifact import BaseArtifact -from swarms.artifacts.text_artifact import TextArtifact + from swarms.artifacts.main_artifact import Artifact __all__ = [ - "BaseArtifact", - "TextArtifact", "Artifact", ] diff --git a/swarms/artifacts/base_artifact.py b/swarms/artifacts/base_artifact.py deleted file mode 100644 index aad07a7b..00000000 --- a/swarms/artifacts/base_artifact.py +++ /dev/null @@ -1,77 +0,0 @@ -from __future__ import annotations - -import json -import uuid -from abc import ABC, abstractmethod -from dataclasses import dataclass -from typing import Any - - -@dataclass -class BaseArtifact(ABC): - """ - Base class for artifacts. - """ - - id: str - name: str - value: Any - - def __post_init__(self): - if self.id is None: - self.id = uuid.uuid4().hex - if self.name is None: - self.name = self.id - - @classmethod - def value_to_bytes(cls, value: Any) -> bytes: - """ - Convert the value to bytes. - """ - if isinstance(value, bytes): - return value - else: - return str(value).encode() - - @classmethod - def value_to_dict(cls, value: Any) -> dict: - """ - Convert the value to a dictionary. - """ - if isinstance(value, dict): - dict_value = value - else: - dict_value = json.loads(value) - - return {k: v for k, v in dict_value.items()} - - def to_text(self) -> str: - """ - Convert the value to text. - """ - return str(self.value) - - def __str__(self) -> str: - """ - Return a string representation of the artifact. - """ - return self.to_text() - - def __bool__(self) -> bool: - """ - Return the boolean value of the artifact. - """ - return bool(self.value) - - def __len__(self) -> int: - """ - Return the length of the artifact. - """ - return len(self.value) - - @abstractmethod - def __add__(self, other: BaseArtifact) -> BaseArtifact: - """ - Add two artifacts together. - """ - ... diff --git a/swarms/artifacts/text_artifact.py b/swarms/artifacts/text_artifact.py deleted file mode 100644 index 13ca4dfd..00000000 --- a/swarms/artifacts/text_artifact.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Callable -from swarms.artifacts.base_artifact import BaseArtifact - - -@dataclass -class TextArtifact(BaseArtifact): - """ - Represents a text artifact. - - Attributes: - value (str): The text value of the artifact. - encoding (str, optional): The encoding of the text (default is "utf-8"). - encoding_error_handler (str, optional): The error handler for encoding errors (default is "strict"). - _embedding (list[float]): The embedding of the text artifact (default is an empty list). - - Properties: - embedding (Optional[list[float]]): The embedding of the text artifact. - - Methods: - __add__(self, other: BaseArtifact) -> TextArtifact: Concatenates the text value of the artifact with another artifact. - __bool__(self) -> bool: Checks if the text value of the artifact is non-empty. - generate_embedding(self, driver: BaseEmbeddingModel) -> Optional[list[float]]: Generates the embedding of the text artifact using a given embedding model. - token_count(self, tokenizer: BaseTokenizer) -> int: Counts the number of tokens in the text artifact using a given tokenizer. - to_bytes(self) -> bytes: Converts the text value of the artifact to bytes using the specified encoding and error handler. - """ - - value: str - encoding: str = "utf-8" - encoding_error_handler: str = "strict" - tokenizer: Callable = None - _embedding: list[float] = field(default_factory=list) - - @property - def embedding(self) -> list[float] | None: - return None if len(self._embedding) == 0 else self._embedding - - def __add__(self, other: BaseArtifact) -> TextArtifact: - return TextArtifact(self.value + other.value) - - def __bool__(self) -> bool: - return bool(self.value.strip()) - - def generate_embedding(self, model) -> list[float] | None: - self._embedding.clear() - self._embedding.extend(model.embed_string(str(self.value))) - - return self.embedding - - def token_count(self) -> int: - return self.tokenizer.count_tokens(str(self.value)) - - def to_bytes(self) -> bytes: - return self.value.encode( - encoding=self.encoding, errors=self.encoding_error_handler - ) From b4945bbd0a4307310ef51e47358f25f44bc8a098 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 29 Nov 2024 11:45:08 -0800 Subject: [PATCH 09/13] [CLEANUP] --- no_llm_agent.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/no_llm_agent.py b/no_llm_agent.py index 2fcbb8f9..3521c677 100644 --- a/no_llm_agent.py +++ b/no_llm_agent.py @@ -3,5 +3,7 @@ from swarms import Agent Agent( agent_name="Stock-Analysis-Agent", model_name="gpt-4o-mini", - max_loops=1, + max_loops="auto", + streaming_on=True, + interactive=True, ).run("What are 5 hft algorithms") From 248b10195c22e7b544cb4b1552ce121c17bed06f Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 29 Nov 2024 11:48:09 -0800 Subject: [PATCH 10/13] [CLEANUP CSS DOCS] --- docs/assets/css/extra.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/assets/css/extra.css b/docs/assets/css/extra.css index 8a8a758f..a9967e01 100644 --- a/docs/assets/css/extra.css +++ b/docs/assets/css/extra.css @@ -8,7 +8,7 @@ display: table; } -/* Dark mode */ +/* Dark mode [data-md-color-scheme="slate"] { --md-default-bg-color: black; } @@ -24,4 +24,4 @@ .md-header.md-header--shadow { color: black; -} \ No newline at end of file +} */ \ No newline at end of file From 241553a9764bcdd358411d6235285536aa8f4316 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 29 Nov 2024 20:45:58 -0800 Subject: [PATCH 11/13] [CLEANUP] --- agent_api.py | 629 +++++++++++++++++++++++++++++++++++ agent_api_test.py | 99 ++++++ swarms/artifacts/__init__.py | 1 - 3 files changed, 728 insertions(+), 1 deletion(-) create mode 100644 agent_api.py create mode 100644 agent_api_test.py diff --git a/agent_api.py b/agent_api.py new file mode 100644 index 00000000..d1968d9d --- /dev/null +++ b/agent_api.py @@ -0,0 +1,629 @@ +import os +from fastapi import ( + FastAPI, + HTTPException, + status, + Query, + BackgroundTasks, +) +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field +from typing import Optional, Dict, Any, List +from loguru import logger +import uvicorn +from datetime import datetime, timedelta +from uuid import UUID, uuid4 +from enum import Enum +from pathlib import Path +from concurrent.futures import ThreadPoolExecutor +import traceback + +from swarms import Agent +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Configure Loguru +logger.add( + "logs/api_{time}.log", + rotation="500 MB", + retention="10 days", + level="INFO", + format="{time} {level} {message}", + backtrace=True, + diagnose=True, +) + + +class AgentStatus(str, Enum): + """Enum for agent status.""" + + IDLE = "idle" + PROCESSING = "processing" + ERROR = "error" + MAINTENANCE = "maintenance" + + +class AgentConfig(BaseModel): + """Configuration model for creating a new agent.""" + + agent_name: str = Field(..., description="Name of the agent") + model_name: str = Field( + ..., + description="Name of the llm you want to use provided by litellm", + ) + description: str = Field( + default="", description="Description of the agent's purpose" + ) + system_prompt: str = Field( + ..., description="System prompt for the agent" + ) + model_name: str = Field( + default="gpt-4", description="Model name to use" + ) + temperature: float = Field( + default=0.1, + ge=0.0, + le=2.0, + description="Temperature for the model", + ) + max_loops: int = Field( + default=1, ge=1, description="Maximum number of loops" + ) + autosave: bool = Field( + default=True, description="Enable autosave" + ) + dashboard: bool = Field( + default=False, description="Enable dashboard" + ) + verbose: bool = Field( + default=True, description="Enable verbose output" + ) + dynamic_temperature_enabled: bool = Field( + default=True, description="Enable dynamic temperature" + ) + user_name: str = Field( + default="default_user", description="Username for the agent" + ) + retry_attempts: int = Field( + default=1, ge=1, description="Number of retry attempts" + ) + context_length: int = Field( + default=200000, ge=1000, description="Context length" + ) + output_type: str = Field( + default="string", description="Output type (string or json)" + ) + streaming_on: bool = Field( + default=False, description="Enable streaming" + ) + tags: List[str] = Field( + default_factory=list, + description="Tags for categorizing the agent", + ) + + +class AgentUpdate(BaseModel): + """Model for updating agent configuration.""" + + description: Optional[str] = None + system_prompt: Optional[str] = None + temperature: Optional[float] = None + max_loops: Optional[int] = None + tags: Optional[List[str]] = None + status: Optional[AgentStatus] = None + + +class AgentSummary(BaseModel): + """Summary model for agent listing.""" + + agent_id: UUID + agent_name: str + description: str + created_at: datetime + last_used: datetime + total_completions: int + tags: List[str] + status: AgentStatus + + +class AgentMetrics(BaseModel): + """Model for agent performance metrics.""" + + total_completions: int + average_response_time: float + error_rate: float + last_24h_completions: int + total_tokens_used: int + uptime_percentage: float + success_rate: float + peak_tokens_per_minute: int + + +class CompletionRequest(BaseModel): + """Model for completion requests.""" + + prompt: str = Field(..., description="The prompt to process") + agent_id: UUID = Field(..., description="ID of the agent to use") + max_tokens: Optional[int] = Field( + None, description="Maximum tokens to generate" + ) + temperature_override: Optional[float] = None + stream: bool = Field( + default=False, description="Enable streaming response" + ) + + +class CompletionResponse(BaseModel): + """Model for completion responses.""" + + agent_id: UUID + response: str + metadata: Dict[str, Any] + timestamp: datetime + processing_time: float + token_usage: Dict[str, int] + + +class AgentStore: + """Enhanced store for managing agents.""" + + def __init__(self): + self.agents: Dict[UUID, Agent] = {} + self.agent_metadata: Dict[UUID, Dict[str, Any]] = {} + self.executor = ThreadPoolExecutor(max_workers=4) + self._ensure_directories() + + def _ensure_directories(self): + """Ensure required directories exist.""" + Path("logs").mkdir(exist_ok=True) + Path("states").mkdir(exist_ok=True) + + async def create_agent(self, config: AgentConfig) -> UUID: + """Create a new agent with the given configuration.""" + try: + + agent = Agent( + agent_name=config.agent_name, + system_prompt=config.system_prompt, + model_name=config.model_name, + max_loops=config.max_loops, + autosave=config.autosave, + dashboard=config.dashboard, + verbose=config.verbose, + dynamic_temperature_enabled=config.dynamic_temperature_enabled, + saved_state_path=f"states/{config.agent_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", + user_name=config.user_name, + retry_attempts=config.retry_attempts, + context_length=config.context_length, + return_step_meta=True, + output_type="str", + streaming_on=config.streaming_on, + ) + + agent_id = uuid4() + self.agents[agent_id] = agent + self.agent_metadata[agent_id] = { + "description": config.description, + "created_at": datetime.utcnow(), + "last_used": datetime.utcnow(), + "total_completions": 0, + "tags": config.tags, + "total_tokens": 0, + "error_count": 0, + "response_times": [], + "status": AgentStatus.IDLE, + "start_time": datetime.utcnow(), + "downtime": timedelta(), + "successful_completions": 0, + } + + logger.info(f"Created agent with ID: {agent_id}") + return agent_id + + except Exception as e: + logger.error(f"Error creating agent: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to create agent: {str(e)}", + ) + + async def get_agent(self, agent_id: UUID) -> Agent: + """Retrieve an agent by ID.""" + agent = self.agents.get(agent_id) + if not agent: + logger.error(f"Agent not found: {agent_id}") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Agent {agent_id} not found", + ) + return agent + + async def update_agent( + self, agent_id: UUID, update: AgentUpdate + ) -> None: + """Update agent configuration.""" + agent = await self.get_agent(agent_id) + metadata = self.agent_metadata[agent_id] + + if update.system_prompt: + agent.system_prompt = update.system_prompt + if update.temperature is not None: + agent.llm.temperature = update.temperature + if update.max_loops is not None: + agent.max_loops = update.max_loops + if update.tags is not None: + metadata["tags"] = update.tags + if update.description is not None: + metadata["description"] = update.description + if update.status is not None: + metadata["status"] = update.status + if update.status == AgentStatus.MAINTENANCE: + metadata["downtime"] += ( + datetime.utcnow() - metadata["last_used"] + ) + + logger.info(f"Updated agent {agent_id}") + + async def list_agents( + self, + tags: Optional[List[str]] = None, + status: Optional[AgentStatus] = None, + ) -> List[AgentSummary]: + """List all agents, optionally filtered by tags and status.""" + summaries = [] + for agent_id, agent in self.agents.items(): + metadata = self.agent_metadata[agent_id] + + # Apply filters + if tags and not any( + tag in metadata["tags"] for tag in tags + ): + continue + if status and metadata["status"] != status: + continue + + summaries.append( + AgentSummary( + agent_id=agent_id, + agent_name=agent.agent_name, + description=metadata["description"], + created_at=metadata["created_at"], + last_used=metadata["last_used"], + total_completions=metadata["total_completions"], + tags=metadata["tags"], + status=metadata["status"], + ) + ) + return summaries + + async def get_agent_metrics(self, agent_id: UUID) -> AgentMetrics: + """Get performance metrics for an agent.""" + metadata = self.agent_metadata[agent_id] + response_times = metadata["response_times"] + + # Calculate metrics + total_time = datetime.utcnow() - metadata["start_time"] + uptime = total_time - metadata["downtime"] + uptime_percentage = ( + uptime.total_seconds() / total_time.total_seconds() + ) * 100 + + success_rate = ( + metadata["successful_completions"] + / metadata["total_completions"] + * 100 + if metadata["total_completions"] > 0 + else 0 + ) + + return AgentMetrics( + total_completions=metadata["total_completions"], + average_response_time=( + sum(response_times) / len(response_times) + if response_times + else 0 + ), + error_rate=( + metadata["error_count"] + / metadata["total_completions"] + if metadata["total_completions"] > 0 + else 0 + ), + last_24h_completions=sum( + 1 + for t in response_times + if (datetime.utcnow() - t).days < 1 + ), + total_tokens_used=metadata["total_tokens"], + uptime_percentage=uptime_percentage, + success_rate=success_rate, + peak_tokens_per_minute=max( + metadata.get("tokens_per_minute", [0]) + ), + ) + + async def clone_agent( + self, agent_id: UUID, new_name: str + ) -> UUID: + """Clone an existing agent with a new name.""" + original_agent = await self.get_agent(agent_id) + original_metadata = self.agent_metadata[agent_id] + + config = AgentConfig( + agent_name=new_name, + description=f"Clone of {original_agent.agent_name}", + system_prompt=original_agent.system_prompt, + model_name=original_agent.llm.model_name, + temperature=original_agent.llm.temperature, + max_loops=original_agent.max_loops, + tags=original_metadata["tags"], + ) + + return await self.create_agent(config) + + async def delete_agent(self, agent_id: UUID) -> None: + """Delete an agent.""" + if agent_id not in self.agents: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Agent {agent_id} not found", + ) + + # Clean up any resources + agent = self.agents[agent_id] + if agent.autosave and os.path.exists(agent.saved_state_path): + os.remove(agent.saved_state_path) + + del self.agents[agent_id] + del self.agent_metadata[agent_id] + logger.info(f"Deleted agent {agent_id}") + + async def process_completion( + self, + agent: Agent, + prompt: str, + agent_id: UUID, + max_tokens: Optional[int] = None, + temperature_override: Optional[float] = None, + ) -> CompletionResponse: + """Process a completion request using the specified agent.""" + start_time = datetime.utcnow() + metadata = self.agent_metadata[agent_id] + + try: + # Update agent status + metadata["status"] = AgentStatus.PROCESSING + metadata["last_used"] = start_time + + # Apply temporary overrides if specified + original_temp = agent.llm.temperature + if temperature_override is not None: + agent.llm.temperature = temperature_override + + # Process the completion + response = agent.run(prompt) + + # Reset overrides + if temperature_override is not None: + agent.llm.temperature = original_temp + + # Update metrics + processing_time = ( + datetime.utcnow() - start_time + ).total_seconds() + metadata["response_times"].append(processing_time) + metadata["total_completions"] += 1 + metadata["successful_completions"] += 1 + + # Estimate token usage (this is a rough estimate) + prompt_tokens = len(prompt.split()) * 1.3 + completion_tokens = len(response.split()) * 1.3 + total_tokens = int(prompt_tokens + completion_tokens) + metadata["total_tokens"] += total_tokens + + # Update tokens per minute tracking + current_minute = datetime.utcnow().replace( + second=0, microsecond=0 + ) + if "tokens_per_minute" not in metadata: + metadata["tokens_per_minute"] = {} + metadata["tokens_per_minute"][current_minute] = ( + metadata["tokens_per_minute"].get(current_minute, 0) + + total_tokens + ) + + return CompletionResponse( + agent_id=agent_id, + response=response, + metadata={ + "agent_name": agent.agent_name, + "model_name": agent.llm.model_name, + "temperature": agent.llm.temperature, + }, + timestamp=datetime.utcnow(), + processing_time=processing_time, + token_usage={ + "prompt_tokens": int(prompt_tokens), + "completion_tokens": int(completion_tokens), + "total_tokens": total_tokens, + }, + ) + + except Exception as e: + metadata["error_count"] += 1 + metadata["status"] = AgentStatus.ERROR + logger.error( + f"Error in completion processing: {str(e)}\n{traceback.format_exc()}" + ) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error processing completion: {str(e)}", + ) + finally: + metadata["status"] = AgentStatus.IDLE + + +class SwarmsAPI: + """Enhanced API class for Swarms agent integration.""" + + def __init__(self): + self.app = FastAPI( + title="Swarms Agent API", + description="Production-grade API for Swarms agent interaction", + version="1.0.0", + docs_url="/v1/docs", + redoc_url="/v1/redoc", + ) + self.store = AgentStore() + # Configure CORS + self.app.add_middleware( + CORSMiddleware, + allow_origins=[ + "*" + ], # Configure appropriately for production + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + self._setup_routes() + + def _setup_routes(self): + """Set up API routes.""" + + @self.app.post("/v1/agent", response_model=Dict[str, UUID]) + async def create_agent(config: AgentConfig): + """Create a new agent with the specified configuration.""" + agent_id = await self.store.create_agent(config) + return {"agent_id": agent_id} + + @self.app.get("/v1/agents", response_model=List[AgentSummary]) + async def list_agents( + tags: Optional[List[str]] = Query(None), + status: Optional[AgentStatus] = None, + ): + """List all agents, optionally filtered by tags and status.""" + return await self.store.list_agents(tags, status) + + @self.app.patch( + "/v1/agent/{agent_id}", response_model=Dict[str, str] + ) + async def update_agent(agent_id: UUID, update: AgentUpdate): + """Update an existing agent's configuration.""" + await self.store.update_agent(agent_id, update) + return {"status": "updated"} + + @self.app.get( + "/v1/agent/{agent_id}/metrics", + response_model=AgentMetrics, + ) + async def get_agent_metrics(agent_id: UUID): + """Get performance metrics for a specific agent.""" + return await self.store.get_agent_metrics(agent_id) + + @self.app.post( + "/v1/agent/{agent_id}/clone", + response_model=Dict[str, UUID], + ) + async def clone_agent(agent_id: UUID, new_name: str): + """Clone an existing agent with a new name.""" + new_id = await self.store.clone_agent(agent_id, new_name) + return {"agent_id": new_id} + + @self.app.delete("/v1/agent/{agent_id}") + async def delete_agent(agent_id: UUID): + """Delete an agent.""" + await self.store.delete_agent(agent_id) + return {"status": "deleted"} + + @self.app.post( + "/v1/agent/completions", response_model=CompletionResponse + ) + async def create_completion( + request: CompletionRequest, + background_tasks: BackgroundTasks, + ): + """Process a completion request with the specified agent.""" + try: + agent = await self.store.get_agent(request.agent_id) + + # Process completion + response = await self.store.process_completion( + agent, + request.prompt, + request.agent_id, + request.max_tokens, + request.temperature_override, + ) + + # Schedule background cleanup + background_tasks.add_task( + self._cleanup_old_metrics, request.agent_id + ) + + return response + + except Exception as e: + logger.error(f"Error processing completion: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error processing completion: {str(e)}", + ) + + @self.app.get("/v1/agent/{agent_id}/status") + async def get_agent_status(agent_id: UUID): + """Get the current status of an agent.""" + metadata = self.store.agent_metadata.get(agent_id) + if not metadata: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Agent {agent_id} not found", + ) + return { + "agent_id": agent_id, + "status": metadata["status"], + "last_used": metadata["last_used"], + "total_completions": metadata["total_completions"], + "error_count": metadata["error_count"], + } + + async def _cleanup_old_metrics(self, agent_id: UUID): + """Clean up old metrics data to prevent memory bloat.""" + metadata = self.store.agent_metadata.get(agent_id) + if metadata: + # Keep only last 24 hours of response times + cutoff = datetime.utcnow() - timedelta(days=1) + metadata["response_times"] = [ + t + for t in metadata["response_times"] + if isinstance(t, (int, float)) + and t > cutoff.timestamp() + ] + + # Clean up old tokens per minute data + if "tokens_per_minute" in metadata: + metadata["tokens_per_minute"] = { + k: v + for k, v in metadata["tokens_per_minute"].items() + if k > cutoff + } + + +def create_app() -> FastAPI: + """Create and configure the FastAPI application.""" + api = SwarmsAPI() + return api.app + + +if __name__ == "__main__": + # Configure uvicorn logging + logger.info("API Starting") + uvicorn.run( + "main:create_app", + host="0.0.0.0", + port=8000, + reload=True, + workers=4, + ) diff --git a/agent_api_test.py b/agent_api_test.py new file mode 100644 index 00000000..74f519d0 --- /dev/null +++ b/agent_api_test.py @@ -0,0 +1,99 @@ +import requests +from loguru import logger +import time + +# Configure loguru +logger.add( + "api_tests_{time}.log", + rotation="100 MB", + level="DEBUG", + format="{time} {level} {message}" +) + +BASE_URL = "http://localhost:8000/v1" + +def test_create_agent(): + """Test creating a new agent.""" + logger.info("Testing agent creation") + + payload = { + "agent_name": "Test Agent", + "system_prompt": "You are a helpful assistant", + "model_name": "gpt-4", + "description": "Test agent", + "tags": ["test"] + } + + response = requests.post(f"{BASE_URL}/agent", json=payload) + logger.debug(f"Create response: {response.json()}") + + if response.status_code == 200: + logger.success("Successfully created agent") + return response.json()["agent_id"] + else: + logger.error(f"Failed to create agent: {response.text}") + return None + +def test_list_agents(): + """Test listing all agents.""" + logger.info("Testing agent listing") + + response = requests.get(f"{BASE_URL}/agents") + logger.debug(f"List response: {response.json()}") + + if response.status_code == 200: + logger.success(f"Found {len(response.json())} agents") + else: + logger.error(f"Failed to list agents: {response.text}") + +def test_completion(agent_id): + """Test running a completion.""" + logger.info("Testing completion") + + payload = { + "prompt": "What is the weather like today?", + "agent_id": agent_id + } + + response = requests.post(f"{BASE_URL}/agent/completions", json=payload) + logger.debug(f"Completion response: {response.json()}") + + if response.status_code == 200: + logger.success("Successfully got completion") + else: + logger.error(f"Failed to get completion: {response.text}") + +def test_delete_agent(agent_id): + """Test deleting an agent.""" + logger.info("Testing agent deletion") + + response = requests.delete(f"{BASE_URL}/agent/{agent_id}") + logger.debug(f"Delete response: {response.json()}") + + if response.status_code == 200: + logger.success("Successfully deleted agent") + else: + logger.error(f"Failed to delete agent: {response.text}") + +def run_tests(): + """Run all tests in sequence.""" + logger.info("Starting API tests") + + # Create agent and get ID + agent_id = test_create_agent() + if not agent_id: + logger.error("Cannot continue tests without agent ID") + return + + # Wait a bit for agent to be ready + time.sleep(1) + + # Run other tests + test_list_agents() + test_completion(agent_id) + test_delete_agent(agent_id) + + logger.info("Tests completed") + +if __name__ == "__main__": + run_tests() \ No newline at end of file diff --git a/swarms/artifacts/__init__.py b/swarms/artifacts/__init__.py index d52375b4..a1a027b4 100644 --- a/swarms/artifacts/__init__.py +++ b/swarms/artifacts/__init__.py @@ -1,4 +1,3 @@ - from swarms.artifacts.main_artifact import Artifact __all__ = [ From c45d73c3221ecad5fe1bba708345fa72da65d171 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 30 Nov 2024 10:26:44 -0800 Subject: [PATCH 12/13] [FOLDER CLEANUP] --- agent_api.py => api/agent_api.py | 0 agent_api_test.py => api/agent_api_test.py | 0 no_llm_agent.py => simple_example.py | 0 auto_swarm_builder.py => swarms/structs/auto_swarm_builder.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename agent_api.py => api/agent_api.py (100%) rename agent_api_test.py => api/agent_api_test.py (100%) rename no_llm_agent.py => simple_example.py (100%) rename auto_swarm_builder.py => swarms/structs/auto_swarm_builder.py (100%) diff --git a/agent_api.py b/api/agent_api.py similarity index 100% rename from agent_api.py rename to api/agent_api.py diff --git a/agent_api_test.py b/api/agent_api_test.py similarity index 100% rename from agent_api_test.py rename to api/agent_api_test.py diff --git a/no_llm_agent.py b/simple_example.py similarity index 100% rename from no_llm_agent.py rename to simple_example.py diff --git a/auto_swarm_builder.py b/swarms/structs/auto_swarm_builder.py similarity index 100% rename from auto_swarm_builder.py rename to swarms/structs/auto_swarm_builder.py From 3ab65bf5592bca6643875286889b8bbeafcd3bd7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 30 Nov 2024 10:27:09 -0800 Subject: [PATCH 13/13] [FOLDER CLEANUP] --- graph_swarm.py => swarms/structs/graph_swarm.py | 0 pulsar_swarm.py => swarms/structs/pulsar_swarm.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename graph_swarm.py => swarms/structs/graph_swarm.py (100%) rename pulsar_swarm.py => swarms/structs/pulsar_swarm.py (100%) diff --git a/graph_swarm.py b/swarms/structs/graph_swarm.py similarity index 100% rename from graph_swarm.py rename to swarms/structs/graph_swarm.py diff --git a/pulsar_swarm.py b/swarms/structs/pulsar_swarm.py similarity index 100% rename from pulsar_swarm.py rename to swarms/structs/pulsar_swarm.py