Finding your Flock: AI, Agents, Simulations, and Social Engagement.
![Flockx - Multi-Agent AI Systems and Simulations](/static/01a194fcbe3ee9c4d53e06f7501feced/a7f05/DALL_E_2024-04-17_17.29.07_-_Create_an_image_that_conveys_a_high-tech_welcoming_virtual_simulation_space_in_a_bright_daytime_setting_with_fluffy_clouds_focusing_on_the_concept_o_ed7neq.webp)
Unlocking the power of Multi-Agent AI systems and simulations. A practical guide, code included.
This is a tutorial on how to implement a multi-agent system using LangChain, OpenAI, and the uAgents framework to simulate social interactions. We cover the components and concepts required, the practical implementation steps, and the code snippets to bring the simulation to life. This is the first in a series. We cover, in detail, how to create a multi-agent simulation where community members collaborate to find a 'third place' that matches their interests.
Our Problem Statement
In our hyper-connected world, where screens dominate our interactions, a profound irony presents itself: the more connected we are digitally, the more isolated we can feel. Have you ever sensed loneliness in a crowded room, or felt disconnected despite the constant buzz of online chatter? With over one-third of adults experiencing chronic loneliness, the digital tools designed to bring us closer are often the same ones that widen the gulf between us.
Imagine a place where every interaction enriches us, where our quirks are celebrated, and community ties are not just virtual but real and meaningful. Throughout history, humans have thrived in tribes and communities, finding strength and security in face-to-face connections. As we navigate the complexities of modern life, the challenges and opportunities before us are clear: to harness the vast capabilities of technology not just to connect us, but to reconnect us—to revive the age-old bonds that have always held societies together.
This is not just a hopeful vision. We seek new ways to combat the modern epidemic of loneliness through innovative, technology-driven solutions.
Pioneering Social Engagement via Technology
FlockX is utilizing multi-agent AI systems in simulations to study social interactions and enhance our platform functionality. In the long term, this approach will help us understand and improve community engagement, foster real-world social connections, and address loneliness through technology. We see the following strategic benefits:
Interaction Analysis: We can simulate social interactions to understand and improve community engagement.
Quality Assurance: AI agents can test the platform's usability and functionality, ensuring a seamless user experience through the eyes of a community member.
Platform Validation: Simulations can validate the platform's effectiveness in fostering real-world social connections.
Let’s get to brass tacks: Practical Implementation
The real value of this post is the practical application of technology as an aid in solving real-life problems (loneliness, social engagement, community activation). We’ll go step by step on how we’ve implemented the first iteration of our multi-agent simulation, show the output, and discuss our analysis.
Concepts and Components Required
We've made a few decisions on the concepts and components required to address in achieving our desired outcomes. Here's a brief overview:
Technology Components:
- LLMs (Large Language Models):
- Purpose: Simulate human-like communication and decision-making processes.
- Implementation: OpenAI’s ChatGPT (gpt-4) for natural language processing, enabling rich, realistic dialogues.
- Agent Frameworks:
- Purpose: Individualize interactions and automate communication within the simulation.
- Implementation:
- LangChain: Facilitates the integration of LLMs like ChatGPT, enhancing the capability of agents to perform language-based tasks.
- uAgents / Agentverse (Fetch.ai): Manages the automation, organization and structured communication between agents, enabling complex interactions and decision-making.
Conceptual Components:
Simulation: A methodological approach to replicate or mimic real-world processes in a controlled environment. In this context, it refers to the creation of a virtual setting where agents interact based on predefined roles and behaviors, representing different community members.
Personas: These are detailed character profiles used in the agent simulation to represent different community members. Each persona has unique characteristics, preferences, and behavior patterns that guide how they interact within the simulated community.
Directed Acyclic Graph (DAG): A data structure used to model dependencies and sequences of events that do not loop back on themselves. In this implementation, DAGs manage the flow of information and interactions between agents, ensuring that communication remains efficient and non-repetitive.
Participant Discovery: Refers to the process by which agents identify and connect with other relevant agents within the simulation. This concept is crucial for mimicking real-world social discovery and community engagement scenarios.
A Tiny Bit of Planning...
Thinking things through before we start writing code can be super helpful. We’ve got a few requirements that we need to meet in order to have a successful simulation. We’ve got a few constraints that we need to work within. We’ve got a few goals that we need to achieve.
Let's plan to create six community members. This is an arbitrary number but feels like just enough to support the requirements. We'll use some AI/LLM in this planning process as well. Let's open up ChatGPT and use the Explore GPTs functionality to find the Persona Creator. Personas are how we'll represent the needs, goals, and behaviors of different community members (agents). They'll help us define and understand the motivations and expectations of our community members. We'll use these personas to guide the interactions between agents in our simulation by using them in our LLM prompts as either system or context variables among other things.
The following is a generated persona which Persona Creator was generous enough to output for us in our desired format (JSON). JSON makes it easier to use later on when we start adding them to our code. The format provides some benefits for dynamically generating community members in bulk via configuration which is helpful for six members but required when we get to 100+ members.
JSON Persona for Sophia Nguyen: click to expand
{
"FullName": "Sophia Nguyen",
"persona_uuid": "475f61dd-5b63-468a-9caf-56737fe33fd7",
"Demographics": {
"AgeRange": "35-45",
"Gender": "Female",
"IncomeLevel": "Professional",
"Location": "Grand Rapids, MI",
"Education": "Bachelor's degree in Marketing"
},
"OccupationIndustry": {
"JobTitle": "Live Music Event Organizer",
"CompanySize": "Works with local venues and artists",
"IndustryType": "Entertainment / Live Music"
},
"Psychographics": {
"Values": [
"Cultural enrichment",
"community connection",
"supporting local artists"
],
"Interests": [
"Live music",
"event planning",
"community arts initiatives"
],
"Hobbies": [
"Attending concerts",
"playing the guitar",
"volunteering for arts organizations"
],
"Lifestyle": "Balances a professional career with a deep commitment to enhancing the local music scene"
},
"Goals": {
"Goals": "Grow the live music scene in Grand Rapids, making it a hub for emerging artists",
"Hopes": "To create a vibrant, supportive community for musicians and music lovers alike"
},
"PainPoints": [
"Finding individuals and businesses willing to invest time and resources in the local music scene",
"Navigating the logistical challenges of organizing live events"
],
"Fears": "That financial and logistical barriers may limit the growth and diversity of the local music scene",
"DecisionCriteria": [
"Potential for community engagement and artist exposure",
"Viability of event logistics and financial sustainability"
],
"ChangeReadiness": "Open to exploring new venues, partnerships, and funding models to support live music",
"Objections": "Concerns about the long-term viability of live music events in the face of changing consumer habits and digital media",
"RoleInPurchaseProcess": "Influencer and decision-maker for venue selections, artist lineups, and promotional strategies",
"PreferredContentChannels": [
"Social media",
"local arts newsletters",
"music blogs to promote events and engage with the community"
]
}
Additional Helpful Information: A Side Quest:We’ve chosen technology from Fetch.ai, in particular, uAgents and eventually we’ll have the option to deploy to Agentverse. Their framework provides us with some initial functionality that puts guardrails and structure around what we are building while also providing a path to a supported production deployment.
Agentverse provides a secure way to develop Agents with the benefit of registering them on a decentralized platform and facilitating interactions with one another: Agentverse: Autonomous Agents
uAgents is a library that allows for creating autonomous AI agents in Python. With simple and expressive decorators, you can have an agent that performs various tasks on a schedule or takes action on various events: uAgents on GitHub,uAgents: Examples,uAgents: Integrations
Development tools we’re using: PyCharm (IDE), Poetry, GitHub Find out more about Poetry here: Poetry: Python Dependency ManagementWe’re only including significant snippets of code in this post. We are not covering things like ‘how to python’ or project setup or creating accounts on OpenAI or using .env vars or a secrets manager etc…Reach out and let us know what you’d like to see next and if you'd like a public repo of the project.
Creating Community Members (Agents) and Planning Dialogue
A picture is worth a thousand words, so, let’s review a quick non-standards compliant diagram of the things and interactions.
![Flockx - Multi-Agent AI Systems and Simulations Diagram](/static/e7a01ac5631befa62f34744420da889c/5e070/CommunitySimulationDiagram_djfc0j.webp)
Directory Layout:
- community-simulator
- app
- main.py
- config
- personas.py
- app_config.py
- src
- agents
- services
- utilities
Entrypoint code:
```python
# main.py
from src.utilities.initialize import initialize_community
from configs.app_config import config
import dotenv
dotenv.load_dotenv()
# initialize the agent community and run their bureau
community_bureau = initialize_community(config)
if __name__ == "__main__":
community_bureau.run(
We are doing the following:
importing our configuration, environment variable management, and initializer
asking for initialization to start and getting our community_bureau object back
starting our community_bureau which orchestrates and runs all our community member agents
Key concepts:
uAgents operate as servers and receive messaging via network address and port
Bureau operates locally as a server
# initialize.py
def initialize_community(configuration):
"""
Initializes the agents and the community_bureau.
First, create our bureau from values in the
configuration. Bureau is initialized with a port and endpoint.
""
bureau = AgentFactory.create_community(
port=configuration["bureau"]["port"],
endpoint="http://localhost:" + configuration["bureau"]["port"] + "/submit"
)
# Load up the agents configuration
# We stored our configuration in a .py file
# so we could just import it and use it.
agents_config = configuration['agents']
# We need a way for each agent to find the others
# without hardcoding it. We'll save contact information
# of each agent into an dict that each agent will have a copy of
# we're not using it in this simulation but it will be necessary.
agent_directory = {}
agent_group = []
# Loop through the config to operate on each agent
for key, value in agents_config[0].items():
name = key
display_name = value['name']
port = value['port']
# seed value provides an agents unique identity
# when registering on the the agent network.
# We need the same seed value each time we start our
# agent in order to persist their address on the network.
# We communicate between agents using their address.
seed = value['seed']
# We created persona UUIDs (universal unique identifier)
# ahead of time. When we move to large scale agent
# creation, we will create and assign at first startup
# and persist them. We use the persona UUID to lookup and
# assign a persona to the appropriate agent.
persona_uuid = value['persona_uuid']
role = value['role']
endpoint = "http://localhost:" + port + "/submit"
# use the AgentFactory to create the agent
ag = AgentFactory.create_member(name, port, seed, endpoint)
# We store the whole config in the meta property
# of the agent. This is a temporary store for the
# agent to use during it's lifecycle.
ag.meta = value
# uAgent framework provides a default key/value store
# for persisting data across incarnations.
ag.storage.set('meta', value)
# ask the agent factory to find and attach the appropriate persona
AgentFactory.attach_persona(ag)
# Store appropriate information in the directory for this agent
# We'll use the address to directly communicate with an agent
# We'll use the role and persona to locate an appropriate agent
# to connect with based on the intention.
agent_directory[persona_uuid] = {
'address': ag.address,
'role': role,
'display_name': display_name,
'persona': ag.persona.json(),
}
# The bureau handles starting all agents under it's
# management. 'Add'ing the agent to the bureau allows
# lifecycle management and broadcasting to all agents in
# the bureau
bureau.add(ag)
agent_group.append(ag)
# end of agent creation loop
# All of our agents have been created.
# Here we do the things that required all agents to
# be already created. (kind of)
# Provide the agent directory to each
# and set up the agents behaviors.
for member in agent_group:
member.directory = agent_directory
member.storage.set('directory', agent_directory)
# Dialog management is our directed acyclic graph
# communication framework.
AgentFactory.attach_dialogue_management(member)
community_bureau = {'community': bureau, 'members': agent_group}
return community_bureau
Most of the hard work is happening in the AgentFactory. Let’s walk through that next.
class AgentFactory():
"""
The AgentFactory class provides static methods to create and attach properties
to an Agent object.
Methods:
- create_member(name: str, port: str, seed: str, endpoint: str) -> Agent: Creates a new Agent object with the given name, port, seed, and endpoint. Initializes the persona, meta, and directory properties if they exist in storage.
- create_community(port, endpoint) -> Bureau: Creates a new Bureau object with the given port and endpoint.
- attach_persona(agent): Attaches a persona object to the given agent by matching the persona UUID in the agent's meta property with the persona UUID in the list of personas. Sets the persona property and updates the persona in storage.
- attach_directory(agent, directory): Attaches a directory object to the given agent.
- attach_dialogue_management(agent): Attaches and defines response patterns to agent communication
"""
@staticmethod
def create_member(name: str, port: str, seed: str, endpoint: str) -> Agent:
ag = Agent(
name=name,
port=port,
seed=seed,
endpoint=endpoint
)
fund_agent_if_low(ag.wallet.address())
# load from storage and/or
# initialize persona
if ag.storage.has("persona"):
ag.persona = PersonaModel.parse_obj(ag.storage.get("persona"))
# initialize meta
# a place to store temporary and metadata
if ag.storage.has('meta'):
ag.meta = ag.storage.get('meta')
else:
ag.meta = {}
# initialize directory
if ag.storage.has('directory'):
ag.directory = ag.storage.get('directory')
else:
ag.directory = {}
return ag
@staticmethod
def create_community(port, endpoint) -> Bureau:
return Bureau(port=port, endpoint=endpoint)
@staticmethod
def attach_persona(agent) -> None:
for persona in personas:
if persona['persona_uuid'] == agent.meta['persona_uuid']:
agent.persona = PersonaModel.parse_obj(persona)
break
@staticmethod
def attach_directory(agent, directory) -> None:
agent.directory = directory
The previous code is pretty straightforward. We are simply creating and initializing the agents. We manage initial storage, bureau, directory and persona. Note: We don’t end up using the directory in this simulation but it will prove necessary for the next phases of our simulation (Part 2 of this series?) where our agents discover each other in a more natural way.
We’ll take a quick break here to explain the background and planning of the code we’ll see next.
Dialogue Management - Directed Acyclic Graph (DAG)
The primary focus of the project is to deliver multi-agent communication. Setting up the communication requires some understanding of the directed acyclic graph concept. uAgents does most of the hard work for us via their newest experimental feature release: Dialogues.
We do need to do some planning and mapping out of our communication pattern.
We won't dig deep into DAGs in this post. Here’s the wikipedia page for a more detailed explanation.![Wikipedia - Directed Acyclic Graph (DAG)](/static/f04aa2bad3499eb88b944aeb18a9c544/c117f/DAG_xsfchc.webp)
We have a goal for our agents in this initial simulation. The community participants will help each other find a third place (not home, not work, a social engagement space). For more on Third places check here.
The DAG framework helps us provide direction to the agent conversations focused on achieving an outcome as opposed to just chatting away forever. The uAgents framework labels this concept as stateful communication in their github examples.
Multi-Agent Communication with uAgents operates on Protocols. Protocols are a critical element of agent communication. Agents can only communicate with each other using the same protocol for a given communication. Agents can support an arbitrary number of protocols. Creating and/or complying with a protocol mostly involves creating a model to represent the message passed between agents and registering the protocol with the agent. Find out more here in theAgentverse: Agent Communication
Since we are using uAgent: Dialogue, a unique protocol per node is required. This is a key element in the agents knowing where in a given conversation they are currently at and what options are available for the next step in the conversation. This will be a lot clearer in a code example provided later in this post.
Planning and mapping the DAG for our goal of finding third places through community member conversations seemed a bit intimidating initially. So, we fired up a couple of conversations with ChatGPT and Gemini. We ended up with a solid design for our dialogue management by conversing with ChatGPT, asking clarifying questions, describing our requirements, and asking for example code. We did upload some of our code in order to tell ChatGPT about style and formatting that we preferred. ChatGPT was significantly more helpful in the process than Gemini/Bard at the time of this project.
A DAG is all about nodes and edges
Nodes represent states or points in the conversation. This could be anything from starting a conversation, exchanging specific information, to ending the dialogue.
Edges represent the transitions or communication paths between these states. These transitions are directed, meaning they go from one state to another, showcasing the flow of the conversation.
In essence, nodes define "what is happening" or "what is the state of interaction" at any point in our simulation. Edges, on the other hand, represent the actions taken that move the simulation from one state (node) to another.
To create a simulation of community members interacting with each other, leveraging the uAgents
framework and the
concept of communication as a directed graph, we can follow these steps. This approach involves defining the states and
transitions (nodes and edges in our graph) based on the interactions between the agents we’ve implemented.
We did go down the rabbit hole of attempting to create an entirely dynamic graph implementation. The idea was to dynamically create nodes and edges at run time through a DynamicGraph class. It was way too abstract to understand easily and support from Fetch advised us that we were likely to run into unpredictable ‘endless loop’ scenarios. So, we defined the specific graph for the conversation pattern that we are simulating.
Step 1: Define the Graph Structure
Since we’re starting with FlockXs current mission of helping community members locate their third place via other community members advice and guidance. We map out that conversation flow.
Proposed Nodes for Dialogue
Initiated: The dialogue starts, indicating a member's intent to find a third place related to their specific interests.
InterestIdentification: The member specifies or confirms their interest(s), guiding the search for a relevant third place.
ThirdPlaceOptions: Based on the identified interests, the system presents options for third places where like-minded individuals gather.
SelectionMade: The member selects a third place from the options provided, indicating a desire to explore it further or connect with others there.
Concluded: The dialogue concludes, ideally with the member having actionable information about a third place to engage with their community.
Proposed Edges for the Dialogue
Identify Interest: From "Initiated" to "InterestIdentification", facilitating the expression or confirmation of the member's interests.
Present Options: From "InterestIdentification" to "ThirdPlaceOptions", where the system presents third place options related to the identified interests.
Make Selection: From "ThirdPlaceOptions" to "SelectionMade", where the member selects their preferred third place.
Conclude Dialogue: From "SelectionMade" to "Concluded", marking the dialogue's successful conclusion with actionable next steps for the member.
We’ve identified two areas where ‘looping’ should/could happen to really replicate human communication patterns. InterestIdentification and ThirdPlaceOptions. These two nodes should be a back and forth conversation where asking probing questions that aid one community member to truly discover and dig into the interests of another community member in the process of aligning interests to an appropriate third place.
We’ve kept all the code for our Community Discovery Dialogue DAG in one file, however, there’s likely optimization that could be done here, in particular, once we add multiple communication flows (DAGs).
It’s super easy to get confusing with the naming of edges, nodes, and handlers. We didn’t really create a pattern here, we just focused on getting it to work. There’s definitely work to do here before we move on to creating lots of possible dialogue flows that represent other types of interactions between agents.
Step 2: Specify Nodes and Transitions
Define the states of our conversation (e.g., "Initiated", "Interest Initiated", "Concluded") and the possible transitions between these states. Each transition is represented by an edge, which dictates the movement from one state (node) to another based on certain conditions.
# community_discovery_dialogue.py
# Node definitions for the dialogue states
initiated_state = Node(
name="Initiated",
description="Dialogue has been initiated to discover interests and a third place."
)
interest_identification_state = Node(
name="InterestIdentification",
description="Identifying member's interests to find related third places."
)
third_place_options_state = Node(
name="ThirdPlaceOptions",
description="Presenting options for third places related to the identified interests."
)
selection_made_state = Node(
name="SelectionMade",
description="Member selects a preferred third place."
)
concluded_state = Node(
name="Concluded",
description="Dialogue concluded with actionable information about the third place."
)
# Edge definitions for the dialogue transitions
ident_interest = Edge(
name="identify_interest",
description="Initiates the process of identifying the member's interest.",
parent=None,
child=interest_identification_state,
)
continue_interest_identification = Edge(
name="continue_interest_identification",
description="Continues the conversation to further identify the member's interests.",
parent=interest_identification_state,
child=interest_identification_state, # Loops back to itself
)
present_options = Edge(
name="present_options",
description="Presents third place options based on the identified interests.",
parent=interest_identification_state,
child=third_place_options_state,
)
review_third_place_options = Edge(
name="review_third_place_options",
description="Reviews the third place options based on ongoing dialogue.",
parent=third_place_options_state,
child=third_place_options_state, # Loops back to itself for further review/discussion
)
make_selection = Edge(
name="make_selection",
description="Member makes a selection from the presented third place options.",
parent=third_place_options_state,
child=selection_made_state,
)
conclude_dialogue = Edge(
name="conclude_dialogue",
description="Concludes the discovery dialogue with next steps for the member to engage with the selected third place.",
parent=selection_made_state,
child=concluded_state,
)
Step 3: Implement the Dialogue Class
We'll extend the Dialogue
class to create a custom dialogue handler. This class will manage the states and
transitions based on our directed graph. Nodes represent different stages of the conversation, and edges define how
to move from one state to another. The nodes and edges are used in the initialization of the CommunityDiscoveryDialogue
as arguments to it’s parent class: Dialogue.
from community_discovery_dialogue.py
class CommunityDiscoveryDialogue(Dialogue):
"""
Represents a dialogue for community discovery.
:param version: The version of the dialogue.
:type version: str | None
:param agent_address: The address of the agent in the community.
:type agent_address: str | None
"""
def __init__(
self,
version: str | None = None,
agent_address: str | None = None,
) -> None:
super().__init__(
name="CommunityDiscoveryDialogue",
version=version,
agent_address=agent_address,
nodes=[
initiated_state,
interest_identification_state,
third_place_options_state,
selection_made_state,
concluded_state,
],
edges=[
ident_interest,
continue_interest_identification,
present_options,
review_third_place_options,
make_selection,
conclude_dialogue,
],
)
Step 4: Attach Models to Transitions:
For each transition, define message models that encapsulate the information exchanged between agents. These models are used to handle the data specific to each part of the conversation flow. These models are core to the Protocols required for agent to agent communication within the framework.
from community_discovery_dialogue.py
# message model definitions for dialogue states
class InterestIdentificationMessage(Model):
"""
Represents a message for interest identification.
Attributes:
interests str: A string interests the member wants to explore.
"""
interests: str # str of interests the member wants to explore
class ContinuedInterestIdentificationMessage(Model):
"""
Represents a continued message for interest identification.
Attributes:
interests str: A string interests the member wants to explore.
"""
interests: str # str of interests the member wants to explore
class ThirdPlaceOptionsMessage(Model):
"""
This class represents a message containing a list of options for third places matching the given interests.
Class: ThirdPlaceOptionsMessage(Model)
Attributes:
options (List[str]): A list of descriptions or names of third places matching the interests.
"""
options: str # Descriptions or names of third places matching the interests
class SelectionMadeMessage(Model):
"""
A message class representing the selection made by the user.
Attributes:
selection (str): The selected third place.
"""
selection: str # The selected third place
class ReviewThirdPlaceOptionsMessage(Model):
"""
Represents a message from a member providing feedback or asking questions about the options for a third place.
"""
feedback: str # Member's feedback or questions about the options
class DialogueConcludedMessage(Model):
"""
Initializes a new instance of the DialogueConcludedMessage class.
:param conclusion: The summary or next steps following the dialogue's conclusion.
:type conclusion: str
"""
conclusion: str # Summary or next steps following the dialogue's conclusion
So, it feels like code duplication that all the message models or almost all have basically the same form. However, it’s more of the fact that we are not using the potential complexity of the framework. We started with more uniqueness to the messages that would be passed. It just seemed to reduce the understandability of the code and make debugging more confusing. Most obviously, in our ThirdPlaceOptionsMessage, we started with List as the format and moved back to str while we were getting things running. When we integrate the simulation into the FlockX platform, that’s the spot where we will be reaching out to our api for real-world third places and the List will be more appropriate.]
Step 5: Implement Transition Handlers
For each state transition defined in our dialogue class, implement handlers that dictate the behavior when transitioning from one state to another. This involves specifying the logic that should be executed when moving between states, such as sending messages, processing received information, or concluding the dialogue.
from community_discovery_dialogue.py inside the CommunityDiscoveryDialogue class
def on_identify_interest(self, model: Type[Model]):
"""
Handler for when the member initiates the discovery process and identifies their interests.
"""
return super()._on_state_transition(ident_interest.name, model)
def on_continue_interest_identification(self, model: Type[Model]):
"""
Handler for continuing the interest identification discussion.
"""
return super()._on_state_transition(continue_interest_identification.name, model)
def on_present_options(self, model: Type[Model]):
"""
Handler for presenting third place options based on the member's identified interests.
"""
return super()._on_state_transition(present_options.name, model)
def on_review_third_place_options(self, model: Type[Model]):
"""
Handler for reviewing and discussing third place options.
"""
return super()._on_state_transition(review_third_place_options.name, model)
def on_make_selection(self, model: Type[Model]):
"""
Handler for when the member makes a selection from the presented third place options.
"""
return super()._on_state_transition(make_selection.name, model)
def on_conclude_dialogue(self, model: Type[Model]):
"""
Final handler for concluding the dialogue, providing the member with next steps to engage with the selected third place.
"""
return super()._on_state_transition(conclude_dialogue.name, model)
Well, there’s more comment here than there is code. uAgents is doing the heavy lifting in the parent class. We are really just indicating how edges represent our flow and allows us to connect those handlers to each agent via protocols. We’ll see how they are attached to an agent when we revisit the AgentFactory soon.
Step 6: Instantiate the Dialogue
Create instances of our dialogue class and register them with our agent instances. Initiate the dialogue from one agent and handle the incoming messages in the other agent according to the defined flow. Here we go back to the AgentFactory class and take a look at how protocols and handlers are attached to the individual agents.
The next section makes significant use of python decorators. To find out more about those:
https://realpython.com/primer-on-python-decorators/
from agent_factory.py inside the AgentFactory class
@staticmethod
def attach_dialogue_management(agent) -> None:
agent._logger.info(f"Attaching dialogue management to agent {agent.address}\n\n\n")
community_dialogue = CommunityDiscoveryDialogue(
version="0.1",
agent_address=agent.address
)
# create all the handlers for this dialogue/agent combo
# This can absolutely be done in a better way....
# We are going to want to add lots of dialogue management to the agent
# So, defining all the handlers here is problematic and this file will get huge
# in the future, also, so much code repition...
@community_dialogue.on_identify_interest(InterestIdentificationMessage)
async def identify_interest_handler(
ctx: Context,
sender: str,
_msg: InterestIdentificationMessage,
):
response, sentiment = agent.persona.discuss_interests(sender, _msg.interests)
if sentiment == 'next step':
await ctx.send(sender, ThirdPlaceOptionsMessage(options=response))
else:
await ctx.send(sender, ContinuedInterestIdentificationMessage(interests=response))
@community_dialogue.on_continue_interest_identification(ContinuedInterestIdentificationMessage)
async def continue_interest_identification_handler(
ctx: Context,
sender: str,
_msg: ContinuedInterestIdentificationMessage,
):
response, sentiment = agent.persona.discuss_interests(sender, _msg.interests)
if sentiment == 'next step':
await ctx.send(sender, ThirdPlaceOptionsMessage(options=response))
elif sentiment == 'continue':
await ctx.send(sender, ContinuedInterestIdentificationMessage(interests=response))
@community_dialogue.on_present_options(ThirdPlaceOptionsMessage)
async def present_options_handler(
ctx: Context,
sender: str,
_msg: ThirdPlaceOptionsMessage,
):
response, sentiment = agent.persona.discuss_third_place_options(sender, _msg.options)
if sentiment == 'continue':
await ctx.send(sender, ReviewThirdPlaceOptionsMessage(feedback=_msg.options))
else:
await ctx.send(sender, ReviewThirdPlaceOptionsMessage(feedback=_msg.options))
@community_dialogue.on_review_third_place_options(ReviewThirdPlaceOptionsMessage)
async def review_third_place_options_handler(
ctx: Context,
sender: str,
_msg: ReviewThirdPlaceOptionsMessage,
):
response, sentiment = agent.persona.review_third_place_options(sender, _msg.feedback)
await ctx.send(sender, SelectionMadeMessage(selection=response))
@community_dialogue.on_make_selection(SelectionMadeMessage)
async def make_selection_handler(
ctx: Context,
sender: str,
_msg: SelectionMadeMessage,
):
response, sentiment = agent.persona.make_selection(sender, _msg.selection)
await ctx.send(sender, DialogueConcludedMessage(conclusion=response))
@community_dialogue.on_conclude_dialogue(DialogueConcludedMessage)
async def conclude_dialogue_handler(
ctx: Context,
sender: str,
_msg: DialogueConcludedMessage,
):
ctx.logger.info(f"{agent.persona.FullName}:on_conclude_dialogue {_msg.conclusion}.\n\n\n")
agent.include(community_dialogue)
This structured approach allows us to create complex, stateful dialogues between agents, with the flexibility to define
custom behavior and data handling for each stage of the conversation. By following these steps and utilizing the
Dialogues
class within the uAgents framework, we can effectively manage communication flows that mimic real-life interactions.
In our initialization step, way back in the beginning, we looped through our configuration and created each agent and attached a Persona and the dialogue to each agent. Hence, the attach_dialogue_management method is conducted on each agent loaded from configuration.
Time to use those Personas for something.
This would not be a very interesting project if we didn’t have AI/LLMs doing something. We use the Persona class we’ve created to not only manage the unique identity of each agent (the persona definitions we discussed earlier) but also the generation of query/response. The Persona represents the community member, their role in the community, and their interests. We’ve created a Persona class and a set of classes to represent various aspects of the Persona. e.g. Demographics, OccupationalIndustry, Psychographics etc… We’ve kept all this in a single file for now persona.py
It’s in the Persona that we utilize LangChain and ChatGPT/OpenAI to integrate an LLM in the project. We make extensive use of prompts, prompt templates, and the concept of context. We make a very specific attempt to generalize our prompts so that we don’t have to write a unique prompt for each side of the conversation. We don’t want to have to write a different prompt for the community member that is offering help to find a third place for another member vs the community member that is seeking help. This may be dramatically different in the future but it works for now.
We’re loading up the properties of a Persona from configuration files stored as JSON. We use pydantic to give us boilerplate validation that we can expand in the future. Find out more about pydantic here:
https://docs.pydantic.dev/latest/
from persona.py classes that will be properties of our Persona
class Demographics(BaseModel):
AgeRange: str
Gender: str
IncomeLevel: str
Location: str
Education: str
class OccupationIndustry(BaseModel):
JobTitle: str
CompanySize: str
IndustryType: str
class Psychographics(BaseModel):
Values: List[str]
Interests: List[str]
Hobbies: List[str]
Lifestyle: str
class Goals(BaseModel):
Goals: str
Hopes: str
Next we define our Persona class:
Notes are in comments throughout the following code. Personality is defined and integrated through prompt templates to deliver the creative LLM output that represents our community members.
class Persona(BaseModel):
FullName: str
persona_uuid: str
Demographics: Demographics
OccupationIndustry: Optional[OccupationIndustry]
Psychographics: Psychographics
Goals: Goals
PainPoints: List[str]
Fears: Optional[str]
DecisionCriteria: Optional[List[str]]
ChangeReadiness: Optional[str]
Objections: Optional[str]
RoleInPurchaseProcess: Optional[str]
PreferredContentChannels: List[str]
def generate_inquiry(self) -> str:
"""
Create an initial query on locating a community and
where they gather. A third place. This starts
the CommunityDiscoveryDialogue conversation flow.
"""
language_model = self.get_llm()
# all the way at the bottom of this code you'll
# find the get_persona_story method. It takes all the elements
# of this persona and creates a personality context for the LLM.
# Basically: "you are so and so, you have this and that experience etc..."
persona_story = self.get_persona_story()
# set up the template. This is a LangChain thing.
# this template creates an inquiry to be sent to another agent
# and/or all the other agents/community members asking for
# help to get engaged in their community.
prompt = PromptTemplate.from_template("""
{context}
You are a helpful community member. You are participating
in a discussion about interests within the community.
You are reaching out to the community to find out where
the community that matches your interests gathers.
Generate an inquiry for the community that is based
off your personal goals and passions.
""")
# define the chain, also LangChain thing.
chain = prompt | language_model
#run the chain (invoke it)
response = chain.invoke({'context': persona_story})
#return the important part of the response for our case.
return response.content
def discuss_interests(self, sender, interests: str) -> str:
"""
Discuss interests is called as part of the handler process initiated
by the CommunityDiscoveryDialog upon receiving a message from another agent.
"""
language_model = self.get_llm()
persona_story = self.get_persona_story()
# this template aids in formulating an individualized response to
# an initial interest inquiry. However, it also serves both sides of
# the conversation.
pre_prompt = PromptTemplate.from_template("""
{context}
You are a helpful community member. You are participating in a discussion about interests within the community.
The ultimate outcome of your conversation is finding an interest that leads to the discovery of a third place.
A community member said this to you:
{user_prompt}
""")
chain = pre_prompt | language_model
response = chain.invoke({'context': persona_story, 'user_prompt': interests})
# this is new. We ask the LLM to evaluate the current state of the
# conversation. It's this evaluation that determines if the
# agent / community member thinks the conversation should continue
# in this phase or move on to another avaible state.
sentiment = self.evaluate_sentiment(response.content, interests)
return response.content, sentiment
def discuss_third_place_options(self, sender, options: str) -> str:
language_model = self.get_llm()
persona_story = self.get_persona_story()
pre_prompt = PromptTemplate.from_template("""
{context}
You are a helpful community member. You are participating in a discussion about generating a list of
third places in the community that are relevent to this conversation.
The ultimate outcome of your conversation is delivering a list of relevant third place locations to discuss in more detail.
A community member is saying this:
{user_prompt}
""")
chain = pre_prompt | language_model
response = chain.invoke({'context': persona_story, 'user_prompt': options})
sentiment = self.evaluate_sentiment(response.content, options)
return response.content, sentiment
def review_third_place_options(self, sender, options: str) -> str:
language_model = self.get_llm()
persona_story = self.get_persona_story()
pre_prompt = PromptTemplate.from_template("""
{context}
You are a helpful community member. You are participating in a discussion about third places in the community.
The ultimate outcome of your conversation is discussing the final third place
locations so you can make a decision.
A community member is saying this:
{user_prompt}
""")
chain = pre_prompt | language_model
response = chain.invoke({'context': persona_story, 'user_prompt': options})
sentiment = self.evaluate_sentiment(response.content, options)
return response.content, sentiment
def evaluate_sentiment(self, my_response, user_message):
"""
This method really controls the flow of each conversation in a
generalized way. In the template, we include the current message
that this agent is about to return in the conversation, the last message
from the other agent in the conversation, and ask the LLM to respond with
how it 'feels' about the state of the conversation. The LLM is instructed
to respond with only 'next step' or 'continue'
"""
prompt = PromptTemplate.from_template("""
You are in a discussion with another community member.
The last message from the other member is: {user_message}
Your next response is: {context}
Evaluate whether you feel like their message and your message indicate that the conversation should move to the
next step or be done.
if you have not yet talked about third places and the other user is engaging, choose continue.
respond only with 'next step' or 'continue'
""")
language_model = self.get_llm()
chain = prompt | language_model
response = chain.invoke({'context': my_response, 'user_message': user_message})
return response.content
def make_selection(self, sender, selection):
language_model = self.get_llm()
persona_story = self.get_persona_story()
pre_prompt = PromptTemplate.from_template("""
{context}
You are a helpful community member. You are participating in a discussion reviewing a list of third places
in the community.
The ultimate outcome of your conversation is a third place location.
A community member is saying this:
{user_prompt}
Provide a complete name and address of the selected third place with a brief summary of our trip.
""")
chain = pre_prompt | language_model
response = chain.invoke({'context': persona_story, 'user_prompt': selection})
sentiment = self.evaluate_sentiment(response.content, selection)
return response.content, sentiment
def get_llm(self):
return ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0.9)
def get_persona_story(self):
"""
Here we create the story that is used as the context of almost all conversations
We use the elements of the persona that give identity to a community member agent.
We format those persona elements into an initial prompt context describing how
we want the LLM to generate. We are telling the LLM who we want it to act as.
"""
pain_points = ', '.join(self.PainPoints)
preferred_channels = ', '.join(self.PreferredContentChannels)
decision_criteria = ', '.join(self.DecisionCriteria) if self.DecisionCriteria else "No specific decision criteria"
persona_story = f"""You are {self.FullName}, a {self.Demographics.AgeRange}
{self.Demographics.Gender} living in {self.Demographics.Location} with an income level of {self.Demographics.IncomeLevel}
and education level of {self.Demographics.Education}. Your values are primarily {', '.join(self.Psychographics.Values)},
with interests in {', '.join(self.Psychographics.Interests)} and hobbies like {', '.join(self.Psychographics.Hobbies)}. Your lifestyle
is best described as {self.Psychographics.Lifestyle}. Your goals include {self.Goals.Goals} and your hopes are {self.Goals.Hopes}.
You experience certain pain points such as {pain_points}. You harbor a certain fear: "{self.Fears}" and are {self.ChangeReadiness} to change.
You make decisions based on certain criteria: {decision_criteria}. You might raise objections due to "{self.Objections}".
You play a role as "{self.RoleInPurchaseProcess}" in the purchase process and prefer the following content channels: {preferred_channels}.
"""
return persona_story
Let’s get to talking.
We will edit the original initializer to include a conversation start between two of our agents. A community leader and a community member that is seeking advice. If we start all agents talking at the same time, it works but it’s really hard to follow the conversations in a meaningful. way.
We’ll use Mia Torres and Harpeet Singh as our collaborating community members. We’ll add the following code to our initialize.py
# !!! we are inside the initialization loop....
# we only want to start a conversation with our identified members
# We're just going to keep it simple and start the conversation between
# Mia Torres and Harpreet Singh.
if member.persona.persona_uuid == "0b0fe337-f023-427f-a159-fc498e19a33a":
@member.on_event("startup")
async def start_communication(ctx: Context):
inquiry = member.persona.generate_inquiry()
member_address = "agent1q0p629hu3558ec25hhu2kk8w58cat97sejlfjtudc26ldw9ergsfzkdfcru"
await ctx.send(member_address, InterestIdentificationMessage(interests=inquiry))
uAgent captures conversations that occur through the Dialogue feature in a json file very similar to the agent storage mechanism. The JSON format is a bit difficult to read as the query and response of the messages are converted/escaped. We’ve unescaped and formatted the conversation for display:
Conversation: CommunityDiscoveryDialogue
Message from Harpreet Singh to Mia Torres
Sender: Harpreet Singh (agent1qwuqa6j6zgqlja9rw26qw7gv289gq0s5uqpnwhau2xd3gj6nfqtn2pnxxxm) Receiver: Mia Torres (agent1q0p629hu3558ec25hhu2kk8w58cat97sejlfjtudc26ldw9ergsfzkdfcru) Timestamp: 2024-04-17 12:15:23 Message Type: InterestIdentificationMessage Content Identifier: interests
Subject: Seeking Community Insights for Local Entrepreneurship and Youth Sports Initiatives
Dear Community Members,
I'm Harpreet Singh, a local businessman, youth sports enthusiast, and a dedicated champion of community development. Fortified with a Master's degree in Business Administration and years of experience in empowering local businesses, I am highly passionate about investing in and strengthening our community—especially in the fields of entrepreneurship and youth sports.
As part of my ongoing commitment to the progression of our community, I am currently exploring opportunities to contribute and participate in local sports, music, and community events that align closely with our collective values and the store's brand strategy. By doing so, my goal is to enhance the store's visibility and, more importantly, foster community goodwill through meaningful sponsorships.
However, identifying potential initiatives and accurately measuring the return on investment in both economic and social terms can be challenging. In these regards, I look forward to gathering your valuable insights and suggestions.
Do you know of any local gatherings, events, or platforms—especially those concentrated on youth sports, entrepreneurship, or business networking—aligned with this vision? Your recommendations could deeply influence my sponsorship decisions and accelerate the impact we can make on our beloved community together.
Feel free to share this message across your networks on LinkedIn, through local business networks, or in community newsletters so we can reach as many community members as possible.
Thank you for your time and anticipation.
Yours Sincerely,
Harpreet Singh
Message from Mia Torres to Harpreet Singh
Sender: Mia Torres (agent1q0p629hu3558ec25hhu2kk8w58cat97sejlfjtudc26ldw9ergsfzkdfcru) Receiver: Harpreet Singh (agent1qwuqa6j6zgqlja9rw26qw7gv289gq0s5uqpnwhau2xd3gj6nfqtn2pnxxxm) Timestamp: 2024-04-17 12:15:41 Message Type: ThirdPlaceOptionsMessage Content Identifier: options
Subject: Re: Seeking Community Insights for Local Entrepreneurship and Youth Sports Initiatives
Dear Harpreet,
First of all, allow me to appreciate your drive and commitment to our community. The endeavors you have mentioned resonate deeply with my values as well, particularly towards youth sports and local entrepreneurship development.
In terms of local gatherings and events that concentrate on entrepreneurship, I would recommend looking into the Grand Rapids Start-Up Weekend and West Michigan Business Innovation Forum. Both events foster a spirit of innovation and business collaboration that can have a significant impact on community development.
As for youth sports, our city's youth soccer leagues host several tournaments throughout the year and could greatly benefit from corporate sponsorship. Grand Rapids Downtown Market also hosts seasonal events that emphasize local products, which makes it an excellent platform for business networking while showcasing your store's commitment to the community.
As for measuring ROI, this is an area where modern technology can aid. You could consider using social listening tools or community sentiment analysis programs. They can help evaluate community impact and sentiment following your sponsorship and measure the effects on your store's brand visibility.
Please let me know if there's any further insight I could provide. Once again, thank you for your community involvement. I look forward to seeing your continued positive impact on Grand Rapids.
Best Regards,
Mia Torres
Message from Harpreet Singh to Mia Torres
Sender: Harpreet Singh (agent1qwuqa6j6zgqlja9rw26qw7gv289gq0s5uqpnwhau2xd3gj6nfqtn2pnxxxm) Receiver: Mia Torres (agent1q0p629hu3558ec25hhu2kk8w58cat97sejlfjtudc26ldw9ergsfzkdfcru) Timestamp: 2024-04-17 12:15:58 Message Type: ReviewThirdPlaceOptionsMessage Content Identifier: feedback
Subject: Re: Seeking Community Insights for Local Entrepreneurship and Youth Sports Initiatives
Dear Mia,
Thank you for your detailed suggestions and for acknowledging our efforts. Your recommendations about the Grand Rapids Start-Up Weekend and the West Michigan Business Innovation Forum are particularly compelling, and I am considering these for our next community involvement initiatives.
Additionally, your mention of using modern technology to track ROI is insightful, and we are exploring tools that can help measure the impact effectively.
Your continued support is invaluable as we strive to foster community engagement and development.
Best regards,
Harpreet Singh
Message from Mia Torres to Harpreet Singh
Sender: Mia Torres (agent1q0p629hu3558ec25hhu2kk8w58cat97sejlfjtudc26ldw9ergsfzkdfcru) Receiver: Harpreet Singh (agent1qwuqa6j6zgqlja9rw26qw7gv289gq0s5uqpnwhau2xd3gj6nfqtn2pnxxxm) Timestamp: 2024-04-17 12:16:15 Message Type: SelectionMadeMessage Content Identifier: selection
Subject: Re: Community Insights Appreciated for our Entrepreneurship and Youth Sports Initiatives
Dear Harpreet,
I am thrilled to hear that you are considering my suggestions for your upcoming community engagement activities. The potential impact of these initiatives on our local community could be profound, and I am eager to see how your involvement will drive positive change.
Should you need further assistance or wish to discuss these plans in more detail, please feel free to reach out. I am here to support you every step of the way.
Looking forward to our continued collaboration and the success of these community-focused events.
Warm regards,
Mia Torres
Message from Harpreet Singh to Mia Torres
Sender: Harpreet Singh (agent1qwuqa6j6zgqlja9rw26qw7gv289gq0s5uqpnwhau2xd3gj6nfqtn2pnxxxm) Receiver: Mia Torres (agent1q0p629hu3558ec25hhu2kk8w58cat97sejlfjtudc26ldw9ergsfzkdfcru) Timestamp: 2024-04-17 12:16:31 Message Type: DialogueConcludedMessage Content Identifier: conclusion
Subject: Re: Community Insights Appreciated for our Entrepreneurship and Youth Sports Initiatives
Dear Mia,
Thank you once again for the constructive dialogue and for sharing your valuable insights and experiences. Your involvement and feedback have been crucial in shaping our approach towards these community initiatives.
As we move forward, I am confident that our efforts will contribute significantly to the community, and I am grateful for your partnership in this journey.
Please stay in touch as we progress, and I look forward to updating you on our developments.
Best regards,
Harpreet Singh
Initial Analysis of the Conversation
DAG Implementation for Structured Dialogue: The conversation begins at an initiated state and follows our pre-defined path through nodes representing different stages of interaction—from identifying interests to selecting a third place. This structured approach ensures that the conversation remains focused and productive, leading to actionable outcomes. Our prompts are extremely general excluding the inclusion of the persona to provide identity.
Identification of Interests: Harpreet expresses his interests in local entrepreneurship and youth sports. This stage is crucial as it sets the direction for the types of third places that might be most relevant and beneficial, aligning with his passions and the community's needs.
Exploration of Third Place Options: Mia responds with suggestions tailored to Harpreet's interests, such as local business forums and sports events. These options not only align with his personal interests but also offer potential venues for community interaction and networking. This reflects the power of targeted, personalized communication.
Decision-Making and Selection: Harpreet evaluates the options presented, demonstrating the decision-making process facilitated by the DAG. This step embodies the transition from digital interaction to real-world action.
Concluding the Dialogue: The dialogue concludes with Harpreet acknowledging the productive exchange and outlining his plans to engage with the selected third places. This final node ensures that the conversation has a clear, actionable outcome, reinforcing the objective.
Miscellaneous Observations: One of the cool things about using AI/LLMs is the element of surprise in outcomes. Variability is a significant reason we chose the DAG structure for a conversation. The DAG guides the overall process but is not overly specific on the individual communications. Here are some of the outcomes that appeared though not expected or asked for:
The conversation is structured almost as an email chain or letters being exchanged. In our prompts, we really only mentioned the phrase “conversation between community members”. We could provide additional guidance in our prompts that define the type of conversation more explicitly. One theory is that we started with a communication from a Persona identified as a professional and business owner. Each progressive message attempted to follow the format in each subsequent response.
Messages in the conversation have a very formal tone. We could include in our prompts to mimic the personality and writing style that might best represent the Persona currently responding.
The participants are extremely complimentary.
The conversation between Harpreet and Mia illustrates the potential of using AI and digital frameworks to bridge the gap between digital connections and real-world interactions through simulation. By leveraging the structured dialogue facilitated by a directed acyclic graph, we can more realistically simulate the way in which humans interact, each attempting to achieve their goal.
In conclusion, this blog post outlines how we at FlockX have applied existing technological tools to address the challenge of loneliness through enhanced community engagement. Using multi-agent systems, artificial intelligence, and language models—primarily developed with Python—we've simulated interactions that aim to strengthen community ties.
The dialogue between Harpreet and Mia, structured through a Directed Acyclic Graph (DAG), serves as a practical example of how these technologies can be orchestrated to facilitate meaningful connections. This structured approach ensures that conversations are directed towards actionable outcomes, simulating the process of discovering 'third places' that foster real social interactions.
Our project illustrates a way to utilize widely available tools to create impactful social simulations. This post is intended as a tutorial to demonstrate the techniques we have employed, offering a roadmap for others to replicate or adapt these methods in different contexts. We are not claiming a proprietary solution but are sharing our approach to using these tools to the best of our ability.
As we continue to explore these technologies and refine our methods, we encourage other developers and organizations to engage with and adapt these tools. By documenting and sharing our process, we hope to contribute to a broader conversation on how technology can be effectively employed to reconnect our social fabric.