diff --git a/units/en/unit0/introduction.mdx b/units/en/unit0/introduction.mdx
index d19a49a28..190438b1f 100644
--- a/units/en/unit0/introduction.mdx
+++ b/units/en/unit0/introduction.mdx
@@ -72,8 +72,9 @@ Here is the **general syllabus for the course**. A more detailed list of topics
| 2 | Frameworks | Understand how the fundamentals are implemented in popular libraries : smolagents, LangGraph, LLamaIndex |
| 3 | Use Cases | Let's build some real life use cases (open to PRs 🤗 from experienced Agent builders) |
| 4 | Final Assignment | Build an agent for a selected benchmark and prove your understanding of Agents on the student leaderboard 🚀 |
+| 5 | Bonus Gradio Module | Learn to build and deploy interactive AI agents with Gradio interfaces |
-*We are also planning to release some bonus units, stay tuned!*
+*We have one bonus unit available for you: Gradio module helps you create interactive interfaces for your agents. More bonus units coming soon!*
## What are the prerequisites?
diff --git a/units/en/unit5/1_introduction.mdx b/units/en/unit5/1_introduction.mdx
new file mode 100644
index 000000000..db254bfaf
--- /dev/null
+++ b/units/en/unit5/1_introduction.mdx
@@ -0,0 +1,85 @@
+# Introduction to Gradio for AI Agents
+
+
+
+In the previous units, we learned how to create powerful AI Agents that can reason, plan, and take actions. But an Agent is only as effective as its ability to interact with users. This is where **Gradio** comes in.
+
+## Why Do We Need a UI for Our Agents?
+
+Meet Sarah, a data scientist who built an amazing AI Agent that can analyze financial data and generate reports. But there's a challenge:
+
+- Her teammates need to interact with the Agent
+- Not everyone is comfortable with code or command line
+- Users want to see the Agent's thought process
+- The Agent needs to handle file uploads and display visualizations
+
+The solution? A user-friendly interface that makes the Agent accessible to everyone.
+
+## What is Gradio?
+
+Gradio is a Python library that makes it easy to create **beautiful web interfaces** for your AI models and Agents. Think of it as a bridge between your Agent's capabilities and its users:
+
+
+With Gradio, you can:
+- Create chat interfaces for your Agents in just a few lines of code
+- Display your Agent's thought process and tool usage
+- Handle file uploads and multimedia content that can be useful for your AI agents
+- Share your Agent with anyone via a URL
+- Deploy your Agent UI to Hugging Face Spaces
+
+## Why Gradio for Agents?
+
+Gradio is particularly well-suited for AI Agents because it offers:
+
+1. **Native Chat Support**: Built-in components for chat interfaces that match how Agents communicate
+
+2. **Thought Process Visualization**: Special features to display your Agent's reasoning steps and tool usage
+
+3. **Real-time Updates**: Stream your Agent's responses and show its progress
+
+4. **File Handling**: Easy integration of file uploads for Agents that process documents or media
+
+Here's a quick example of how simple it is to create an Agent interface with Gradio:
+
+```python
+from smolagents import (
+ load_tool,
+ CodeAgent,
+ HfApiModel,
+ GradioUI
+)
+
+# Import tool from Hub
+image_generation_tool = load_tool("m-ric/text-to-image", trust_remote_code=True)
+# initialize a model
+model = HfApiModel()
+# Initialize the agent with the image generation tool
+agent = CodeAgent(tools=[image_generation_tool], model=model)
+# launch the gradio agentic ui
+GradioUI(agent).launch(share=True)
+```
+
+This creates a complete chat interface:
+
+
+
+Note that this launches a Gradio UI; however, it does not offer the option to customize the interface. We will explore how to create a custom agentic UI with Gradio and how to build more complex systems that incorporate the Gradio agent UI.
+
+
+## Setting Up Gradio
+
+Before we dive deeper, let's set up Gradio in your environment:
+
+```bash
+pip install --upgrade gradio
+```
+
+
+If you're working in Google Colab or Jupyter, you can restart your runtime after installing Gradio to ensure all components are properly loaded.
+
+In the next section, we'll build our first Agent interface using Gradio's ChatInterface. We'll see how to:
+- Create a basic chat UI
+- Display the Agent's responses
+- Handle user inputs effectively
+
+Ready to build your first Agent UI? Let's move on to the next chapter!
\ No newline at end of file
diff --git a/units/en/unit5/2_first-agent-interface.mdx b/units/en/unit5/2_first-agent-interface.mdx
new file mode 100644
index 000000000..b1dccafce
--- /dev/null
+++ b/units/en/unit5/2_first-agent-interface.mdx
@@ -0,0 +1,241 @@
+# Building Your First Agent Interface
+
+Now that we understand why Gradio is useful for Agents, let's create our first Agent interface! We'll start with the gr.ChatInterface, which provides everything we need to get an Agent up and running quickly.
+
+## The ChatInterface Component
+
+The `gr.ChatInterface` is a high-level component that handles all the essential parts of a chat application:
+- Message history management
+- User input handling
+- Bot response display
+- Real-time / streaming updates
+
+
+## Your First Agent UI
+
+We'll start by installing langchain and langgraph for this section. Additionally, set up the environment variable `OPENAI_API_KEY` to store your OpenAI API keys.
+
+```python
+pip install langchain
+pip install langchain-openai langgraph
+
+# OPENAI_API_KEY
+import os
+os.environ["OPENAI_API_KEY"]="your-openai-api-key"
+```
+
+
+Let's create a simple Agent that helps with weather information. Here's how to build it:
+
+```python
+import os
+import gradio as gr
+from gradio import ChatMessage
+import requests
+from typing import Dict, List
+from langchain_core.messages import HumanMessage
+from langchain_core.tools import tool
+from langchain_openai import ChatOpenAI
+from langgraph.checkpoint.memory import MemorySaver
+from langgraph.prebuilt import create_react_agent
+
+# Weather and location tools
+@tool
+def get_lat_lng(location_description: str) -> dict[str, float]:
+ """Get the latitude and longitude of a location."""
+ return {"lat": 51.1, "lng": -0.1} # London coordinates as dummy response
+
+@tool
+def get_weather(lat: float, lng: float) -> dict[str, str]:
+ """Get the weather at a location."""
+ return {"temperature": "21°C", "description": "Sunny"} # Dummy response
+
+
+def stream_from_agent(message: str, history: List[Dict[str, str]]) -> gr.ChatMessage:
+ """Process messages through the LangChain agent with visible reasoning."""
+
+ # Initialize the agent
+ llm = ChatOpenAI(temperature=0, model="gpt-4")
+ memory = MemorySaver()
+ tools = [get_lat_lng, get_weather]
+ agent_executor = create_react_agent(llm, tools, checkpointer=memory)
+
+ # Add message to history
+ past_messages = [HumanMessage(content=message)]
+ for h in history:
+ if h["role"] == "user":
+ past_messages.append(HumanMessage(content=h["content"]))
+
+ messages_to_display = []
+ final_response = None
+
+ for chunk in agent_executor.stream(
+ {"messages": past_messages},
+ config={"configurable": {"thread_id": "abc123"}}
+ ):
+ # Handle agent's actions and tool usage
+ if chunk.get("agent"):
+ for msg in chunk["agent"]["messages"]:
+ if msg.content:
+ final_response = msg.content
+
+ # Handle tool calls
+ for tool_call in msg.tool_calls:
+ tool_message = ChatMessage(
+ content=f"Parameters: {tool_call['args']}",
+ metadata={
+ "title": f"🛠️ Using {tool_call['name']}",
+ "id": tool_call["id"],
+ }
+ )
+ messages_to_display.append(tool_message)
+ yield messages_to_display
+
+ # Handle tool responses
+ if chunk.get("tools"):
+ for tool_response in chunk["tools"]["messages"]:
+ # Find the corresponding tool message
+ for msg in messages_to_display:
+ if msg.metadata.get("id") == tool_response.tool_call_id:
+ msg.content += f"\nResult: {tool_response.content}"
+ yield messages_to_display
+
+ # Add the final response as a regular message
+ if final_response:
+ messages_to_display.append(ChatMessage(content=final_response))
+ yield messages_to_display
+
+# Create the Gradio interface
+demo = gr.ChatInterface(
+ fn=stream_from_agent,
+ type="messages",
+ title="🌤️ Weather Assistant",
+ description="Ask about the weather anywhere! Watch as I gather the information step by step.",
+ examples=[
+ "What's the weather like in Tokyo?",
+ "Is it sunny in Paris right now?",
+ "Should I bring an umbrella in New York today?"
+ ],
+
+)
+
+demo.launch()
+```
+
+This creates a complete chat interface:
+
+
+## Understanding the key elements
+
+Let's break down what's happening:
+
+1. **The Agent Function**:
+ ```python
+ def stream_from_agent(message: str, history: List[Dict[str, str]]) -> gr.ChatMessage:
+ ```
+ - Processes messages through the LangChain agent
+ - Takes the current message and chat history
+ - Uses `yield` to show intermediate tool-usage and thoughts
+ - Returns the final response
+
+2. **Tools available**:
+ ```python
+ def get_lat_lng(location_description: str) -> dict[str, float]:
+ def get_weather(lat: float, lng: float) -> dict[str, str]:
+ ```
+ - get_lat_lng : Get lattitude and longitude of a place from the place description
+ - get_weather : Get weather details for the given latitude and logitude
+
+3. **ChatInterface Configuration**:
+ ```python
+ demo = gr.ChatInterface(
+ fn=stream_from_agent,
+ title="🌤️ Weather Assistant",
+ ...
+ )
+ ```
+ - Connects the Agent function
+ - Sets up the UI appearance
+ - Configures interactive features
+
+
+3. **Streaming Responses**:
+ - Using `yield` enables real-time updates
+ - Shows the Agent's thought process and tool-usage in real time
+ - Keeps users engaged while waiting
+
+We are using `type="messages"` in `gr.ChatInterface`. This passes the history as a list of dictionaries with openai-style "role" and "content" keys.
+
+ChatInterface requires only one parameter: fn, a function that returns LLM responses based on the user input and chat history. There are additional parameters, like `type`, that help control the chatbot's appearance and behavior. We'll explore the ChatInterface class further in the following chapters.
+
+## Adding More Features
+
+Let's enhance our Agent UI with more capabilities:
+
+1. **Auto closing the tool usage accordion after the agent is done populating them.**
+
+Using the Metadata dictionary key called `status` to communicate to the gradio ui when the tool usage is complete.
+This also adds a spinner appears next to the thought title. If the `status` is "done", the thought accordion becomes closed. If not provided, the thought accordion is open and no spinner is displayed.
+
+```python
+
+def stream_from_agent(message: str, history: List[Dict[str, str]]) -> gr.ChatMessage:
+ """
+ Process messages through the LangChain agent with visible reasoning.
+ """
+ ...
+ # Handle tool calls
+ for tool_call in msg.tool_calls:
+ tool_message = ChatMessage(
+ content=f"Parameters: {tool_call['args']}",
+ metadata={
+ "title": f"🛠️ Using {tool_call['name']}",
+ "id": tool_call["id"],
+ "status": "pending",
+ }
+ )
+ messages_to_display.append(tool_message)
+ yield messages_to_display
+ tool_message.metadata["status"] = "done"
+ ...
+```
+
+2. **Add more features**
+- Allow editing of past messages to regenerate response.
+- Add example icons to the examples
+- Display Chatgpt type Chat history in the UI.
+
+```python
+
+# Create enhanced Gradio interface
+demo = gr.ChatInterface(
+ fn=stream_from_agent,
+ type="messages",
+ title="🌤️ Weather Assistant",
+ description="Ask about the weather anywhere! Watch as I gather the information step by step.",
+ examples=[
+ "What's the weather like in Tokyo?",
+ "Is it sunny in Paris right now?",
+ "Should I bring an umbrella in New York today?"
+ ],
+ example_icons=["https://cdn3.iconfinder.com/data/icons/landmark-outline/432/japan_tower_tokyo_landmark_travel_architecture_tourism_view-256.png",
+ "https://cdn2.iconfinder.com/data/icons/city-building-1/200/ArcdeTriomphe-256.png",
+ "https://cdn2.iconfinder.com/data/icons/city-icons-for-offscreen-magazine/80/new-york-256.png"
+ ],
+ save_history=True,
+ editable=True
+
+)
+```
+
+Try modifying the example above to create your own Agent interface. Experiment with different use-cases like research, article-writing, travel-planner, _etc._
+
+## What's Next?
+
+You now have a foundation for building Agent interfaces! In the next chapter, we'll dive deeper into `gr.ChatMessage` and learn how to display complex Agent behaviors like:
+- Tool usage visualization
+- Structured thought processes
+- Multi-step reasoning
+- Nested agents
+
+We'll also explore how to integrate these interfaces with more world-class LLMs and external tools.
\ No newline at end of file
diff --git a/units/en/unit5/3_enhance-agent-with-chatmessage.mdx b/units/en/unit5/3_enhance-agent-with-chatmessage.mdx
new file mode 100644
index 000000000..7408d739e
--- /dev/null
+++ b/units/en/unit5/3_enhance-agent-with-chatmessage.mdx
@@ -0,0 +1,367 @@
+# Enhancing Agent Interactions with ChatMessage
+
+In the previous chapters, we created a basic Agent interface. Now, let's enhance it to display the rich behaviors that make Agents special: their thought processes, reasoning steps, and tool usage.
+
+## Understanding gr.ChatMessage Format
+
+For advanced Agent UIs, we need more control over message formatting. This is where the `gr.ChatMessage` class comes in:
+
+```python
+from gradio import ChatMessage
+
+# Basic usage
+message = ChatMessage(
+ content="Hello, I'm an AI Assistant",
+ role="assistant"
+)
+
+# Example with metadata dictionary for displaying LLM thoughts
+thinking_message = ChatMessage(
+ content="Let me analyze this step by step...",
+ role="assistant",
+ metadata={"title": "🧠 Thinking"}
+)
+```
+
+- The ChatMessage dataclass has this structure:
+
+```python
+@dataclass
+class ChatMessage:
+ content: str | Component # Text or a media component
+ role: str # "user" or "assistant"
+ metadata: dict = None # For thought display, nesting, etc.
+ options: list[dict] = None # For suggested responses
+```
+
+The only required field is content. The value of `gr.Chatbot` is a list of these ChatMessage dataclasses.
+
+- The MetadataDict is a typed dictionary to represent metadata for a message in the Chatbot component. An instance of this dictionary is used for the metadata field in a ChatMessage when the chat message should be displayed as a thought or a tool use.
+
+```python
+class MetadataDict(TypedDict):
+ title: str # Title of the thought or tool-use bubble
+ id: int | str # For nested thoughts, Agents
+ parent_id: int | str # For nested thoughts, Agents
+ duration: float # For time duration of your step
+ status: Literal # "pending" or "done"
+ log: str # For loging any other information for a step
+```
+The only required field is title. When status is set "pending", an animated spinner is shown next to the title, when set as "done" the spinner stops and disappear.
+
+- The OptionDict is a typed dictionary to represent an option in a ChatMessage. A list of these dictionaries is used for the options field in a ChatMessage.
+
+```python
+class OptionDict(TypedDict):
+ label: str # label of the option
+ value: str # value that is returned on selection
+```
+Use the `options` key in the `gr.ChatMessage` instance to specify the preset responses in a given chat message.
+
+
+## Displaying Agent Thoughts and Reasoning
+
+One of the most powerful features of Agents is their ability to reason through problems. With `gr.ChatMessage`, we can make this thinking visible. Let's look at an example where you display an LLM's thoughts using the ChatMessage class and the Metadata dictionary :
+
+```python
+import os
+import re
+import gradio as gr
+from gradio import ChatMessage
+
+# Setting up the APi Key for accessing DeepSeek R1
+os.environ["SAMBANOVA_API_KEY"]="xxx"
+
+client = openai.OpenAI(
+ api_key=os.environ.get("SAMBANOVA_API_KEY"),
+ base_url="https://api.sambanova.ai/v1",
+)
+
+# Function to parse the DeepSeek response and extract thinking content
+def parse_deepseek_response(response_text):
+ # Extract content between tags
+ thinking_match = re.search(r'(.*?)', response_text, re.DOTALL)
+
+ if thinking_match:
+ thinking = thinking_match.group(1).strip()
+ # Get the actual response (everything after )
+ actual_response = re.sub(r'.*?', '', response_text, flags=re.DOTALL).strip()
+ return thinking, actual_response
+ else:
+ # If no thinking tags found, return empty thinking and full response
+ return "", response_text
+
+# Chat function for the ChatInterface
+def deepseek_chat(message, history):
+ # Convert history to the format expected by the OpenAI API
+ messages = []
+
+ # Add system message
+ messages.append({"role": "system", "content": "You are a helpful assistant"})
+
+ # Add chat history
+ for msg in history:
+ messages.append({"role": msg["role"], "content": msg["content"]})
+
+ # Add the current message
+ messages.append({"role": "user", "content": message})
+
+ try:
+ # Get the sambanova client and make the API call
+ response = client.chat.completions.create(
+ model="DeepSeek-R1",
+ messages=messages,
+ temperature=0.1,
+ top_p=0.1
+ )
+
+ # Get the response content
+ response_text = response.choices[0].message.content
+
+ # Parse the response to extract thinking and final response
+ thinking, actual_response = parse_deepseek_response(response_text)
+
+ # Return list of ChatMessage objects
+ result = []
+
+ # Add thinking as a collapsible section if it exists
+ if thinking:
+ result.append(
+ ChatMessage(
+ content=thinking,
+ metadata={"title": "🧠 Thinking Process"}
+ )
+ )
+
+ # Add the actual response
+ result.append(
+ ChatMessage(
+ content=actual_response
+ )
+ )
+
+ return result
+
+ except Exception as e:
+ # Handle any errors
+ return [
+ ChatMessage(
+ content=f"An error occurred: {str(e)}",
+ metadata={"title": "⚠️ Error"}
+ )
+ ]
+
+# Create the Gradio interface
+demo = gr.ChatInterface(
+ fn=deepseek_chat,
+ type="messages",
+ title="DeepSeek-R1 Chatbot with Thinking Visualization",
+ description="This chatbot shows the DeepSeek model's thinking process in a collapsible section.",
+ examples=[
+ "Hello there!",
+ "Is 9.9 > 9.11?",
+ "How many r are there in the word strawberry?",
+ "What's the meaning of life? Give non obvious answer only."
+ ],
+
+)
+
+demo.launch()
+```
+
+This creates an interface where users can see the Agent's thought process unfold in real-time.
+
+< Need to add a gif of the app deployed on Spaces >
+
+
+## Tool Usage Visualization
+
+Now let's visualize dummy Agents using the `gr.ChatMessage` dataclass - a key capability we learned above:
+
+```python
+import gradio as gr
+from gradio import ChatMessage
+import time
+import random
+
+def calculator_tool(expression):
+ """Simple calculator tool"""
+ try:
+ return eval(expression)
+ except:
+ return "Error: Could not evaluate expression"
+
+def weather_tool(location):
+ """Simulated weather tool"""
+ conditions = ["sunny", "cloudy", "rainy", "snowy"]
+ temp = random.randint(0, 35)
+ return f"{random.choice(conditions)}, {temp}°C in {location}"
+
+def agent_with_tools(query, history):
+ # Add user message
+ history.append({"role": "user", "content": query})
+ yield history
+
+ # Initial thinking
+ thinking = ChatMessage(
+ role="assistant",
+ content="Let me think about this query...",
+ metadata={"title": "🧠 Thinking", "id": 1}
+ )
+ history.append(thinking)
+ yield history
+
+ # Decide which tool to use based on query
+ if "calculate" in query.lower() or any(op in query for op in ['+', '-', '*', '/']):
+ # Extract the expression (simplified for demo)
+ expression = query.split("calculate")[-1].strip() if "calculate" in query.lower() else query
+
+ # Show tool usage as nested thought
+ tool_call = ChatMessage(
+ role="assistant",
+ content=f"Expression to evaluate: {expression}",
+ metadata={
+ "title": "🧮 Calculator Tool",
+ "parent_id": 1,
+ "id": 2,
+ "status": "pending"
+ }
+ )
+ history.append(tool_call)
+ yield history
+
+ # Simulate calculation time
+ time.sleep(1)
+
+ # Get result and update tool call
+ result = calculator_tool(expression)
+ tool_call.content = f"Expression: {expression}\nResult: {result}"
+ tool_call.metadata["status"] = "done"
+ tool_call.metadata["duration"] = 0.8 # Simulated duration
+ yield history
+
+ # Final response
+ response = ChatMessage(
+ role="assistant",
+ content=f"I calculated that for you. The result is {result}."
+ )
+
+ elif "weather" in query.lower():
+ # Extract location (simplified)
+ location = query.split("weather in")[-1].strip() if "weather in" in query.lower() else "your location"
+
+ # Show tool usage
+ tool_call = ChatMessage(
+ role="assistant",
+ content=f"Checking weather for: {location}",
+ metadata={
+ "title": "🌤️ Weather Tool",
+ "parent_id": 1,
+ "id": 2,
+ "status": "pending"
+ }
+ )
+ history.append(tool_call)
+ yield history
+
+ # Simulate API call
+ time.sleep(1.5)
+
+ # Get result and update tool call
+ result = weather_tool(location)
+ tool_call.content = f"Location: {location}\nCurrent conditions: {result}"
+ tool_call.metadata["status"] = "done"
+ tool_call.metadata["duration"] = 1.2 # Simulated duration
+ yield history
+
+ # Final response
+ response = ChatMessage(
+ role="assistant",
+ content=f"I checked the weather for you. It's currently {result}."
+ )
+ else:
+ # Default response for other queries
+ time.sleep(1)
+ response = ChatMessage(
+ role="assistant",
+ content=f"I understand you're asking about '{query}', but I don't have a specific tool for that. I can help with calculations or weather."
+ )
+
+ # Add final response
+ history.append(response)
+ yield history
+
+demo = gr.ChatInterface(
+ agent_with_tools,
+ title="Agent with Tool Visualization",
+ description="Ask me to calculate something or check the weather!",
+ examples=[
+ "Calculate 145 * 32",
+ "What's the weather in Tokyo?",
+ "Tell me about quantum physics"
+ ],
+ type="messages"
+)
+
+demo.launch()
+```
+
+This creates an interface that visually shows:
+1. The Agent thinking
+2. The Agent deciding which tool to use
+3. The tool execution as a nested step
+4. The final response that incorporates tool results
+
+< Add a gif of the gradio ui for this chatbot: https://huggingface.co/spaces/ysharma/Enhance_UI_with_ChatMEssage>
+
+
+## Nested Thoughts for Complex Reasoning
+
+For Agents that follow complex reasoning chains, we can use nested thoughts:
+
+```python
+# Parent thought
+parent = ChatMessage(
+ role="assistant",
+ content="Breaking down the problem",
+ metadata={"title": "🧠 Main Analysis", "id": "main"}
+)
+
+# Child thoughts
+child1 = ChatMessage(
+ role="assistant",
+ content="Analyzing first component",
+ metadata={"title": "Step 1", "parent_id": "main", "id": "step1"}
+)
+
+child2 = ChatMessage(
+ role="assistant",
+ content="Analyzing second component",
+ metadata={"title": "Step 2", "parent_id": "main", "id": "step2"}
+)
+```
+
+This approach can be used to represent nested-agents too.
+When using IDs for nested thoughts, the IDs can be any unique strings or numbers. Just make sure the `parent_id` of a child thought matches the `id` of its parent.
+
+< To Dos>
+Add real examples for
+- Multi-step reasoning (smolagents)
+- Nested agents (smolagents)
+
+## What's Next?
+
+You've now learned how to create advanced Agent interfaces that display:
+- Structured thinking processes
+- Tool usage with status indicators
+- Complex reasoning with nested thoughts and nested agents
+
+In the next chapter, we'll explore additional advanced features like:
+- Adding file upload capabilities
+- Customizing with Sidebar and Multipage components
+- Styling and theming your Agent UI < re look>
+- < need to add more things >
+
+These features will help you build fully-featured Agent applications that are intuitive and powerful for users.
+
+Try combining the examples in this chapter with your own Agent logic. Experiment with different thought structures and tool visualizations to find the clearest way to display your Agent's capabilities.
diff --git a/units/en/unit5/4_build_with MCPserver_and_client.mdx b/units/en/unit5/4_build_with MCPserver_and_client.mdx
new file mode 100644
index 000000000..263bdde3f
--- /dev/null
+++ b/units/en/unit5/4_build_with MCPserver_and_client.mdx
@@ -0,0 +1,580 @@
+# Building Agent UIs with Model Context Protocol (MCP)
+
+In previous units, we explored creating basic Agent interfaces with Gradio's ChatInterface and advanced visualization using ChatMessage. Now, let's take your Agent UIs to the next level by integrating with the Model Context Protocol (MCP), which enables seamless communication between Claude and external tools.
+
+## What is Model Context Protocol (MCP)?
+The Model Context Protocol (MCP) is an open standard that allows LLMs to connect with external data sources and tools. Think of MCP like a "USB-C port for AI" - it provides a standardized way for AI models to interact with various tools and data sources.
+
+With MCP you can:
+- Connect Claude Desktop to specialized tools that extend its capabilities
+- Create a modular architecture where tools can be easily added or swapped
+- Use an LLM application like Gradio chatbot and use it as a MCP Client
+
+
+## Setting Up Your MCP Environment
+
+Before we start building our Gradio interface, you'll need to set up your MCP environment:
+
+```bash
+
+pip install mcp-client anthropic gradio
+```
+
+For this tutorial, we'll use a simple weather MCP server. You can find the full code for the server `weather.py` [here](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/weather-server-python/weather.py).
+
+## Building a Gradio Interface for MCP
+
+Let us create a Gradio application that connects to an MCP weather server and allows users to interact with Claude and MCP tools through a chat interface.
+
+First, setting up the necessary imports:
+
+### Imports and Setup
+
+```python
+
+import gradio as gr
+from gradio.components.chatbot import ChatMessage
+import asyncio
+import os
+import sys
+import json
+from typing import List, Dict, Any, Union
+from contextlib import AsyncExitStack
+
+from mcp import ClientSession, StdioServerParameters
+from mcp.client.stdio import stdio_client
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+# Create a simple event loop for async operations
+loop = asyncio.new_event_loop()
+asyncio.set_event_loop(loop)
+```
+
+- We import necessary libraries: Gradio for the interface, asyncio for handling asynchronous operations, and other utility modules.
+- The MCP-specific imports (ClientSession, StdioServerParameters, stdio_client) allow us to connect to MCP servers. We import the Anthropic client to interact with Claude.
+- You need to make sure you have set your Anthropic API key as an environment variable: `ANTHROPIC_API_KEY=your_api_key_here`
+- We set up an event loop for asyncio operations, which will be important for handling the asynchronous nature of MCP tools.
+
+Next, let us create the `MCPClientWrapper` class that handles connecting to the MCP server and processing messages.
+
+### The MCPClientWrapper Class
+This class wraps all the functionality needed to connect to an MCP server.
+
+#### Initialization
+```python
+class MCPClientWrapper:
+ def __init__(self):
+ self.session = None
+ self.exit_stack = None
+ self.anthropic = Anthropic()
+ self.tools = []
+```
+
+- `session`: Will hold the MCP client session once connected
+- `exit_stack`: Manages asynchronous resources and their cleanup
+- `anthropic`: The Anthropic client for calling Claude
+- `tools`: Will store the available tools from the MCP server
+
+#### Connection Method
+```python
+def connect(self, server_path: str) -> str:
+ """Connect to the MCP server"""
+ if not server_path:
+ return "Error: Please provide a server script path"
+
+ try:
+ # Run the async connect operation in the event loop
+ async def _connect():
+ # Clean up any existing connections
+ if self.exit_stack:
+ await self.exit_stack.aclose()
+
+ self.exit_stack = AsyncExitStack()
+
+ # Determine if Python or Node.js script
+ is_python = server_path.endswith('.py')
+ is_js = server_path.endswith('.js')
+ if not (is_python or is_js):
+ raise ValueError("Server script must be a .py or .js file")
+
+ command = "python" if is_python else "node"
+ server_params = StdioServerParameters(
+ command=command,
+ args=[server_path],
+ env=None
+ )
+
+ # Create connection to server
+ stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
+ self.stdio, self.write = stdio_transport
+ self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
+
+ # Initialize session and get available tools
+ await self.session.initialize()
+ response = await self.session.list_tools()
+ tools = response.tools
+
+ self.tools = [{
+ "name": tool.name,
+ "description": tool.description,
+ "input_schema": tool.inputSchema
+ } for tool in response.tools]
+
+ return [tool.name for tool in tools]
+
+ tool_names = loop.run_until_complete(_connect())
+ return f"✅ Connected to MCP server. Available tools: {', '.join(tool_names)}"
+
+ except Exception as e:
+ return f"❌ Error connecting to server: {str(e)}"
+```
+
+This method:
+
+- Takes a path to an MCP server script (like `weather.py` in our example)
+- Determines whether it's a Python or JavaScript script
+- Sets up the appropriate command to run the script
+- Establishes a connection to the server through stdio (standard input/output)
+- Initializes the session and retrieves available tools
+- Returns a success or error message
+
+The connection process is asynchronous, so we define an inner `_connect()` async function and run it with `loop.run_until_complete()`.
+
+#### Message Processing
+
+```python
+ def process_message(self, message: str, history: List[Union[Dict[str, Any], ChatMessage]]) -> List[Union[Dict[str, Any], ChatMessage]]:
+ """Process a message using Claude and the MCP tools"""
+ if not self.session:
+ return history + [ChatMessage(role="assistant", content="Please connect to an MCP server first.")]
+
+ try:
+ # Run the async query operation in the event loop
+ async def _process_query():
+ # Format conversation history for Claude
+ claude_messages = []
+ for msg in history:
+ # Handle both ChatMessage objects and dictionaries
+ if isinstance(msg, ChatMessage):
+ role = msg.role
+ content = msg.content
+ else: # Dictionary
+ role = msg.get("role")
+ content = msg.get("content")
+
+ # Skip metadata and other fields
+ if role in ["user", "assistant", "system"]:
+ claude_messages.append({"role": role, "content": content})
+
+ # Add current query
+ claude_messages.append({"role": "user", "content": message})
+
+ # Initial Claude API call
+ response = self.anthropic.messages.create(
+ model="claude-3-5-sonnet-20241022",
+ max_tokens=1000,
+ messages=claude_messages,
+ tools=self.tools
+ )
+
+ # Process response and handle tool calls
+ result_messages = []
+
+ for content in response.content:
+ if content.type == 'text':
+ # Add main text response - ensure content.text is a string
+ text_content = content.text
+ if isinstance(text_content, list):
+ # If it's a list, join it into a single string
+ text_content = " ".join(str(item) for item in text_content)
+
+ result_messages.append(ChatMessage(
+ role="assistant",
+ content=text_content
+ ))
+ elif content.type == 'tool_use':
+ tool_name = content.name
+ tool_args = content.input
+
+ # Create a thought message for the tool call
+ result_messages.append(ChatMessage(
+ role="assistant",
+ content=f"I'll use the {tool_name} tool to help answer your question.",
+ metadata={
+ "title": f"🛠️ Using tool: {tool_name}",
+ "log": f"Parameters: {tool_args}",
+ "status": "pending",
+ "id": f"tool_call_{tool_name}"
+ }
+ ))
+
+ # Add formatted tool parameters for better readability
+ formatted_args = "```json\n" + json.dumps(tool_args, indent=2) + "\n```"
+ result_messages.append(ChatMessage(
+ role="assistant",
+ content=formatted_args,
+ metadata={
+ "parent_id": f"tool_call_{tool_name}",
+ "id": f"params_{tool_name}",
+ "title": "Tool Parameters"
+ }
+ ))
+
+ # Execute tool call
+ result = await self.session.call_tool(tool_name, tool_args)
+
+ # Get result content and ensure it's a string
+ result_content = result.content
+ if isinstance(result_content, list):
+ result_content = "\n".join(str(item) for item in result_content)
+
+ # Update with tool results
+ result_messages.append(ChatMessage(
+ role="assistant",
+ content="Here are the results from the tool:",
+ metadata={
+ "title": f"📊 Tool Result for {tool_name}",
+ "status": "done",
+ "id": f"result_{tool_name}"
+ }
+ ))
+
+ # Add formatted result content
+ formatted_result = "```\n" + str(result_content) + "\n```"
+ result_messages.append(ChatMessage(
+ role="assistant",
+ content=formatted_result,
+ metadata={
+ "parent_id": f"result_{tool_name}",
+ "id": f"raw_result_{tool_name}",
+ "title": "Raw Output"
+ }
+ ))
+
+ # Continue conversation with tool results
+ # Build a simple message for Claude to continue
+ tool_response_for_claude = {"role": "user", "content": f"Tool result for {tool_name}: {str(result_content)}"}
+ claude_messages.append(tool_response_for_claude)
+
+ # Get next response from Claude with the tool results
+ next_response = self.anthropic.messages.create(
+ model="claude-3-5-sonnet-20241022",
+ max_tokens=1000,
+ messages=claude_messages,
+ )
+
+ # Add Claude's interpretation of the tool results
+ if next_response.content and next_response.content[0].type == 'text':
+ final_text = next_response.content[0].text
+ # Ensure final_text is a string
+ if isinstance(final_text, list):
+ final_text = " ".join(str(item) for item in final_text)
+
+ result_messages.append(ChatMessage(
+ role="assistant",
+ content=final_text
+ ))
+
+ return result_messages
+
+ new_messages = loop.run_until_complete(_process_query())
+ return history + [ChatMessage(role="user", content=message)] + new_messages
+
+ except Exception as e:
+ import traceback
+ error_details = traceback.format_exc()
+ return history + [
+ ChatMessage(role="user", content=message),
+ ChatMessage(role="assistant", content=f"❌ Error processing message: {str(e)}\n\nDetails: {error_details}")
+ ]
+```
+
+This method is the heart of the application. It:
+- Formats the conversation history for Claude
+- Calls Claude with the current message and available tools
+- Processes Claude's response, including any tool calls
+- Executes tools as needed and returns the results
+- Gets Claude's final response after seeing the tool results
+
+
+Let's further break down the key parts:
+
+**Preparing Conversation for Claude**
+
+This code prepares the conversation history in the format Claude expects (a list of messages with role and content).
+
+```python
+# Format conversation history for Claude
+claude_messages = []
+for msg in history:
+ # Handle both ChatMessage objects and dictionaries
+ if isinstance(msg, ChatMessage):
+ role = msg.role
+ content = msg.content
+ else: # Dictionary
+ role = msg.get("role")
+ content = msg.get("content")
+
+ # Skip metadata and other fields
+ if role in ["user", "assistant", "system"]:
+ claude_messages.append({"role": role, "content": content})
+
+# Add current query
+claude_messages.append({"role": "user", "content": message})
+```
+
+**Calling Claude**
+
+We call Claude with:
+
+- The conversation history
+- The available MCP tools
+- Other parameters like model and max_tokens
+
+```python
+# Initial Claude API call
+response = self.anthropic.messages.create(
+ model="claude-3-5-sonnet-20241022",
+ max_tokens=1000,
+ messages=claude_messages,
+ tools=self.tools
+)
+```
+
+**Processing Claude's Response**
+
+Claude's response can contain either text content or a request to use a tool. We will handle both the cases.
+
+```python
+for content in response.content:
+ if content.type == 'text':
+ # Add main text response - ensure content.text is a string
+ text_content = content.text
+ if isinstance(text_content, list):
+ # If it's a list, join it into a single string
+ text_content = " ".join(str(item) for item in text_content)
+
+ result_messages.append(ChatMessage(
+ role="assistant",
+ content=text_content
+ ))
+ elif content.type == 'tool_use':
+ tool_name = content.name
+ tool_args = content.input
+
+ # Create a thought message for the tool call
+ result_messages.append(ChatMessage(
+ role="assistant",
+ content=f"I'll use the {tool_name} tool to help answer your question.",
+ metadata={
+ "title": f"🛠️ Using tool: {tool_name}",
+ "log": f"Parameters: {tool_args}",
+ "status": "done",
+ "id": f"tool_call_{tool_name}"
+ }
+ ))
+
+ # Add formatted tool parameters for better readability
+ formatted_args = "```json\n" + json.dumps(tool_args, indent=2) + "\n```"
+ result_messages.append(ChatMessage(
+ role="assistant",
+ content=formatted_args,
+ metadata={
+ "parent_id": f"tool_call_{tool_name}",
+ "id": f"params_{tool_name}",
+ "title": "Tool Parameters"
+ }
+ ))
+
+ # Execute tool call
+ result = await self.session.call_tool(tool_name, tool_args)
+
+ # Get result content and ensure it's a string
+ result_content = result.content
+ if isinstance(result_content, list):
+ result_content = "\n".join(str(item) for item in result_content)
+
+ # Update with tool results
+ result_messages.append(ChatMessage(
+ role="assistant",
+ content="Here are the results from the tool:",
+ metadata={
+ "title": f"📊 Tool Result for {tool_name}",
+ "status": "done",
+ "id": f"result_{tool_name}"
+ }
+ ))
+
+ # Add formatted result content
+ formatted_result = "```\n" + str(result_content) + "\n```"
+ result_messages.append(ChatMessage(
+ role="assistant",
+ content=formatted_result,
+ metadata={
+ "parent_id": f"result_{tool_name}",
+ "id": f"raw_result_{tool_name}",
+ "title": "Raw Output"
+ }
+ ))
+
+ # Continue conversation with tool results
+ # Build a simple message for Claude to continue
+ tool_response_for_claude = {"role": "user", "content": f"Tool result for {tool_name}: {str(result_content)}"}
+ claude_messages.append(tool_response_for_claude)
+
+ # Get next response from Claude with the tool results
+ next_response = self.anthropic.messages.create(
+ model="claude-3-5-sonnet-20241022",
+ max_tokens=1000,
+ messages=claude_messages,
+ )
+
+ # Add Claude's interpretation of the tool results
+ if next_response.content and next_response.content[0].type == 'text':
+ final_text = next_response.content[0].text
+ # Ensure final_text is a string
+ if isinstance(final_text, list):
+ final_text = " ".join(str(item) for item in final_text)
+
+ result_messages.append(ChatMessage(
+ role="assistant",
+ content=final_text
+ ))
+```
+
+- We add Claude's text response to our conversation.
+- Display a message that Claude is using a tool
+- Show the tool parameters in a formatted way
+- Actually executes the tool call, and displays the tool results
+- Sends the results back to Claude for interpretation
+- Shows Claude's final response after seeing the tool results
+
+You'll note that we are using the ChatMessage format we learned in Unit 3 to visualize the Tool Usage:
+- When Claude decides to use a tool, we show a message with the tool name and parameters
+- We use the metadata field to create a collapsible section with the tool call details
+- The tool results are displayed in a nested format under the tool call
+Claude's final interpretation of the results is shown as a regular message
+
+
+Now let's create the Gradio interface:
+
+### Gradio Interface
+
+```python
+# Define the Gradio interface
+def gradio_interface():
+ with gr.Blocks(title="MCP Weather Client") as demo:
+ gr.Markdown("# MCP Weather Assistant")
+ gr.Markdown("Connect to your MCP weather server and chat with the assistant")
+
+ with gr.Row():
+ with gr.Column(scale=4):
+ server_path = gr.Textbox(
+ label="Server Script Path",
+ placeholder="Enter path to server script (e.g., weather.py)",
+ value="weather.py"
+ )
+ with gr.Column(scale=1):
+ connect_btn = gr.Button("Connect")
+
+ status = gr.Textbox(label="Connection Status", interactive=False)
+
+ # Use type="messages" for the enhanced chatbot
+ chatbot = gr.Chatbot(
+ value=[],
+ height=500,
+ type="messages", # Use the messages format
+ show_copy_button=True,
+ avatar_images=("👤", "🤖") # User and assistant avatars
+ )
+
+ with gr.Row():
+ msg = gr.Textbox(
+ label="Your Question",
+ placeholder="Ask about weather or alerts (e.g., What's the weather in New York?)",
+ scale=4
+ )
+ clear_btn = gr.Button("Clear Chat", scale=1)
+
+ # Set up event handlers
+ connect_btn.click(client.connect, inputs=server_path, outputs=status)
+
+ # Process the message and update chatbot
+ def bot_message(message, history):
+ """Process the user message and return updated history"""
+ if not message:
+ return history
+ return client.process_message(message, history)
+
+ # Set up the message flow
+ msg.submit(bot_message, [msg, chatbot], [chatbot]).then(
+ lambda: "", None, [msg]
+ )
+
+ clear_btn.click(lambda: [], None, chatbot)
+
+ return demo
+```
+
+This function creates the Gradio interface with following features:
+
+- A connection section with a textbox for the MCP server path and a connect button
+- A status display to show that the connection with MCP server is live
+- A chatbot component that shows the conversation. A message input and clear button.
+
+The event handlers connect user actions to our MCPClientWrapper functions:
+
+- The connect button calls `client.connect()`
+- The message input calls `bot_message()` which processes the message through our client
+
+### Main Execution
+
+This section:
+
+- Checks if the Anthropic API key is available
+- Creates and launches the Gradio interface
+
+```python
+if __name__ == "__main__":
+ # Check for ANTHROPIC_API_KEY
+ if not os.getenv("ANTHROPIC_API_KEY"):
+ print("Warning: ANTHROPIC_API_KEY not found in environment. Please set it in your .env file.")
+
+ # Launch the Gradio interface
+ interface = gradio_interface()
+ interface.launch(debug=True)
+```
+
+## How It All Comes Together
+
+- When you run the `app.py` file , it creates a Gradio chat interface that uses locally hosted MCP Server and itself works as a MCP Client
+- The Chatbot Client interface establishes a connection to the MCP server and lists the available tools (`get_alerts` and `get_forecasts`)
+- As soon as you send a message in the chat, Claude receives it and decides whether to use any tools
+- If Claude uses tools, they're executed and the results are shown. Claude provides a final response based on the tool results. The entire conversation is displayed in the chat interface.
+
+The Gradio implementation is really powerful as it shows:
+
+- The LLM (Claude) deciding on using tools based on user queries along with their arguments and returned values.
+- The conversation flowing naturally with tool usage shown transparently to the user.
+
+This UI can be extended to many types of tools and use cases, making gradio a flexible choice for building AI assistants that can interact with external systems through MCP Server and MCP Client.
+
+## Building Your Own MCP Servers
+While we've used a weather server in this example, you can create MCP servers for many types of tools:
+
+- **Database Access**: Connect to your databases without exposing credentials
+- **API Integration**: Access third-party APIs like other Gradio apps through Python/JS clients, GitHub, Jira, Slack, _etc._
+- **File Operations**: Work with files on your local system
+- **Custom Business Logic**: Implement domain-specific tools
+
+To build with your own MCP server, follow these steps:
+
+- Define your tool functions (e.g., database queries, API calls)
+- Create an MCP server that exposes these functions
+- Connect your Gradio app to your MCP server as shown in the example above
+- Let Claude or any other suitable LLM use your tools
+- Display the tool calling through the Gradio interface
+
+You can find more information on creating MCP servers in the [MCP documentation](https://modelcontextprotocol.io/introduction).
\ No newline at end of file
diff --git a/units/en/unit5/5_Advanced_Agentic_UI_features.mdx b/units/en/unit5/5_Advanced_Agentic_UI_features.mdx
new file mode 100644
index 000000000..48f8bd9b7
--- /dev/null
+++ b/units/en/unit5/5_Advanced_Agentic_UI_features.mdx
@@ -0,0 +1,540 @@
+# Building Advanced Agentic UI with smolagents
+
+In previous units, we explored creating basic and enhanced Agent interfaces with Gradio. Now, let's build a fully-featured Agentic UI using the smolagents framework, which provides a powerful way to visualize agent reasoning, track tool usage, and create interactive interfaces.
+
+## Introduction to smolagents UI
+
+The smolagents library comes with a built-in Gradio UI that makes it easy to visualize your agent's reasoning process. This interface shows:
+
+- The agent's thought process as it reasons through problems
+- Tool usage and execution logs
+- Final answers and outputs
+- Images and other media generated by the agent
+
+Let's start by looking at how easy it is to launch the default UI:
+
+```python
+from smolagents import (
+ load_tool,
+ CodeAgent,
+ HfApiModel,
+ GradioUI
+)
+
+# Import tool from Hub
+image_generation_tool = load_tool("m-ric/text-to-image", trust_remote_code=True)
+
+# Initialize a model
+model = HfApiModel(model_id="mistralai/Mistral-7B-Instruct-v0.2")
+
+# Initialize the agent with the image generation tool
+agent = CodeAgent(tools=[image_generation_tool], model=model)
+
+# Launch the Gradio UI
+GradioUI(agent).launch(share=True)
+```
+
+This simple code creates a complete interface:
+
+
+
+## Understanding the smolagents UI Architecture
+
+Now let's dive deeper into how the smolagents UI works. The main component is the `GradioUI` class, which wraps around a smolagents agent and creates a Gradio interface for it. You can find the full code [here](https://github.com/huggingface/smolagents/blob/main/src/smolagents/gradio_ui.py).
+
+Let's examine the key components of this UI:
+
+### 1. The GradioUI Class
+
+The `GradioUI` class is responsible for creating and launching the Gradio interface. It takes a smolagents agent as input and provides methods for interacting with the agent and handling file uploads.
+
+```python
+class GradioUI:
+ """A one-line interface to launch your agent in Gradio"""
+
+ def __init__(self, agent: MultiStepAgent, file_upload_folder: str | None = None):
+ if not _is_package_available("gradio"):
+ raise ModuleNotFoundError(
+ "Please install 'gradio' extra to use the GradioUI: `pip install 'smolagents[gradio]'`"
+ )
+ self.agent = agent
+ self.file_upload_folder = file_upload_folder
+ self.name = getattr(agent, "name") or "Agent interface"
+ self.description = getattr(agent, "description", None)
+ if self.file_upload_folder is not None:
+ if not os.path.exists(file_upload_folder):
+ os.mkdir(file_upload_folder)
+```
+
+The `create_app` method builds the Gradio interface with a sidebar, chat area, and text input component:
+
+```python
+def create_app(self):
+ import gradio as gr
+
+ with gr.Blocks(theme="ocean", fill_height=True) as demo:
+ # Add session state to store session-specific data
+ session_state = gr.State({})
+ stored_messages = gr.State([])
+ file_uploads_log = gr.State([])
+
+ with gr.Sidebar():
+ # Rest of the Sidebar content here...
+ text_input = gr.Textbox(
+ lines=3,
+ label="Chat Message",
+ container=False,
+ placeholder="Enter your prompt here and press Shift+Enter or press the button",
+ )
+
+ # Main chat interface
+ chatbot = gr.Chatbot(
+ label="Agent",
+ type="messages",
+ avatar_images=(
+ None,
+ "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/smolagents/mascot_smol.png",
+ ),
+ resizeable=True,
+ scale=1,
+ )
+
+ # Set up event handlers...
+
+ return demo
+```
+
+### 2. Using the Sidebar Component
+
+One of the key features of the smolagents UI is the use of the Gradio Sidebar component for user inputs and controls. The sidebar provides a clean separation between the input controls and the chat display.
+
+```python
+with gr.Sidebar():
+ gr.Markdown(
+ f"# {self.name.replace('_', ' ').capitalize()}"
+ "\n> This web ui allows you to interact with a `smolagents` agent that can use tools and execute steps to complete tasks."
+ + (f"\n\n**Agent description:**\n{self.description}" if self.description else "")
+ )
+
+ with gr.Group():
+ gr.Markdown("**Your request**", container=True)
+ text_input = gr.Textbox(
+ lines=3,
+ label="Chat Message",
+ container=False,
+ placeholder="Enter your prompt here and press Shift+Enter or press the button",
+ )
+ submit_btn = gr.Button("Submit", variant="primary")
+
+ # File upload controls if enabled...
+```
+
+The sidebar component is particularly useful for agent interfaces as it:
+- Keeps the input controls always visible, even when the chat history gets long
+- Provides a clear separation between inputs and outputs
+- Creates a more intuitive interface for users familiar with chat applications like ChatGPT and Claude.
+
+### 3. Visualizing Agent Thoughts and Tool Usage
+
+The heart of the smolagents UI is the `pull_messages_from_step` function, which converts agent steps into Gradio ChatMessage objects. This function handles the complex task of displaying different types of agent steps (actions, planning, final answers) in a visually appealing way.
+
+Let's take a closer look at how it processes action steps:
+
+```python
+if isinstance(step_log, ActionStep):
+ # Output the step number
+ step_number = f"Step {step_log.step_number}" if step_log.step_number is not None else "Step"
+ yield gr.ChatMessage(role="assistant", content=f"**{step_number}**")
+
+ # First yield the thought/reasoning from the LLM
+ if hasattr(step_log, "model_output") and step_log.model_output is not None:
+ # Clean up the LLM output
+ model_output = step_log.model_output.strip()
+ # Remove any trailing and extra backticks
+ model_output = re.sub(r"```\s*", "```", model_output)
+ model_output = re.sub(r"\s*```", "```", model_output)
+ model_output = re.sub(r"```\s*\n\s*", "```", model_output)
+ model_output = model_output.strip()
+ yield gr.ChatMessage(role="assistant", content=model_output)
+
+ # For tool calls, create a parent message
+ if hasattr(step_log, "tool_calls") and step_log.tool_calls is not None:
+ first_tool_call = step_log.tool_calls[0]
+ used_code = first_tool_call.name == "python_interpreter"
+ parent_id = f"call_{len(step_log.tool_calls)}"
+
+ # Process tool arguments and yield a message with tool usage info
+ # ...
+
+ parent_message_tool = gr.ChatMessage(
+ role="assistant",
+ content=content,
+ metadata={
+ "title": f"🛠️ Used tool {first_tool_call.name}",
+ "id": parent_id,
+ "status": "done",
+ },
+ )
+ yield parent_message_tool
+```
+
+This function uses several advanced features of `gr.ChatMessage` that we learned about in Unit 3:
+
+1. **Metadata for Thought Bubbles**: It uses the `metadata` parameter with a `title` key to create collapsible sections for tool usage.
+
+2. **Status Indicators**: The `status` key is set to "done" when a tool execution is complete.
+
+3. **Nested Messages**: It creates parent-child relationships between messages using the `id` and `parent_id` keys.
+
+For example, here's how execution logs are displayed as a child of the tool call:
+
+```python
+# Display execution logs if they exist
+if hasattr(step_log, "observations") and (
+ step_log.observations is not None and step_log.observations.strip()
+): # Only yield execution logs if there's actual content
+ log_content = step_log.observations.strip()
+ if log_content:
+ log_content = re.sub(r"^Execution logs:\s*", "", log_content)
+ yield gr.ChatMessage(
+ role="assistant",
+ content=f"```bash\n{log_content}\n",
+ metadata={"title": "📝 Execution Logs", "status": "done"},
+ )
+```
+
+### 4. Streaming Agent Responses
+
+The `stream_to_gradio` function is responsible for running the agent and streaming its outputs as Gradio ChatMessages:
+
+```python
+def stream_to_gradio(
+ agent,
+ task: str,
+ task_images: list | None = None,
+ reset_agent_memory: bool = False,
+ additional_args: Optional[dict] = None,
+):
+ """Runs an agent with the given task and streams the messages from the agent as gradio ChatMessages."""
+ total_input_tokens = 0
+ total_output_tokens = 0
+
+ for step_log in agent.run(
+ task, images=task_images, stream=True, reset=reset_agent_memory, additional_args=additional_args
+ ):
+ # Track tokens if model provides them
+ if getattr(agent.model, "last_input_token_count", None) is not None:
+ total_input_tokens += agent.model.last_input_token_count
+ total_output_tokens += agent.model.last_output_token_count
+ if isinstance(step_log, (ActionStep, PlanningStep)):
+ step_log.input_token_count = agent.model.last_input_token_count
+ step_log.output_token_count = agent.model.last_output_token_count
+
+ for message in pull_messages_from_step(step_log):
+ yield message
+```
+
+This function:
+1. Runs the agent with the user's task
+2. Tracks token usage
+3. Processes each step log through `pull_messages_from_step`
+4. Yields ChatMessage objects to the Gradio interface
+
+The streaming approach provides a responsive experience, showing the agent's reasoning in real-time rather than making the user wait for the complete result.
+
+## Enhancing the smolagents UI
+
+Now let's add some enhancements to the default smolagents UI to make it even more powerful.
+
+### Adding File Upload Capabilities
+
+One important feature for agent interfaces is the ability to upload files for the agent to process. The smolagents framework already supports this, but let's look at how we can implement file uploads in the gradio UI using Multimodal Textbox.
+
+First let's use the Multimodal Textbox instead of a simple textbox:
+
+```python
+text_input = gr.MultimodalTextbox(
+ interactive=True,
+ file_count="multiple",
+ show_label=False,
+ sources=["upload"],
+ file_types=[".csv", "image"],
+ placeholder="Enter your prompt here and press Shift+Enter or press the button"
+)
+```
+
+Update the event handlers accordingly:
+```python
+# Updated event handlers for multimodal input
+text_input.submit(
+ self.log_user_message,
+ [text_input, file_uploads_log],
+ [stored_messages, text_input, submit_btn],
+).then(self.interact_with_agent, [stored_messages, chatbot, session_state], [chatbot]).then(
+ lambda: (
+ gr.MultimodalTextbox(
+ interactive=True,
+ file_count="multiple",
+ show_label=False,
+ sources=["upload"],
+ file_types=[".csv", "image"],
+ placeholder="Enter your prompt here and press Shift+Enter or press the button"
+ ),
+ gr.Button(interactive=True),
+ ),
+ None,
+ [text_input, submit_btn],
+)
+
+submit_btn.click(
+ self.log_user_message,
+ [text_input, file_uploads_log],
+ [stored_messages, text_input, submit_btn],
+).then(self.interact_with_agent, [stored_messages, chatbot, session_state], [chatbot]).then(
+ lambda: (
+ gr.MultimodalTextbox(
+ interactive=True,
+ file_count="multiple",
+ show_label=False,
+ sources=["upload"],
+ file_types=[".csv", "image"],
+ placeholder="Enter your prompt here and press Shift+Enter or press the button"
+ ),
+ gr.Button(interactive=True),
+ ),
+ None,
+ [text_input, submit_btn],
+)
+```
+
+Modify the `log_user_message` method to handle multimodal input:
+
+```python
+def log_user_message(self, multimodal_input, file_uploads_log):
+ """
+ Handle the multimodal input from the user
+ multimodal_input will be a dict with 'text' and 'files' keys
+ """
+ import gradio as gr
+
+ # Extract text and files from multimodal input
+ text = multimodal_input.get("text", "")
+ files = multimodal_input.get("files", [])
+
+ # Build the message
+ messages = []
+
+ # Add file paths first
+ for file_path in files:
+ # File paths come directly in the files array
+ messages.append({"role": "user", "content": {"path": file_path}})
+ # Add to file uploads log if not already present
+ if file_path not in file_uploads_log:
+ file_uploads_log.append(file_path)
+
+ # Add text message if present
+ if text:
+ messages.append({"role": "user", "content": text})
+
+ # Add any previously uploaded files through the separate file uploader
+ if len(file_uploads_log) > 0:
+ text += f"\nPreviously uploaded files: {file_uploads_log}"
+
+ return messages, {"text": "", "files": []}, gr.Button(interactive=False)
+```
+
+- The method now accepts multimodal input which is a dictionary with 'text' and 'files' keys.
+- Processes both text and attached files
+- Combines file information into the message
+
+Now when a user submits a prompt, the file paths are appended to the prompt so the agent knows about them.
+
+We also need to update the `interact_with_agent` method so that it can handle user prompts with file inputs robustly:
+
+```python
+def interact_with_agent(self, messages, chatbot_history, session_state):
+ import gradio as gr
+
+ # Get the agent type from the template agent
+ if "agent" not in session_state:
+ session_state["agent"] = self.agent
+
+ try:
+ # Add all messages to chatbot history
+ for message in messages:
+ chatbot_history.append(message)
+ yield chatbot_history
+
+ # Extract text and files for the agent
+ text_messages = [msg["content"] for msg in messages if isinstance(msg["content"], str)]
+ file_messages = [msg["content"]["path"] for msg in messages if isinstance(msg["content"], dict)]
+
+ # Combine text messages and file information for the agent
+ task = " ".join(text_messages)
+ if file_messages:
+ task += f"\nAttached files: {', '.join(file_messages)}"
+
+ for msg in stream_to_gradio(session_state["agent"], task=task, reset_agent_memory=False):
+ chatbot_history.append(msg)
+ yield chatbot_history
+
+ yield chatbot_history
+ except Exception as e:
+ print(f"Error in interaction: {str(e)}")
+ chatbot_history.append(gr.ChatMessage(role="assistant", content=f"Error: {str(e)}"))
+ yield chatbot_history
+```
+
+You can find the full updated gradio UI file [here](https://github.com/yvrjsharma/HugginFace_Gradio/blob/main/updated_smolagents_gradio_ui.py).
+
+### Creating a Multipage Application
+
+For more complex agent interfaces, we can extend our application with multiple pages. This allows us to include documentation, different agent configurations, or specialized interfaces for different tasks.
+
+Here's how we can create a multipage application with our smolagents UI:
+
+```python
+import gradio as gr
+from smolagents import CodeAgent, HfApiModel, GradioUI, load_tool, DuckDuckGoSearchTool
+
+# Initialize agents
+search_agent = CodeAgent(
+ tools=[DuckDuckGoSearchTool()],
+ model=HfApiModel(model_id="mistralai/Mistral-7B-Instruct-v0.2"),
+)
+
+# Import tool from Hub
+# Initialize the agent with the image generation tool
+image_agent = CodeAgent(
+ tools=[load_tool("m-ric/text-to-image", trust_remote_code=True)],
+ model=HfApiModel(model_id="mistralai/Mistral-7B-Instruct-v0.2")
+)
+
+# Create UI instances
+search_ui = GradioUI(search_agent)
+image_ui = GradioUI(image_agent)
+
+# Build multipage app
+with gr.Blocks(theme="ocean") as demo:
+ gr.Markdown("# Multiple-Page Agent Interface")
+
+ # Main page
+ search_ui.create_app()
+
+# Add additional pages
+with demo.route("Image Generation", "/image") as image_page:
+ gr.Markdown("# Image Generation Agent")
+ image_ui.create_app()
+
+# Documentation page
+with demo.route("Documentation", "/docs") as docs_page:
+ gr.Markdown("# Agent Documentation")
+ gr.Markdown("""
+ ## How to use this interface
+
+ - **Search Agent**: Use the main page for your web search queries
+ - **Image Generation**: Use the image page to create images from text descriptions
+ - **Tips**: Be specific in your requests for best results
+ """)
+
+demo.launch()
+```
+
+This creates a **multipage** application with:
+- A Main page with the Search Agent
+- An Image Generation page with the Image Agent
+- A Documentation page with helpful information
+
+Each page has its own URL path, and Gradio automatically creates a navigation bar at the top of the interface. You can read more about creating multipage apps with Gradio [here](https://www.gradio.app/guides/multipage-apps).
+
+
+### Adding Deep Links
+
+You can add a button to your Gradio app that creates a unique URL you can use to share your app and all components as they currently are with others.
+
+- This is useful for sharing unique and interesting generations from your agentic application, or
+- For saving a snapshot of your app at a particular point in time.
+
+It is very simple to add a deep link button to your app, you just need to place the `gr.DeepLinkButton` component anywhere in your app.
+
+Note that, for the URL to be accessible to others, your app must be available at a public URL. So be sure to host your agentic app on platforms like Hugging Face Spaces or use the `share=True` parameter when launching your agentic app.
+
+Let's see an example of how this works:
+
+```python
+import gradio as gr
+from smolagents import CodeAgent, HfApiModel, GradioUI, load_tool
+
+# Import tool from Hub
+# Initialize the agent with the image generation tool
+image_agent = CodeAgent(
+ tools=[load_tool("m-ric/text-to-image", trust_remote_code=True)],
+ model=HfApiModel(model_id="mistralai/Mistral-7B-Instruct-v0.2")
+)
+
+# Create UI instance
+image_ui = GradioUI(image_agent)
+
+# Build another gradio UI
+with gr.Blocks(theme="citrus") as demo:
+ gr.Markdown("# Share your Agent Interfaces with Deeplinks")
+ # how to
+ gr.Markdown("""
+ ### How to use this interface
+
+ - Click the DeepLinks button
+ - It copies a unique URL that you can use to share your app and all components as they currently are with others
+ - Useful for sharing unique and interesting generations from your agentic applications!
+ """)
+ # render agent ui
+ image_ui.create_app()
+ gr.DeepLinkButton()
+
+demo.launch()
+```
+
+
+## Styling and Theming Your Agent UI
+
+The smolagents UI uses Gradio's theming system to create an appealing interface. By default, it uses the "ocean" theme, but you can customize this to match your branding or preferences.
+
+To change the theme, modify the `gr.Blocks` constructor:
+
+```python
+with gr.Blocks(theme=gr.themes.Soft(), fill_height=True) as demo:
+ # UI components here...
+```
+
+Gradio provides several built-in themes:
+- `gr.themes.Base()` - The default theme
+- `gr.themes.Soft()` - A softer color palette
+- `gr.themes.Monochrome()` - A black and white theme
+- `gr.themes.Glass()` - A transparent theme
+
+You can also create custom themes by extending the `gr.Theme` class:
+
+```python
+custom_theme = gr.themes.Soft().set(
+ body_background_fill="linear-gradient(to right, #4880EC, #019CAD)",
+ body_background_fill_dark="linear-gradient(to right, #1e3c72, #2a5298)",
+ button_primary_background_fill="#019CAD",
+ button_primary_background_fill_dark="#2a5298",
+)
+
+with gr.Blocks(theme=custom_theme) as demo:
+ # UI components here...
+```
+Read more in our Theming Guide, [here](https://www.gradio.app/guides/theming-guide).
+
+## Summary
+
+In this unit, we've learned how to:
+
+1. Use the smolagents library's built-in GradioUI to visualize agent thinking and tool usage
+2. Understand the architecture of the smolagents UI, particularly the `pull_messages_from_step` function
+3. Enhance the UI with file upload capabilities
+4. Create multipage applications using different agents with clear navigation routes
+5. Apply custom themes to improve the appearance
+6. Use Deep Links for sharing snapshot of your Agent's results and state
+
+The smolagents UI demonstrates the power of Gradio for creating agent interfaces that are both highly functional and user-friendly. By visualizing the agent's thought process and tool usage, it helps users understand what the agent is doing and build trust in its capabilities.
\ No newline at end of file
diff --git a/units/en/unit5/6_Conclusion.mdx b/units/en/unit5/6_Conclusion.mdx
new file mode 100644
index 000000000..b1bb64826
--- /dev/null
+++ b/units/en/unit5/6_Conclusion.mdx
@@ -0,0 +1,28 @@
+# Conclusion
+
+**Congratulations** on completing the Gradio for AI Agents module! 🥳🎉
+
+Throughout these five units, you've learned how to create powerful and intuitive user interfaces for your AI Agents. You started with simple chat interfaces and progressed to complex, multi-agent systems with advanced visualization capabilities.
+
+Let's recap what you've accomplished:
+
+- In Unit 1, you learned the fundamentals of Gradio and why it's perfect for building Agent UIs
+- In Unit 2, you created your first Agent interface using ChatInterface and saw how to display an Agent's thought process
+- In Unit 3, you mastered advanced visualization techniques with ChatMessage for showing tool usage and nested reasoning
+- In Unit 4, you integrated the Model Context Protocol (MCP) to connect your interface with external tools
+- In Unit 5, you built a complete agentic UI using smolagents with file uploads, Sidebars, Deeplinks and Multiple pages
+
+These skills give you everything you need to build professional-grade interfaces for your AI Agents.
+
+With Gradio, you can transform any AI Agent from a command-line tool into a fully interactive application that can be shared with users of all technical backgrounds.
+
+## What's Next?
+Now that you have these Gradio skills, consider exploring:
+- Deploying your Agent UIs to Hugging Face Spaces for wider access
+- Building specialized interfaces for domain-specific Agents
+- Combining multiple Agents in a single interface
+
+
+Finally, we would love **to hear what you think of the course and how we can improve it**. If you have some feedback then, please 👉 [fill this form](https://docs.google.com/forms/d/e/1FAIpQLSe9VaONn0eglax0uTwi29rIn4tM7H2sYmmybmG5jJNlE5v0xA/viewform?usp=dialog)
+
+**Keep building, keep learning, and stay awesome!** 🤗
\ No newline at end of file
diff --git a/units/en/unit5/README.md b/units/en/unit5/README.md
new file mode 100644
index 000000000..362073837
--- /dev/null
+++ b/units/en/unit5/README.md
@@ -0,0 +1,42 @@
+# Bonus Module: Building User Interfaces for AI Agents with Gradio
+
+## Chapters
+- Introduction to Gradio for AI Agents
+ - Why Gradio for Agent UIs?
+ - Understanding the role of UI in Agent interactions
+ - Setting up Gradio
+
+- Building Your First Agent Interface
+ - Introduction to gr.ChatInterface
+ - Basic Agent chat UI implementation
+ - Adding more features to the chat UI
+
+
+- Enhancing Agent Interactions with ChatMessage
+ - Understanding gr.ChatMessage format
+ - Displaying Agent thoughts and reasoning
+ - Tool usage visualization
+
+
+- Advanced Agent UI Features
+ - Adding file upload capabilities
+ - Customizing with Sidebar and Multipage
+ - Styling and theming your Agent UI
+ - Showing visible thought process of agents
+
+- Hands-on Project: Building a Complete Agent UI
+ - Implementing an Agentic UI : smolagents
+ - Adding tool usage visualization
+ - Customizing the interface
+ - Implementing an Agentic UI : langchain
+ - Adding tool usage visualization
+ - Customizing the interface
+ - Implementing an Agentic UI : llamaindex
+ - Adding tool usage visualization
+ - Customizing the interface
+ - Bonus Module Final Quiz
+
+- Conclusion and Next Steps
+ - Best gradio-practices
+ - Additional resources
+