In the grand scheme of evolution, gradual changes over time are often punctuated by sudden leaps forward. These monumental moments shape life as we know it, despite sometimes feeling like a drop in the ocean. The development of our uAgents framework isn't quite as dramatic, but we like to think it follows a similar pattern. While the steady stream of updates and enhancements might seem like a slow process, every now and then, a release comes along that feels like a significant step forward. In this post, we're looking at one of those steps - our recent v0.11.0 release - and hope to share why fundamental developments hopefully will root in this release, so subtle that in a year from now it very likely would not stand out at all.
For uAgents communication is crucial. Both in terms of mere functionality, but also in the way it is made possible. To understand the impact, let's have a look at how communication happened to this point. Disclaimer: This expects a good familiarity with application of uAgents and a basic understanding of the general framework and we will not shy away from being lazy and just refer you to the relevant code lines or documentation, if not elementary to the concrete example.
async def send_message(ctx: Context): msg = f"Hello there {bob.name} my name is {alice.name}." await ctx.send(bob.address, Message(text=msg))
In order to send a message, the current context (managing how you handle messages, or taking care of actual message delivery), the recipients address and the actual message is needed. The uAgent framework leverages HTTP and thus ultimately IP addresses to route messages. We leave the question why
bob.address
Agent Name Service
Enter ctx.broadcast which looks intriguingly similar, yet offers so much more power:
async def say_hello(ctx: Context): await ctx.broadcast(proto.digest, message=BroadcastExampleRequest())
Instead of an explicit and solitary address, we are sending this message to a protocol. In simple terms a protocol is uniquely identified by the messages it defines and the relation of these messages (E.g.,
RequestMsg
ResponseMsg
Broadcast
If you have worked with agents before, you should have come across something like this:
@user.on_message(QueryTableResponse, replies={BookTableRequest}) async def handle_query_response(ctx: Context, sender: str, msg: QueryTableResponse):
With the
on_message
QueryTableResponse
BookTableRequest
QueryTableResponse
With
Dialogues
In case it's not immediately obvious what is behind these points let's go through them and start with the last. Imagine a uAgent marketplace where uAgents offer and consume services or goods for their owners. From a technical perspective there is no need to distinguish between an agent that offers a haircut service or an agent that offers a pizza. In both cases the flow could abstractly be as follows:
However, if I fancy a pizza I don't want to sort through all the barber agents and maybe details in negotiation such as 'extra pineapples' doesn't make sense for a barber agent. But with dialogues we can define the above flow as a pattern, ensuring that every agent participating in the marketplace behaves the same and instantiates this pattern with different service dialogues, such as the pizza service or the barber service. Besides the obvious benefits in efficiency and simplicity it also makes bootstrapping of sub-ecosystems in the agentverse much easier as any developer wishing to participate in this ecosystem doesn't need to fiddle around with their own protocol, but can simply use and instantiate the provided pattern / dialogue. "Standardisation" is not exactly always connoted positively, yet almost without alternative in multi-stakeholder environments.
Putting it all together, this is what the definition of a pattern generally looks like. We start with defining the directed graph by nodes and edges representing the intended communication flow:
# Node definition for the dialogue states chatting_state = Node( name="Chit Chatting", description="While in this state, more messages can be exchanged.", ) end_state = Node( name="Concluded", description="This is the state after the dialogue has been concluded and " "no more messages will be accepted.", ) # Edge definition for the dialogue transitions start_dialogue = Edge( name="Start Dialogue", description=( "A message that initiates a ChitChat conversation and provides " "any information needed to set the context and let the receiver " "decide whether to accept or directly end this conversation." ), parent=None, child=chatting_state, ) cont_dialogue = Edge( name="Continue Dialogue", description=( "A general message structure to exchange information without " "annotating further states or limiting the message flow in any way." ), parent=chatting_state, child=chatting_state, ) end_dialogue = Edge( name="End Dialogue", description=( "A final message that can be sent at any time by either party " "to finish this dialogue." ), parent=chatting_state, child=end_state, )
Next we create the abstract pattern and define message handlers corresponding to the edges of the graph.
class SimpleChitChatDialogue(Dialogue): ... def on_start_dialogue(self, model: Type[Model]): """ This handler is triggered when the initial message of the dialogue is received. It automatically transitions into the chit-chatting state for the next message exchange or directly into the end state. """ return super()._on_state_transition(start_dialogue.name, model) def on_continue_dialogue(self, model: Type[Model]): ... def on_end_session(self, model: Type[Model]): ...
This makes it possible to use these special decorators similar to the general
on_message
on_initiate_session
# define dialogue messages; each transition needs a separate message class InitiateChitChatDialogue(Model): pass class ChitChatDialogueMessage(Model): text: str class ConcludeChitChatDialogue(Model): pass # instantiate the dialogues chitchat_dialogue = SimpleChitChatDialogue( version="0.1", agent_address=agent.address, ) # specify with which particular message this agents fills the pattern @chitchat_dialogue.on_initiate_session(InitiateChitChatDialogue) async def start_chitchat( ctx: Context, sender: str, _msg: InitiateChitChatDialogue, ): # do something when the dialogue is initiated await ctx.send(sender, ChitChatDialogueMessage(text="Wasssuuup!")) ...
By doing so, this agent will automatically track the state for each session, i.e. reject
ConcludeChitChatDialogue
InitiateChitChatDialogue
ctx.dialogue
@chitchat_dialogue.on_end_session(ConcludeChitChatDialogue) async def conclude_chitchat( ctx: Context, sender: str, _msg: ConcludeChitChatDialogue, ): # do something when the dialogue is concluded after messages have been exchanged ctx.logger.info(ctx.dialogue)
Since we are an AI startup, we will leave the last words to AI and see how it summarizes in a few seconds and in a more entertaining and eloquent way what's taken me more time than I care to admit.
Welcome to the world of uAgents, where we believe in the power of slow and steady evolution with occasional leaps into the extraordinary. Our latest release, the v0.11.0, is one such leap. It's like the evolutionary equivalent of developing opposable thumbs, but in the realm of agent communication.
In the uAgent world, communication is kind of a big deal. It's like being at a party where you need to not only know the dance moves, but also the secret handshake. It's all about sending messages and knowing where to send them. But let's face it, knowing everyone's address at a party is a bit of a stretch. That's where the magic of
ctx.broadcast
But what if you want to have a meaningful conversation at this party? You need something more than just shouting into the crowd. Enter
Dialogues
In essence, working with uAgents is like being at the coolest tech party around. The music is bumping, the conversation is flowing, and the dance floor is always open for a new routine.