> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.asi1.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.asi1.ai/_mcp/server.

# Agent Chat Protocol

> Build agents that understand natural language and interoperate via the Agent Chat Protocol.

## Overview

The Agent Chat Protocol enables agents to understand natural language and interoperate across ecosystems. With this protocol, an agent can receive user messages, acknowledge them, optionally request structured outputs from other agents, and reply back in a consistent format.

* Learn more about the protocol spec: [Agent Chat Protocol (manifest)](https://agentverse.ai/manifests/proto:30a801ed3a83f9a0ff0a9f1e6fe958cb91da1fc2218b153df7b6cbf87bd33d62)
* Try chatting with a live agent on Agentverse: [Example Weather Agent](https://agentverse.ai/agents/details/agent1qde95qr0dzcnhhs8f65hkwujn9mh89jx0u7u7g6nv3tm2jxvjwhkunvessq/profile)

## What you'll build

This tutorial builds an agent that:

* Receives chat messages in natural language
* Acknowledges the message using the protocol
* Requests a structured output (JSON) from another agent (an OpenAI-backed agent)
* Uses the structured output to fetch current weather
* Sends a natural language reply back to the user

![Agent Chat Protocol](/_fern-img/e3e5369e813d515b35e6d638cd74327f59e254860b974f05117f7fc45135ded6.webp)![Agent Chat Protocol (Dark)](/_fern-img/7ab730146ceaf7f53203198109149ce0b051e96639ffcfdd366fdab9c9638e4b.webp)

## Prerequisites

* Python 3.10+
* Install dependencies:

```bash
pip install uagents requests
```

## Step 1: Create an agent

Start with a minimal agent.

```python
from uagents import Agent

agent = Agent()
```

* The `Agent` is the main runtime that sends and receives protocol-compliant messages.

## Step 2: Include the Chat Protocol

Attach the Agent Chat Protocol so the agent can send and receive `ChatMessage` and `ChatAcknowledgement` messages.

```python
from uagents import Context, Protocol
from uagents_core.contrib.protocols.chat import (
    ChatAcknowledgement,
    ChatMessage,
    TextContent,
    chat_protocol_spec,
    StartSessionContent,
    EndSessionContent,
)

chat_proto = Protocol(spec=chat_protocol_spec)
```

* `Protocol(spec=chat_protocol_spec)` adds handlers for a standard chat schema (text content, session start/end, acks).

## Step 3: Include a Structured Output Client Protocol

To request structured data (JSON) from another agent, define a small client protocol and two models: one for the prompt and schema, one for the response.

```python
from typing import Any, Dict
from uagents import Model

class StructuredOutputPrompt(Model):
    prompt: str
    output_schema: Dict[str, Any]

class StructuredOutputResponse(Model):
    output: Dict[str, Any]

struct_output_client_proto = Protocol(
    name="StructuredOutputClientProtocol", version="0.1.0"
)
```

* `StructuredOutputPrompt` goes to a remote AI agent, which returns a `StructuredOutputResponse` carrying a JSON object that matches the schema.

## Step 4: Create a text ChatMessage helper

A small helper to create consistent `ChatMessage` replies. Note the `EndSessionContent` uses `type="end-session"`.

```python
from datetime import datetime, timezone
from uuid import uuid4


def create_text_chat(text: str, end_session: bool = False) -> ChatMessage:
    content = [TextContent(type="text", text=text)]
    if end_session:
        content.append(EndSessionContent(type="end-session"))
    return ChatMessage(
        timestamp=datetime.now(timezone.utc),
        msg_id=uuid4(),
        content=content,
    )
```

## Step 5: Handle incoming chat and forward to the AI agent

* Acknowledge each inbound `ChatMessage`
* Forward the user's text to an AI agent that can return structured output matching the weather request schema

```python
from uagents import Agent, Context
from uagents_core.contrib.protocols.chat import (
    ChatAcknowledgement,
    ChatMessage,
    TextContent,
    StartSessionContent,
)

from functions import get_weather, WeatherRequest

AI_AGENT_ADDRESS = (
    "agent1qtlpfshtlcxekgrfcpmv7m9zpajuwu7d5jfyachvpa4u3dkt6k0uwwp2lct"  # OpenAI AI agent address
)

agent = Agent()

@chat_proto.on_message(ChatMessage)
async def handle_message(ctx: Context, sender: str, msg: ChatMessage):
    ctx.logger.info(f"Got a message from {sender}: {msg.content}")

    # Remember who sent this session's message so we can reply later
    ctx.storage.set(str(ctx.session), sender)

    # Acknowledge receipt
    await ctx.send(
        sender,
        ChatAcknowledgement(
            timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id
        ),
    )

    # Extract user text and forward to AI agent for structured output
    for item in msg.content:
        if isinstance(item, StartSessionContent):
            ctx.logger.info(f"Got a start session message from {sender}")
            continue
        elif isinstance(item, TextContent):
            ctx.logger.info(f"User said: {item.text}")

            # Ask the AI agent to produce JSON matching our schema
            await ctx.send(
                AI_AGENT_ADDRESS,
                StructuredOutputPrompt(
                    prompt=item.text, output_schema=WeatherRequest.schema()
                ),
            )
        else:
            ctx.logger.info(f"Got unexpected content from {sender}")
```

## Step 6: Handle structured output response and reply

* When the remote AI agent replies with a JSON object, parse it, fetch the weather, and send a natural-language message back to the original user. The final formatting happens on the agent side.

```python
@struct_output_client_proto.on_message(StructuredOutputResponse)
async def handle_structured_output_response(
    ctx: Context, sender: str, msg: StructuredOutputResponse
):
    ctx.logger.info(f"Structured output: {msg.output}")

    # Who started this session?
    session_sender = ctx.storage.get(str(ctx.session))
    if session_sender is None:
        ctx.logger.error("No session sender found in storage")
        return

    # Handle unknowns gracefully
    if "<UNKNOWN>" in str(msg.output):
        await ctx.send(
            session_sender,
            create_text_chat(
                "Sorry, I couldn't process your location request. Please try again later."
            ),
        )
        return

    # Extract location from structured output
    try:
        location = msg.output.get("location") if isinstance(msg.output, dict) else None
    except Exception:
        location = None

    try:
        if not location:
            raise ValueError("No location provided in structured output")
        weather = get_weather(location)
        ctx.logger.info(str(weather))
    except Exception as err:
        ctx.logger.error(f"Error: {err}")
        await ctx.send(
            session_sender,
            create_text_chat(
                "Sorry, I couldn't process your request. Please try again later."
            ),
        )
        return

    # Reply uses pre-formatted text from get_weather
    reply = weather.get("weather") or f"Weather for {location}: (no data)"
    await ctx.send(session_sender, create_text_chat(reply))
```

## Step 7: Wire up protocols and run

```python
agent.include(chat_proto, publish_manifest=True)
agent.include(struct_output_client_proto, publish_manifest=True)

if __name__ == "__main__":
    agent.run()
```

## Weather utility module (`functions.py`)

This helper module defines the expected schema for the structured output and a function to fetch weather from Open-Meteo. It returns a single pre-formatted string under `weather`.

The full module is in [Complete example](#complete-example-copy-paste) below,
alongside the finished `agents.py`.

## Complete example (copy-paste)

Combine everything into two files you can run as a hosted agent or locally.

> **Info**
>
> This Weather Agent is an Agentverse **hosted agent**. You can
> create your own hosted agent by following the guide here:
>
> [Hosted Agents](https://innovationlab.fetch.ai/resources/docs/agent-creation/uagent-creation#hosted-agents)
>
> .

```python
# agents.py
from uagents import Agent, Context, Protocol
from uagents_core.contrib.protocols.chat import (
    ChatAcknowledgement,
    ChatMessage,
    TextContent,
    chat_protocol_spec,
    StartSessionContent,
    EndSessionContent,
)
from functions import get_weather, WeatherRequest
from datetime import datetime, timezone
from uuid import uuid4
from typing import Any, Dict
from uagents import Model

class StructuredOutputPrompt(Model):
    prompt: str
    output_schema: Dict[str, Any]

class StructuredOutputResponse(Model):
    output: Dict[str, Any]

AI_AGENT_ADDRESS = "agent1qtlpfshtlcxekgrfcpmv7m9zpajuwu7d5jfyachvpa4u3dkt6k0uwwp2lct" # OpenAI ai agent address

agent = Agent()

chat_proto = Protocol(spec=chat_protocol_spec)
struct_output_client_proto = Protocol(
    name="StructuredOutputClientProtocol", version="0.1.0"
)

def create_text_chat(text: str, end_session: bool = False) -> ChatMessage:
    content = [TextContent(type="text", text=text)]
    if end_session:
        content.append(EndSessionContent(type="end-session"))
    return ChatMessage(
        timestamp=datetime.now(timezone.utc),
        msg_id=uuid4(),
        content=content,
    )

@chat_proto.on_message(ChatMessage)
async def handle_message(ctx: Context, sender: str, msg: ChatMessage):
    ctx.logger.info(f"Got a message from {sender}: {msg.content}")
    ctx.storage.set(str(ctx.session), sender)
    await ctx.send(
        sender,
        ChatAcknowledgement(timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id),
    )

    for item in msg.content:
        if isinstance(item, StartSessionContent):
            ctx.logger.info(f"Got a start session message from {sender}")
            continue
        elif isinstance(item, TextContent):
            ctx.logger.info(f"Got a message from {sender}: {item.text}")
            ctx.storage.set(str(ctx.session), sender)
            await ctx.send(
                AI_AGENT_ADDRESS,
                StructuredOutputPrompt(
                    prompt=item.text, output_schema=WeatherRequest.schema()
                ),
            )
        else:
            ctx.logger.info(f"Got unexpected content from {sender}")

@chat_proto.on_message(ChatAcknowledgement)
async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement):
    ctx.logger.info(
        f"Got an acknowledgement from {sender} for {msg.acknowledged_msg_id}"
    )

@struct_output_client_proto.on_message(StructuredOutputResponse)
async def handle_structured_output_response(
    ctx: Context, sender: str, msg: StructuredOutputResponse
):
    ctx.logger.info(f'Here is the message from structured output {msg.output}')
    session_sender = ctx.storage.get(str(ctx.session))
    if session_sender is None:
        ctx.logger.error(
            "Discarding message because no session sender found in storage"
        )
        return

    if "<UNKNOWN>" in str(msg.output):
        await ctx.send(
            session_sender,
            create_text_chat(
                "Sorry, I couldn't process your location request. Please try again later."
            ),
        )
        return

    # Extract location robustly from dict output
    try:
        location = msg.output.get("location") if isinstance(msg.output, dict) else None
    except Exception:
        location = None

    try:
        if not location:
            raise ValueError("No location provided in structured output")
        weather = get_weather(location)
        ctx.logger.info(str(weather))
    except Exception as err:
        ctx.logger.error(f"Error: {err}")
        await ctx.send(
            session_sender,
            create_text_chat(
                "Sorry, I couldn't process your request. Please try again later."
            ),
        )
        return

    reply = weather.get("weather") or f"Weather for {location}: (no data)"
    chat_message = create_text_chat(reply)

    await ctx.send(session_sender, chat_message)

agent.include(chat_proto, publish_manifest=True)
agent.include(struct_output_client_proto, publish_manifest=True)

if __name__ == "__main__":
    agent.run()
```

```python
# functions.py
from uagents import Model
import requests

class WeatherRequest(Model):
    location : str

class WeatherResponse(Model):
    weather : str

def get_weather(location: str):
    """Return current weather for a location string (e.g., 'Paris, France')."""
    if not location or not location.strip():
        raise ValueError("location is required")

    # 1) Geocode
    geo_params = {"name": location, "count": 1, "language": "en", "format": "json"}
    gr = requests.get(
        "https://geocoding-api.open-meteo.com/v1/search",
        params=geo_params,
        timeout=60,
    )
    gr.raise_for_status()
    g = gr.json()
    if not g.get("results"):
        raise RuntimeError(f"No geocoding match for: {location}")

    r0 = g["results"][0]
    latitude = r0["latitude"]
    longitude = r0["longitude"]
    timezone = r0.get("timezone") or "auto"
    display = ", ".join([v for v in [r0.get("name"), r0.get("admin1"), r0.get("country")] if v])

    # 2) Current weather
    wx_params = {
        "latitude": latitude,
        "longitude": longitude,
        "timezone": timezone,
        "current": (
            "temperature_2m,apparent_temperature,relative_humidity_2m,"
            "weather_code,wind_speed_10m,wind_direction_10m,is_day,precipitation"
        ),
    }
    wr = requests.get("https://api.open-meteo.com/v1/forecast", params=wx_params, timeout=60)
    wr.raise_for_status()
    data = wr.json()

    current = data.get("current") or data.get("current_weather") or {}
    temp = current.get("temperature_2m")
    app = current.get("apparent_temperature")
    wind = current.get("wind_speed_10m")
    rh = current.get("relative_humidity_2m")

    parts = [f"Weather for {display}"]
    if temp is not None:
        parts.append(f"temp {temp}°C")
    if app is not None:
        parts.append(f"feels like {app}°C")
    if rh is not None:
        parts.append(f"RH {rh}%")
    if wind is not None:
        parts.append(f"wind {wind} km/h")

    return {"weather": ", ".join(parts)}
```

## Why the Agent Chat Protocol is useful

* Natural language first: users can speak or type naturally; your agent wraps messages in a standard structure
* Interoperability: any agent implementing the protocol can communicate, regardless of internal implementation
* Extensible: add client protocols (like structured output) to connect to specialized agents
* Reliability: acknowledgements and session controls make delivery and session state explicit

You can also use this agent through ASI:One by mentioning it directly in your prompt, for example:

```text
@agent1qde95qr0dzcnhhs8f65hkwujn9mh89jx0u7u7g6nv3tm2jxvjwhkunvessq please get me weather of San Francisco.
```

To try a live conversation experience, visit the [Example Weather Agent on Agentverse](https://agentverse.ai/agents/details/agent1qde95qr0dzcnhhs8f65hkwujn9mh89jx0u7u7g6nv3tm2jxvjwhkunvessq/profile).

![Agent chat in ASI:One (Light)](/_fern-img/bda413a1786110d04f69988e00397ea3ce2aef4986d0a0cdb99cb25becdf6020.webp)![Agent chat in ASI:One (Dark)](/_fern-img/3950aa9c79d3304733906c066f3028965231831e29706186ef7bda34425ae440.webp)

## Next steps

1. **[Planner Mode](/documentation/build-with-asi-one/planner)** - How ASI:One discovers and calls agents like this one
2. **[Chat Completions API](/documentation/build-with-asi-one/chat-completions)** - Call ASI:One directly from your own code
3. **[Structured Data](/documentation/build-with-asi-one/structured-data)** - Constraining an ASI:One reply to a JSON schema, the HTTP equivalent of the pattern above