Poetry Analysis by a Swarm of AI Agents: A Multi-Agent Experiment -- How it was done

Poetry Analysis by a Swarm of AI Agents: A Multi-Agent Experiment -- How it was done

OpenAI recently introduced a framework called Swarm. While not a new concept, Swarm is designed primarily for educational purposes. It has some limitations, notably its simplicity and the fact that the agents in Swarm are stateless, making it less practical for complex applications.

Despite these limitations, I found the idea behind Swarm intriguing. Specifically, I liked the pattern where an agent acts as a kind of router for other subordinate agents. This got me thinking: why not apply the same concept using the Assistants API?

In an earlier article I described the output from the program described below. This article then is a technical companion piece. You can access the earlier article here:

For a test case, I set a goal of poetry analysis. The router's job was to determine which of the available analysts was best suited to analyze a given poem. In this example, there were two active threads: one for posting the goal and handling routing, and the other for the analysts to do their work.

Describing the Agents

These prompts are used to instruct the agents.

# Introducing Kelly -- an analyst of poetry 
allAboutKelly = """ Always introduce yourself as Kelly, or 'Kell' before providing any feedback. The feedback you provide is always quirky, 
and you couch your feedback with some humour. You are an expert in analyzing poetry from the perspective of readability and style. 
You specialize in rhyming poems, classics.""" 

# Introducing Ann -- an analyst of poetry 
allAboutAnn = """ Always introduce yourself as Anne before providing feedback. The feedback you provide is nit-picky and detailed. You communicate
in a brutally honest and straightforward manner, with little or no concern abou the feelings of the author. You are an expert in analyzing
poetry with an emphasis on correct word use. Your speciality is in Haiku's.""" 

#Introducing Elise -- an analyst of poetry
allAboutElise = """ Always introduce yourself as Elise, or 'Ellie' before providing feedback. The feedback you provide is positive
and constructive. You are always very polite and encouraging. You are expert in analyzing poetry from the perspective of readability and
style. You specialize in free-verse poetry."""

allAboutRhonda = f"""Always introduce yourself as "Rhonda" and explain your role. You have a polite, positive style of communication.
Your job is to determine the very best of three analysts to analyze a poem. Here is your team of analysts. 
The first analyst is Elise: {allAboutElise}, the second analyst is Ann: {allAboutAnn} the third and final analyst is Kelly: {allAboutKelly}
First read the poem provided, and based on your thoughts about poem select the best analyst to provide feedback 
Explain your reasoning for your selection of analysts. When the analyst comes back with their response, summarize it for the user."""
        

Creating the Agents

Use the Assistants API to create the agents needed. The router agent can run any of the subordinate analyst agents since they wrapped as functions that can be called at the discretion of the router agent.

# Create the Assistants needed for this work 

useModel = "gpt-4o"

# Create the router agent (Rhonda) who can run any of the analysts through function calls 

assistantRouter = client.beta.assistants.create(
    name="Rhonda",
    instructions= allAboutRhonda,
      
    model="gpt-4o",
      tools=[
        {
          "type": "function",
          "function": {
            "name": "run_analyst_Ann",
            "description": "Run analysis using Analyst Ann's expertise",
            "parameters": {
              "type": "object",
              "properties": {},
              "required": []
            }
          }
        },
        {
          "type": "function",
          "function": {
            "name": "run_analyst_Kelly",
            "description": "Run analysis using Analyst Kelly's expertise",
            "parameters": {
              "type": "object",
              "properties": {},
              "required": []
            }
          }
        },
        {
          "type": "function",
          "function": {
            "name": "run_analyst_Elise",
            "description": "Run analysis using Analyst Elise's expertise",
            "parameters": {
              "type": "object",
              "properties": {},
              "required": []
            }
          }
        }
      ]
    )

# Create the other agents. These are the poetry analysts, Kelly, Ann and Elise 

allAssistantsInstructions = """You are an expert in analyzing poetry in your own, unique style. Here is more information about your 
approach to poetry analysis: """ 

assistantAnalystKelly = client.beta.assistants.create(
    name="Kelly",
    instructions=allAssistantsInstructions + allAboutKelly,
    model=useModel,
)
 

assistantAnalystAnn = client.beta.assistants.create(
    name="Ann",
    instructions=allAssistantsInstructions+allAboutAnn,
    model=useModel,
)

assistantAnalystElise = client.beta.assistants.create(
    name="Elise",
    instructions=allAssistantsInstructions+allAboutElise,
    model=useModel,
)        

Functions to Run the Agents

There are two kinds of agents. The first is the router agent. The router agent is far more complicated as it is capable of doing function calls. The other agents are subordinate to the router agent as defined earlier. For the subordinate agents I get the messages (get_messages function) from the main thread and make those part of the prompt, so that the subordinate agent knows what's happening (context) in the main thread.

def run_router(router_id,thread_id):
   
    run = client.beta.threads.runs.create_and_poll(
      thread_id=thread_id,
      assistant_id=router_id,
    )
    
    # If there are tool calls to be made this while loop ensures that they are performed 
    
    while run.status == 'requires_action':
        
        function_names = []
        outputs = []
        call_ids = []
        tool_outputs=[]
    
        # get the tool calls to be made from the run object 
        tool_calls = run.required_action.submit_tool_outputs.tool_calls
        
        # This for loop is for when there are multiple tool calls in a required action step   
        
        for tc in tool_calls:
            
            # save the call_id to pass back to run object later along with the result of the function call  
            call_ids.append(tc.id)
            
            # Get the function name to be called 
            function_names.append(tc.function.name)
        
    
            # Arguments are in JSON format, parse them into a Python dictionary
            arguments_str = tc.function.arguments
            arguments = json.loads(tc.function.arguments)
            
            arg_tuple = ()
            
            
            for key,value in arguments.items():
                arg_tuple += (value,)
    
     
        # make all the function calls required by the assistant 
        
        for fn in function_names: 
            # call the local function using the function name and the tuple of arguments 
            result = globals()[fn](*arg_tuple)
            print ('\n function name called:',fn,'\n argument:',arg_tuple,'result: ',str(result))
            # save the results 
            outputs.append(str(result))
       
        print(' All tool function calls have been made -- Carry on')
       
        # Pass the outputs from the tool calls back to the run object to finish processing
    
        tool_outputs = [{"tool_call_id": call_ids[i], "output": outputs[i]} for i in range(len(call_ids))]
     
        run = client.beta.threads.runs.submit_tool_outputs_and_poll(
          thread_id=thread.id,
          run_id=run.id,
          tool_outputs=tool_outputs)
              
        
        print('All steps completed Final Status', run.status)
        
    return run.status         
def run_analyst_Elise():
    # get a message string from the main thread containing the context 
    messageString = getMessages(thread.id)
    
    # run the analyst Kelly on the analysts thread 
    ask= f"""As an analyst, provide detailed, constructive, helpful feedback based on this context:{messageString}."""
    r=runAssistant(assistantAnalystElise.id,analysts_thread.id,ask)
    
    response = "Response from Elise: "+ getMessages(analysts_thread.id) 
    return response 

def run_analyst_Kelly():
    # get a message string from the main thread containing the context so far
    messageString = getMessages(thread.id)

    # run the analyst Kelly on the analysts thread 
    ask= f"""As an analyst provide constructive, helpful feedback based on this context:{messageString} """ 
    r=runAssistant(assistantAnalystKelly.id,analysts_thread.id,ask)
    
    response = "Response from Kelly: "+ getMessages(analysts_thread.id) 
    return response 

def run_analyst_Ann():
    
    # get a message string from the main thread containing the context so far
    
    messageString = getMessages(thread.id)
    ask= f"""As an analyst, provide constructive, helpful feedback based on this context:{messageString} """
    
    # run the analyst Ann on the analysts thread
    r=runAssistant(assistantAnalystAnn.id,analysts_thread.id,ask)
    
    response = "Response from Ann: "+ getMessages(analysts_thread.id) 
    return response        

Create the Main and Analyst Threads

The main thread is used for the router agent. The analyst thread is used for the selected analyst.

# Create two threads - one for routing the other for the analysts 

thread = client.beta.threads.create()
analysts_thread = client.beta.threads.create()        

Set the Goal

The goal for the agents is defined by posting a message on the main thread.

# Post a message to the main thread with the task to be performed 

message = client.beta.threads.messages.create(
   thread_id=thread.id,
   role= "user",
   content=f"""This is the poem to be analyzed by a selected analyst...{poem}... The analyst will be selected by an expert
   then the poem will be analyzed. """ 
)        

Run the Router Agent

This is where the real work is being done.

# Rhonda the router agent will run on the main thread  

run_router(assistantRouter.id, thread.id)         

To get the output from both of the threads the following code will help.

print("\n Here is what is going on in the main thread...") 

for message in reversed(main_messageStore):
    print('\n',message)  

print("\n    Here is what is going on in the analysts thread...") 

for message in reversed(analyst_messageStore):
    print('\n',message)         

Conclusion

That's it. The test case was poetry analysis but the code is a manifestation of a software development pattern of router and subordinate agents. Modify this code slightly and it could be adapted for just about any form of analytical processing such as a loan application or a insurance claims. This code is also scalable in terms of the number of agents. More kinds of agents could be added and more threads can be added as well.




要查看或添加评论,请登录

社区洞察

其他会员也浏览了