article_icon
article

uAgents Communication Overview

Take a look at uAgents' communication framework.

2024-03-085 min readDavid N, Fetch.ai

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.

code-icon
code-icon
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 

code-icon
code-icon
bob.address
 is a cryptic hash and not an IP address and what the 
code-icon
code-icon
Agent Name Service
 got to do with it for another time and simply note that we need to know an explicit address already at development time for communication to happen. I guess you would agree that this is not scalable at all, even more considering the dynamic fluctuation P2P networks tend to expose.

Is there anybody out there? (Feature: Broadcast)

Enter ctx.broadcast which looks intriguingly similar, yet offers so much more power:

code-icon
code-icon
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., 

code-icon
code-icon
RequestMsg
 is expected to be followed by 
code-icon
code-icon
ResponseMsg
). Agents need to specify which protocols they are speaking, otherwise they won't be able to communicate. Now since every agent needs to register their supported protocols with the Almanac, it's easy to retrieve all agents (and ultimately their address/endpoints) of which you can be sure that they will understand your message and respond accordingly. 
code-icon
code-icon
Broadcast
 does exactly that under the hood. Instead of you going through the explorer, it queries all currently registered agents (supporting the desired protocol), resolve their address and send the given message to each of them. So you as a developer don't need to know or care about your counterparts and you will automatically benefit from every new agent registering with your protocol without a single code change at your side! Now this is what scalability should look like.

Excuse me, is this yours? (Feature: Dialogues)

If you have worked with agents before, you should have come across something like this:

code-icon
code-icon
@user.on_message(QueryTableResponse, replies={BookTableRequest})
async def handle_query_response(ctx: Context, sender: str, msg: QueryTableResponse):

With the 

code-icon
code-icon
on_message
 decorator a developer adds the message pair 
code-icon
code-icon
QueryTableResponse
 →
code-icon
code-icon
BookTableRequest
 to the agents protocol and registers the decorated function as a callback whenever a 
code-icon
code-icon
QueryTableResponse
 message is received. As mentioned above, this ensures that you will only receive and return expected messages, but beyond that a protocol is more of a functional wrapper, following your specifications and translating them into a structure that the system can use to ensure interoperability and take away some message validation bits.

With 

code-icon
code-icon
Dialogues
 we wanted to provide a more robust structure that also has the potential of easing agent development, especially when it comes to scaling an ecosystem. Think of Dialogues as super-powered Protocols with the following additional features:

  • Stateful multi-message sessions: access previous messages within the same dialogue session whenever a new message is received
  • Resume dialogues at any time (even after agent restart)
  • Enforce the intended sequence of messages, by tracking the current state of the dialogue in a directed graph representation
  • Possibility to define general communication patterns that define abstract rules and logical flow of messages which can be instantiated with different message models

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:

  1. A sends a request with the preferred conditions
  2. B replies with available resources and conditions
  3. A and B negotiate to find the best match (or abort if there is no match)
  4. A and B settle a payment commitment

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:

code-icon
code-icon
# 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.

code-icon
code-icon
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 

code-icon
code-icon
on_message
 decorators with all the known implications. The actual instantiation of a dialogue with concrete message models could look like this. Note how the specification of the message model happens implicitly when given as argument to the 
code-icon
code-icon
on_initiate_session
 decorator.

code-icon
code-icon
# 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 

code-icon
code-icon
ConcludeChitChatDialogue
messages if it wasn't preceded with a 
code-icon
code-icon
InitiateChitChatDialogue
 message, it can manage multiple sessions with different states at the same time and in each session the history of exchanged messages in this session can simply be accessed by calling 
code-icon
code-icon
ctx.dialogue
.

code-icon
code-icon
@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 

code-icon
code-icon
ctx.broadcast
 comes in. It's like shouting your message into the crowd and only those who understand your lingo will respond.

But what if you want to have a meaningful conversation at this party? You need something more than just shouting into the crowd. Enter 

code-icon
code-icon
Dialogues
, the supercharged version of Protocols. It's like going from a game of charades to a well-structured debate. With Dialogues, you can have multi-message sessions, resume dialogues at any time, enforce the sequence of messages, and define general communication patterns. It's like having a well-choreographed dance routine in the middle of a party.

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.


More from Fetch