Skip to content

Commit

Permalink
Merge branch 'dev/genesis-cli'
Browse files Browse the repository at this point in the history
# Conflicts:
#	agency_swarm/agency/agency.py
  • Loading branch information
VRSEN committed Feb 14, 2024
2 parents 09b73bd + 96e070c commit 427cf21
Show file tree
Hide file tree
Showing 67 changed files with 703 additions and 467 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,19 @@ Get completion from the agency:
completion_output = agency.get_completion("Please create a new website for our client.", yield_messages=False)
```

## Creating Agent Templates Locally (CLI)
# CLI

## Genesis Agency

The `genesis` command starts the genesis agency in your terminal to help you create new agencies and agents.

#### **Command Syntax:**

```bash
agency-swarm genesis [--openai_key "YOUR_API_KEY"]
```

## Creating Agent Templates Locally

This CLI command simplifies the process of creating a structured environment for each agent.

Expand All @@ -188,6 +200,7 @@ When you run the `create-agent-template` command, it creates the following folde
└── AgentName/ # Directory for the specific agent
├── files/ # Directory for files that will be uploaded to openai
├── schemas/ # Directory for OpenAPI schemas to be converted into tools
├── tools/ # Directory for tools to be imported by default.
├── AgentName.py # The main agent class file
├── __init__.py # Initializes the agent folder as a Python package
├── instructions.md or .txt # Instruction document for the agent
Expand All @@ -200,7 +213,7 @@ This structure ensures that each agent has its dedicated space with all necessar
## Future Enhancements

1. [x] Creation of agencies that can autonomously create other agencies.
2. [ ] Asynchronous communication and task handling.
2. [x] Asynchronous communication and task handling.
3. [ ] Inter-agency communication for a self-expanding system.

## Contributing
Expand Down
192 changes: 150 additions & 42 deletions agency_swarm/agency/agency.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import inspect
import json
import os
import readline
import shutil
import uuid
from enum import Enum
from typing import List, TypedDict, Callable, Any, Dict, Literal
Expand Down Expand Up @@ -61,6 +63,8 @@ def __init__(self,
self.ceo = None
self.agents = []
self.agents_and_threads = {}
self.main_recipients = []
self.recipient_agents = None
self.shared_files = shared_files if shared_files else []
self.settings_path = settings_path
self.settings_callbacks = settings_callbacks
Expand All @@ -81,20 +85,21 @@ def __init__(self,
self.user = User()
self.main_thread = Thread(self.user, self.ceo)

def get_completion(self, message: str, message_files=None, yield_messages=True):
def get_completion(self, message: str, message_files=None, yield_messages=True, recipient_agent=None):
"""
Retrieves the completion for a given message from the main thread.
Parameters:
message (str): The message for which completion is to be retrieved.
message_files (list, optional): A list of file ids to be sent as attachments with the message. Defaults to None.
yield_messages (bool, optional): Flag to determine if intermediate messages should be yielded. Defaults to True.
recipient_agent (Agent, optional): The agent to which the message should be sent. Defaults to the first agent in the agency chart.
Returns:
Generator or final response: Depending on the 'yield_messages' flag, this method returns either a generator yielding intermediate messages or the final response from the main thread.
"""
gen = self.main_thread.get_completion(message=message, message_files=message_files,
yield_messages=yield_messages)
yield_messages=yield_messages, recipient_agent=recipient_agent)

if not yield_messages:
while True:
Expand All @@ -105,7 +110,7 @@ def get_completion(self, message: str, message_files=None, yield_messages=True):

return gen

def demo_gradio(self, height=600, dark_mode=True, share=False):
def demo_gradio(self, height=450, dark_mode=True, share=False):
"""
Launches a Gradio-based demo interface for the agency chatbot.
Expand All @@ -132,19 +137,77 @@ def demo_gradio(self, height=600, dark_mode=True, share=False):
else:
js = js.replace("{theme}", "light")

message_file_ids = []
message_file_names = None
recipient_agents = [agent.name for agent in self.main_recipients]
recipient_agent = self.main_recipients[0]

with gr.Blocks(js=js) as demo:
chatbot = gr.Chatbot(height=height)
msg = gr.Textbox()
with gr.Row():
with gr.Column(scale=9):
dropdown = gr.Dropdown(label="Recipient Agent", choices=recipient_agents,
value=recipient_agent.name)
msg = gr.Textbox(label="Your Message", lines=4)
with gr.Column(scale=1):
file_upload = gr.Files(label="Files", type="filepath")
button = gr.Button(value="Send", variant="primary")

def handle_dropdown_change(selected_option):
nonlocal recipient_agent
recipient_agent = self.get_agent_by_name(selected_option)

def handle_file_upload(file_list):
nonlocal message_file_ids
nonlocal message_file_names
message_file_ids = []
message_file_names = []
if file_list:
try:
for file_obj in file_list:
with open(file_obj.name, 'rb') as f:
# Upload the file to OpenAI
file = self.main_thread.client.files.create(
file=f,
purpose="assistants"
)
message_file_ids.append(file.id)
message_file_names.append(file.filename)
print(f"Uploaded file ID: {file.id}")
return message_file_ids
except Exception as e:
print(f"Error: {e}")
return str(e)

return "No files uploaded"

def user(user_message, history):
if history is None:
history = []

original_user_message = user_message

# Append the user message with a placeholder for bot response
user_message = "👤 User: " + user_message.strip()
return "", history + [[user_message, None]]
if recipient_agent:
user_message = f"👤 User 🗣️ @{recipient_agent.name}:\n" + user_message.strip()
else:
user_message = f"👤 User:" + user_message.strip()

def bot(history):
# Replace this with your actual chatbot logic
gen = self.get_completion(message=history[-1][0])
nonlocal message_file_names
if message_file_names:
user_message += "\n\n📎 Files:\n" + "\n".join(message_file_names)

return original_user_message, history + [[user_message, None]]

def bot(original_message, history):
nonlocal message_file_ids
nonlocal message_file_names
nonlocal recipient_agent
print("Message files: ", message_file_ids)
# Replace this with your actual chatbot logic
gen = self.get_completion(message=original_message, message_files=message_file_ids, recipient_agent=recipient_agent)
message_file_ids = []
message_file_names = []
try:
# Yield each message from the generator
for bot_message in gen:
Expand All @@ -154,14 +217,23 @@ def bot(history):
message = bot_message.get_sender_emoji() + " " + bot_message.get_formatted_content()

history.append((None, message))
yield history
yield "", history
except StopIteration:
# Handle the end of the conversation if necessary

pass

# Chain the events
button.click(
user,
inputs=[msg, chatbot],
outputs=[msg, chatbot]
).then(
bot, [msg, chatbot], [msg, chatbot]
)
dropdown.change(handle_dropdown_change, dropdown)
file_upload.change(handle_file_upload, file_upload)
msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False).then(
bot, chatbot, chatbot
bot, [msg, chatbot], [msg, chatbot]
)

# Enable queuing for streaming intermediate outputs
Expand All @@ -171,23 +243,54 @@ def bot(history):
demo.launch(share=share)
return demo

def run_demo(self):
def recipient_agent_completer(self, text, state):
"""
Autocomplete completer for recipient agent names.
"""
Runs a demonstration of the agency's capabilities in an interactive command line interface.
options = [agent for agent in self.recipient_agents if agent.lower().startswith(text.lower())]
if state < len(options):
return options[state]
else:
return None

This function continuously prompts the user for input and displays responses from the agency's main thread. It leverages the generator pattern for asynchronous message processing.
def setup_autocomplete(self):
"""
Sets up readline with the completer function.
"""
self.recipient_agents = [agent.name for agent in self.main_recipients] # Cache recipient agents for autocomplete
readline.set_completer(self.recipient_agent_completer)
readline.parse_and_bind('tab: complete')

Output:
Outputs the responses from the agency's main thread to the command line.
def run_demo(self):
"""
Enhanced run_demo with autocomplete for recipient agent names.
"""
self.setup_autocomplete() # Prepare readline for autocomplete

while True:
console.rule()
text = input("USER: ")
text = input("👤 USER: ")

if text.lower() == "exit":
break

recipient_agent = None
if "@" in text:
recipient_agent = text.split("@")[1].split(" ")[0]
text = text.replace(f"@{recipient_agent}", "").strip()
try:
recipient_agent = [agent for agent in self.recipient_agents if agent.lower() == recipient_agent.lower()][0]
recipient_agent = self.get_agent_by_name(recipient_agent)
except Exception as e:
print(f"Recipient agent {recipient_agent} not found.")
continue

try:
gen = self.main_thread.get_completion(message=text)
gen = self.main_thread.get_completion(message=text, recipient_agent=recipient_agent)
while True:
message = next(gen)
if message.sender_name.lower() == "user":
continue
message.cprint()
except StopIteration as e:
pass
Expand Down Expand Up @@ -291,12 +394,20 @@ def _parse_agency_chart(self, agency_chart):
If a node is a list, it iterates through the agents in the list, adding them to the agency and establishing communication
threads between them. It raises an exception if the agency chart is invalid or if multiple CEOs are defined.
"""
if not isinstance(agency_chart, list):
raise Exception("Invalid agency chart.")

if len(agency_chart) == 0:
raise Exception("Agency chart cannot be empty.")

for node in agency_chart:
if isinstance(node, Agent):
if self.ceo:
raise Exception("Only 1 ceo is supported for now.")
self.ceo = node
self._add_agent(self.ceo)
if not self.ceo:
self.ceo = node
self._add_agent(self.ceo)
else:
self._add_agent(node)
self._add_main_recipient(node)

elif isinstance(node, list):
for i, agent in enumerate(node):
Expand All @@ -319,7 +430,6 @@ def _parse_agency_chart(self, agency_chart):
"agent": agent.name,
"recipient_agent": other_agent.name,
}

else:
raise Exception("Invalid agency chart.")

Expand All @@ -346,6 +456,20 @@ def _add_agent(self, agent):
else:
return self.get_agent_ids().index(agent.id)

def _add_main_recipient(self, agent):
"""
Adds an agent to the agency's list of main recipients.
Parameters:
agent (Agent): The agent to be added to the agency's list of main recipients.
This method adds an agent to the agency's list of main recipients. These are agents that can be directly contacted by the user.
"""
main_recipient_ids = [agent.id for agent in self.main_recipients]

if agent.id not in main_recipient_ids:
self.main_recipients.append(agent)

def _read_instructions(self, path):
"""
Reads shared instructions from a specified file and stores them in the agency.
Expand Down Expand Up @@ -416,23 +540,15 @@ class SendMessage(BaseTool):
message_files: List[str] = Field(default=None,
description="A list of file ids to be sent as attachments to this message. Only use this if you have the file id that starts with 'file-'.",
examples=["file-1234", "file-5678"])
caller_agent_name: str = Field(default=agent.name,
description="The agent calling this tool. Defaults to your name. Do not change it.")

@field_validator('recipient')
def check_recipient(cls, value):
if value.value not in recipient_names:
raise ValueError(f"Recipient {value} is not valid. Valid recipients are: {recipient_names}")
return value

@field_validator('caller_agent_name')
def check_caller_agent_name(cls, value):
if value != agent.name:
raise ValueError(f"Caller agent name must be {agent.name}.")
return value

def run(self):
thread = outer_self.agents_and_threads[self.caller_agent_name][self.recipient.value]
thread = outer_self.agents_and_threads[self.caller_agent.name][self.recipient.value]

if not outer_self.async_mode:
gen = thread.get_completion(message=self.message, message_files=self.message_files)
Expand Down Expand Up @@ -467,23 +583,15 @@ class GetResponse(BaseTool):
"""This tool allows you to check the status of a task or get a response from a specified recipient agent, if the task has been completed. You must always use 'SendMessage' tool with the designated agent first."""
recipient: recipients = Field(...,
description=f"Recipient agent that you want to check the status of. Valid recipients are: {recipient_names}")
caller_agent_name: str = Field(default=agent.name,
description="The agent calling this tool. Defaults to your name. Do not change it.")

@field_validator('recipient')
def check_recipient(cls, value):
if value.value not in recipient_names:
raise ValueError(f"Recipient {value} is not valid. Valid recipients are: {recipient_names}")
return value

@field_validator('caller_agent_name')
def check_caller_agent_name(cls, value):
if value != agent.name:
raise ValueError(f"Caller agent name must be {agent.name}.")
return value

def run(self):
thread = outer_self.agents_and_threads[self.caller_agent_name][self.recipient.value]
thread = outer_self.agents_and_threads[self.caller_agent.name][self.recipient.value]

return thread.check_status()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from agency_swarm import Agent
from agency_swarm.tools.coding import ReadFile
from agency_swarm.tools.genesis import CreateAgentTemplate, ImportAgent, GetAvailableAgents
from .tools.CreateAgentTemplate import CreateAgentTemplate
# from .tools.GetAvailableAgents import GetAvailableAgents
# from .tools.ImportAgent import ImportAgent
from .tools.ReadManifesto import ReadManifesto


class AgentCreator(Agent):
Expand All @@ -12,9 +14,10 @@ def __init__(self, **kwargs):

# Add required tools
kwargs['tools'].extend([CreateAgentTemplate,
GetAvailableAgents,
ReadFile,
ImportAgent])
# GetAvailableAgents,
ReadManifesto,
# ImportAgent
])

# Set instructions
if 'instructions' not in kwargs:
Expand Down
Loading

0 comments on commit 427cf21

Please sign in to comment.