# How to Extend the Functionality of AI Agents for Beginners: A Complete Guide

> Extend AI Agents for Beginners functionality by adding custom tools, middleware, memory providers, and multi-agent workflows with this complete guide.

- Repository: [Microsoft/ai-agents-for-beginners](https://github.com/microsoft/ai-agents-for-beginners)
- Tags: how-to-guide
- Published: 2026-04-22

---

**You can extend the functionality of AI Agents for Beginners by adding custom tools with `@ai_function`, implementing middleware for request/response processing, integrating memory providers for persistent state, and building multi-agent workflows using `WorkflowBuilder`.**

This article walks you through the exact architecture and code patterns used in the Microsoft AI Agents for Beginners repository. Whether you want to add sentiment analysis, database-backed memory, or complex business workflows, you'll learn how to extend the codebase while maintaining its pedagogical structure.

---

## Understanding the Extension Architecture

The AI Agents for Beginners repository is built around the **Microsoft Agent Framework (MAF)**, a Python library that provides structured components for building AI agents. Each lesson in the repository demonstrates specific capabilities through self-contained examples.

### Core Extension Points

| Component | Purpose | Extension Method |
|-----------|---------|------------------|
| **Tools** | LLM-callable functions | Add `@ai_function` decorated functions |
| **Middleware** | Pre/post-processing hooks | Implement `@executor` decorated functions |
| **Memory providers** | Persistent state across turns | Configure `ChatMessageStore` or custom providers |
| **Workflows** | Multi-agent orchestration | Use `WorkflowBuilder` with conditional edges |
| **Agents** | Encapsulated LLM + tools | Create new `AgentExecutor` instances |

These components are demonstrated in [`14-microsoft-agent-framework/code-samples/hotel_booking_workflow_sample.py`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/14-microsoft-agent-framework/code-samples/hotel_booking_workflow_sample.py), which serves as the reference implementation for all extensions.

---

## Step 1: Creating a New Lesson Module

To extend the functionality of AI Agents for Beginners, start by creating a properly structured lesson folder that matches the repository's pedagogical pattern.

### Folder Structure

```bash
mkdir 16-sentiment-analysis
mkdir 16-sentiment-analysis/code_samples
mkdir 16-sentiment-analysis/images

```

### Required Files

| File | Purpose |
|------|---------|
| [`README.md`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/README.md) | Concept explanation and learning objectives |
| `code_samples/16-sentiment-python-agent-framework.ipynb` | Interactive Jupyter notebook |
| [`code_samples/sentiment_demo.py`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/code_samples/sentiment_demo.py) | Standalone verification script |
| `images/` | Workflow diagrams and architecture visuals |

Update the top-level [`README.md`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/README.md) or [`AGENTS.md`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/AGENTS.md) to include your new lesson in the course index. The repository's GitHub Actions workflow ([`.github/workflows/co-op-translator.yml`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/.github/workflows/co-op-translator.yml)) will automatically translate your new lesson into 50+ languages once merged.

---

## Step 2: Building Custom Tools with @ai_function

Tools are the primary extension mechanism for adding capabilities to agents. The `@ai_function` decorator from the Microsoft Agent Framework converts Python functions into LLM-callable tools.

### Basic Tool Pattern

```python
from typing import Annotated
from agent_framework import ai_function
import json

@ai_function(description="Detects sentiment of user message.")
def analyze_sentiment(
    message: Annotated[str, "User message to analyze"]
) -> str:
    """Simple keyword-based sentiment analysis."""
    lower = message.lower()
    if any(word in lower for word in ["love", "great", "awesome"]):
        result = {"sentiment": "positive", "confidence": 0.95}
    elif any(word in lower for word in ["hate", "terrible", "bad"]):
        result = {"sentiment": "negative", "confidence": 0.93}
    else:
        result = {"sentiment": "neutral", "confidence": 0.80}
    
    return json.dumps(result)

```

### Advanced Tool Features

- **Structured outputs**: Return JSON strings that match Pydantic models
- **Async support**: Define `async def` functions for I/O-bound operations
- **Error handling**: Raise exceptions that the agent framework converts to tool error messages

Tools are registered with agents through the `tools` parameter in `chat_client.create_agent()`.

---

## Step 3: Creating Agents with Custom Configuration

Agents in the Microsoft Agent Framework combine an LLM client, system instructions, tools, and output schemas into executable units.

### Agent Creation Pattern

```python
from agent_framework import AgentExecutor
from agent_framework.openai import OpenAIChatClient
from pydantic import BaseModel

# Define structured output models

class SentimentResult(BaseModel):
    sentiment: str  # "positive", "neutral", "negative"

    confidence: float

class ReplyResult(BaseModel):
    reply: str

# Initialize LLM client

chat_client = OpenAIChatClient(model_id="gpt-4o")

# Create sentiment analysis agent

sentiment_agent = AgentExecutor(
    chat_client.create_agent(
        instructions=(
            "You are a sentiment analyzer. "
            "Only call the analyze_sentiment tool and return its JSON output. "
            "Do not add conversational text."
        ),
        tools=[analyze_sentiment],  # From Step 2

        response_format=SentimentResult,
    ),
    id="sentiment_agent",
)

# Create response generation agent

reply_agent = AgentExecutor(
    chat_client.create_agent(
        instructions=(
            "You are a helpful assistant. "
            "Use the detected sentiment to adapt your tone: "
            "be enthusiastic for positive, empathetic for negative, "
            "and professional for neutral. "
            "Return a short reply in JSON format."
        ),
        response_format=ReplyResult,
    ),
    id="reply_agent",
)

```

Key configuration options include:
- **`instructions`**: System prompt defining agent behavior
- **`tools`**: List of `@ai_function` decorated tools
- **`response_format`**: Pydantic model for structured outputs
- **`temperature`/`max_tokens`**: Generation parameters (passed to client)

---

## Step 4: Implementing Middleware for Cross-Cutting Concerns

Middleware provides hooks for logic that runs before or after tool calls and LLM invocations. The `@executor` decorator and `FunctionInvocationContext` enable extensions for logging, authentication, rate limiting, and observability.

### Middleware Patterns

```python
from agent_framework import executor, FunctionInvocationContext
from agent_framework.types import AgentExecutorResponse
import time

# Pre/post tool call middleware

@executor(id="timing_middleware")
async def log_execution_time(ctx: FunctionInvocationContext) -> None:
    """Logs duration of function invocations."""
    start = time.time()
    await ctx.next()  # Proceed to actual function

    duration = time.time() - start
    print(f"[Timing] {ctx.function_name} took {duration:.3f}s")

# Post-processing middleware for responses

@executor(id="audit_middleware")
async def audit_response(ctx: FunctionInvocationContext) -> None:
    """Captures agent outputs for compliance auditing."""
    await ctx.next()
    if isinstance(ctx.result, AgentExecutorResponse):
        # Log to external system

        print(f"[Audit] Agent {ctx.executor_id} output: {ctx.result.agent_run_response.text[:200]}")

```

Middleware is attached to agents by passing `middleware=[timing_middleware, audit_middleware]` to `AgentExecutor` or by using the `FunctionInvocationContext` pattern shown in [`hotel_booking_workflow_sample.py`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/hotel_booking_workflow_sample.py) lines 83-96.

---

## Step 5: Adding Memory Providers for Persistent State

Memory providers enable agents to retain information across conversation turns. The Microsoft Agent Framework supports multiple memory backends through `ChatMessageStore` and `context_providers`.

### Memory Configuration Options

| Provider | Use Case | Implementation |
|----------|----------|----------------|
| **In-memory** | Testing, single-session demos | Default `ChatMessageStore` |
| **Mem0** | Personalization, long-term user memory | `Mem0Provider` with user ID |
| **Custom database** | Enterprise persistence, audit requirements | Subclass `ChatMessageStore` |

### Memory Implementation Example

```python
from agent_framework.memory import ChatMessageStore, Mem0Provider

# Simple in-memory store with session persistence

memory_store = ChatMessageStore(max_messages=20)

# Mem0 integration for cross-session memory

mem0_provider = Mem0Provider(
    api_key=os.getenv("MEM0_API_KEY"),
    user_id="user_12345",
)

# Create agent with memory

context_agent = AgentExecutor(
    chat_client.create_agent(
        instructions="You are a helpful assistant with memory of past conversations.",
        tools=[analyze_sentiment],
    ),
    id="context_agent",
    chat_message_store_factory=lambda: memory_store,  # Per-session memory

    context_providers=[mem0_provider],  # Cross-session memory

)

```

Memory providers are passed to `AgentExecutor` via `chat_message_store_factory` (for conversation history) and `context_providers` (for additional context sources).

---

## Step 6: Building Multi-Agent Workflows with WorkflowBuilder

The `WorkflowBuilder` enables complex orchestration patterns including sequential processing, conditional branching, and parallel execution (fan-out/fan-in).

### Workflow Construction Pattern

```python
from agent_framework import WorkflowBuilder, AgentExecutorRequest
from agent_framework.types import ChatMessage, Role

# Conditional routing functions

def is_negative_sentiment(response: Any) -> bool:
    """Route to escalation path for negative feedback."""
    if not isinstance(response, AgentExecutorResponse):
        return False
    try:
        result = SentimentResult.model_validate_json(response.agent_run_response.text)
        return result.sentiment == "negative"
    except Exception:
        return False

def is_positive_or_neutral(response: Any) -> bool:
    return not is_negative_sentiment(response)

# Build workflow

workflow = (
    WorkflowBuilder()
    .set_start_executor(sentiment_agent)  # First: analyze sentiment

    .add_edge(
        sentiment_agent, 
        escalated_reply_agent,  # Special handling for negative

        condition=is_negative_sentiment
    )
    .add_edge(
        sentiment_agent,
        reply_agent,  # Standard handling

        condition=is_positive_or_neutral
    )
    .add_edge(escalated_reply_agent, display)
    .add_edge(reply_agent, display)
    .build()
)

# Execute

async def run_workflow(user_message: str):
    request = AgentExecutorRequest(
        messages=[ChatMessage(Role.USER, text=user_message)],
        should_respond=True,
    )
    return await workflow.run(request)

```

The `WorkflowBuilder` pattern in [`hotel_booking_workflow_sample.py`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/hotel_booking_workflow_sample.py) (lines 68-79) demonstrates:
- **Sequential execution**: Default edge from one executor to next
- **Conditional edges**: `condition` parameter for branching logic
- **Fan-out/Fan-in**: Multiple edges from single executor for parallel processing

---

## Complete Extension Example: Sentiment-Aware Support Bot

Here's a consolidated, runnable example that combines all extension patterns into a new capability you can add to the repository.

```python
#!/usr/bin/env python3
"""
sentiment_support_bot.py
Complete example for extending AI Agents for Beginners with sentiment analysis.
Place in: 16-sentiment-analysis/code_samples/
"""

import os
import json
import asyncio
from typing import Annotated, Any
from dataclasses import dataclass

from agent_framework import (
    AgentExecutor,
    AgentExecutorRequest,
    ChatMessage,
    Role,
    WorkflowBuilder,
    WorkflowContext,
    ai_function,
    executor,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.types import AgentExecutorResponse
from dotenv import load_dotenv
from pydantic import BaseModel

# Load environment variables

load_dotenv()


# ============================================================

# 1. PYDANTIC MODELS (Structured Outputs)

# ============================================================

class SentimentResult(BaseModel):
    """Output from sentiment analysis tool."""
    sentiment: str  # "positive", "neutral", "negative"

    confidence: float
    detected_keywords: list[str]


class ReplyResult(BaseModel):
    """Final support response."""
    reply: str
    escalation_recommended: bool


# ============================================================

# 2. CUSTOM TOOLS (@ai_function)

# ============================================================

@ai_function(description="Analyzes sentiment of customer message with keyword detection.")
def analyze_sentiment(
    message: Annotated[str, "Customer message to analyze"]
) -> str:
    """Production-ready sentiment analysis (replace with Azure AI Language for accuracy)."""
    lower = message.lower()
    
    positive_keywords = ["love", "great", "awesome", "excellent", "thank", "happy"]
    negative_keywords = ["hate", "terrible", "bad", "awful", "frustrated", "angry", "worst"]
    
    detected_positive = [w for w in positive_keywords if w in lower]
    detected_negative = [w for w in negative_keywords if w in lower]
    
    if detected_positive and not detected_negative:
        result = SentimentResult(
            sentiment="positive",
            confidence=0.9 + 0.05 * len(detected_positive),
            detected_keywords=detected_positive
        )
    elif detected_negative:
        result = SentimentResult(
            sentiment="negative",
            confidence=0.85 + 0.05 * len(detected_negative),
            detected_keywords=detected_negative
        )
    else:
        result = SentimentResult(
            sentiment="neutral",
            confidence=0.75,
            detected_keywords=[]
        )
    
    return result.model_dump_json()


@ai_function(description="Looks up customer order by ID.")
def lookup_order(
    order_id: Annotated[str, "Order ID to look up"]
) -> str:
    """Stub for order lookup (replace with database query)."""
    # Simulated database response

    mock_orders = {
        "ORD-12345": {"status": "shipped", "items": ["Widget Pro"], "eta": "2 days"},
        "ORD-67890": {"status": "processing", "items": ["Gadget Max"], "eta": "5 days"},
    }
    return json.dumps(mock_orders.get(order_id, {"error": "Order not found"}))


# ============================================================

# 3. MIDDLEWARE (observability & logging)

# ============================================================

@executor(id="logging_middleware")
async def log_interactions(ctx: WorkflowContext) -> None:
    """Logs all agent interactions for debugging."""
    print(f"\n[LOG] Executing: {ctx.executor_id}")
    print(f"[LOG] Input: {ctx.input_data}")
    
    await ctx.next()  # Continue to actual execution

    
    print(f"[LOG] Output: {ctx.result}")


# ============================================================

# 4. AGENT CREATION

# ============================================================

# Initialize LLM client

chat_client = OpenAIChatClient(model_id="gpt-4o")

# Sentiment analysis agent

sentiment_agent = AgentExecutor(
    chat_client.create_agent(
        instructions=(
            "You are a sentiment analysis specialist. "
            "Use the analyze_sentiment tool to classify customer messages. "
            "Return ONLY the JSON result from the tool."
        ),
        tools=[analyze_sentiment],
        response_format=SentimentResult,
    ),
    id="sentiment_agent",
    # middleware=[logging_middleware]  # Optional

)

# Order lookup agent (for complex queries)

order_agent = AgentExecutor(
    chat_client.create_agent(
        instructions=(
            "You help customers with order inquiries. "
            "Use lookup_order when an order ID is provided. "
            "Otherwise, ask clarifying questions."
        ),
        tools=[lookup_order],
    ),
    id="order_agent",
)

# Response generation agent with tone adaptation

def make_reply_agent(sentiment: str):
    """Factory for tone-specific response agents."""
    tone_instructions = {
        "positive": "Be enthusiastic and celebratory. Use exclamation points!",
        "negative": "Be deeply empathetic. Acknowledge frustration. Offer concrete solutions.",
        "neutral": "Be professional and concise. Stick to facts.",
    }
    
    return AgentExecutor(
        chat_client.create_agent(
            instructions=(
                f"You are a support response specialist. {tone_instructions.get(sentiment, tone_instructions['neutral'])} "
                "Generate a helpful response based on the context provided. "
                "Return JSON with 'reply' and 'escalation_recommended' fields."
            ),
            response_format=ReplyResult,
        ),
        id=f"reply_agent_{sentiment}",
    )


# ============================================================

# 5. WORKFLOW ORCHESTRATION

# ============================================================

def extract_sentiment(response: Any) -> str:
    """Parse sentiment from agent response."""
    if not isinstance(response, AgentExecutorResponse):
        return "neutral"
    try:
        result = SentimentResult.model_validate_json(response.agent_run_response.text)
        return result.sentiment
    except Exception:
        return "neutral"

# Conditional routing functions

def is_positive(response: Any) -> bool:
    return extract_sentiment(response) == "positive"

def is_negative(response: Any) -> bool:
    return extract_sentiment(response) == "negative"

def is_neutral(response: Any) -> bool:
    return extract_sentiment(response) == "neutral"

# Build the workflow with sentiment-based routing

# sentiment_agent → [positive/negative/neutral] → appropriate reply_agent → display

# Create tone-specific agents

reply_positive = make_reply_agent("positive")
reply_negative = make_reply_agent("negative")
reply_neutral = make_reply_agent("neutral")

# Display executor

@executor(id="display")
async def display_result(response: Any, ctx: WorkflowContext) -> None:
    """Final output handler."""
    print("\n" + "="*50)
    print("🗨️  FINAL SUPPORT RESPONSE")
    print("="*50)
    
    if isinstance(response, AgentExecutorResponse):
        try:
            result = ReplyResult.model_validate_json(response.agent_run_response.text)
            print(f"\nReply: {result.reply}")
            print(f"Escalation recommended: {result.escalation_recommended}")
        except Exception as e:
            print(f"Raw response: {response.agent_run_response.text}")
            print(f"Parse error: {e}")
    else:
        print(f"Unexpected response type: {type(response)}")
    
    print("="*50)
    await ctx.yield_output(response)

# Assemble workflow

workflow = (
    WorkflowBuilder()
    .set_start_executor(sentiment_agent)
    # Branch based on sentiment

    .add_edge(sentiment_agent, reply_positive, condition=is_positive)
    .add_edge(sentiment_agent, reply_negative, condition=is_negative)
    .add_edge(sentiment_agent, reply_neutral, condition=is_neutral)
    # All paths converge to display

    .add_edge(reply_positive, display)
    .add_edge(reply_negative, display)
    .add_edge(reply_neutral, display)
    .build()
)


# ============================================================

# 6. EXECUTION

# ============================================================

async def demo():
    """Run sentiment-aware support workflow."""
    test_messages = [
        "I love your product! It's absolutely amazing!",
        "I'm really frustrated with this delay. This is terrible service.",
        "What's the status of order ORD-12345?",
    ]
    
    for msg in test_messages:
        print(f"\n{'─'*50}")
        print(f"👤 USER: {msg}")
        print(f"{'─'*50}")
        
        request = AgentExecutorRequest(
            messages=[ChatMessage(Role.USER, text=msg)],
            should_respond=True,
        )
        
        await workflow.run(request)

if __name__ == "__main__":
    asyncio.run(demo())

```

Save this file as [`16-sentiment-analysis/code_samples/sentiment_demo.py`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/16-sentiment-analysis/code_samples/sentiment_demo.py) and run with `python sentiment_demo.py` to verify your extension works correctly.

---

## Integrating Memory Providers for Stateful Agents

Memory providers extend agent functionality by enabling persistence across conversation turns. The Microsoft Agent Framework supports multiple memory backends through a factory pattern.

### Memory Configuration

```python
from agent_framework.memory import ChatMessageStore, Mem0Provider

# In-memory store (default, per-session)

session_memory = ChatMessageStore(max_messages=50)

# Mem0 integration for user-specific long-term memory

long_term_memory = Mem0Provider(
    api_key=os.getenv("MEM0_API_KEY"),
    user_id="user_12345",
)

# Create agent with memory

stateful_agent = AgentExecutor(
    chat_client.create_agent(
        instructions="You remember details from previous conversations.",
        tools=[lookup_order],
    ),
    id="stateful_agent",
    chat_message_store_factory=lambda: session_memory,
    context_providers=[long_term_memory],
)

```

Custom memory providers can be implemented by subclassing `ChatMessageStore` and implementing `get_messages()`, `add_message()`, and `clear()` methods.

---

## Adding Observability with OpenTelemetry

Production extensions should include observability for debugging and monitoring. The Microsoft Agent Framework integrates with OpenTelemetry for distributed tracing.

### Observability Implementation

```python
from opentelemetry import trace, metrics
from agent_framework.observability import register_tracer

# Initialize tracer (from hotel_booking_workflow_sample.py pattern)

tracer = trace.get_tracer("agent_framework")
meter = metrics.get_meter("agent_framework")

# Custom spans

@executor(id="observed_agent")
async def observed_execution(ctx: WorkflowContext) -> None:
    with tracer.start_as_current_span("agent_execution") as span:
        span.set_attribute("agent.id", ctx.executor_id)
        span.set_attribute("message.count", len(ctx.messages))
        
        await ctx.next()
        
        span.set_attribute("response.length", len(ctx.result.text))

# Custom metrics

request_counter = meter.create_counter(
    "agent_requests_total",
    description="Total agent requests"
)

```

These patterns from [`hotel_booking_workflow_sample.py`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/hotel_booking_workflow_sample.py) (lines 80-90) enable integration with Azure Monitor, Jaeger, or any OpenTelemetry-compatible backend.

---

## Key Files for Extension Reference

| File | Path | Purpose |
|------|------|---------|
| [`AGENTS.md`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/AGENTS.md) | [`/AGENTS.md`](https://github.com/microsoft/ai-agents-for-beginners/blob/main//AGENTS.md) | Project overview and contribution guidelines |
| [`requirements.txt`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/requirements.txt) | [`/requirements.txt`](https://github.com/microsoft/ai-agents-for-beginners/blob/main//requirements.txt) | Python dependencies including `agent-framework` |
| [`hotel_booking_workflow_sample.py`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/hotel_booking_workflow_sample.py) | [`14-microsoft-agent-framework/code-samples/hotel_booking_workflow_sample.py`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/14-microsoft-agent-framework/code-samples/hotel_booking_workflow_sample.py) | Complete reference for workflows, tools, middleware, and memory |
| [`README.md`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/README.md) (Lesson 14) | [`14-microsoft-agent-framework/README.md`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/14-microsoft-agent-framework/README.md) | MAF concepts documentation |
| [`00-course-setup/README.md`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/00-course-setup/README.md) | [`00-course-setup/README.md`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/00-course-setup/README.md) | Environment setup and Azure authentication |
| [`co-op-translator.yml`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/co-op-translator.yml) | [`.github/workflows/co-op-translator.yml`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/.github/workflows/co-op-translator.yml) | Automatic translation pipeline |
| [`devcontainer.json`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/devcontainer.json) | [`.devcontainer/devcontainer.json`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/.devcontainer/devcontainer.json) | Pre-configured development environment |

---

## Best Practices for Repository Extensions

Follow these practices to ensure your extensions align with the repository's educational mission and technical standards.

### Structural Guidelines

- **Maintain pedagogical flow**: Start with concept explanation, show minimal code, then expand to complete implementation
- **Reuse existing patterns**: Copy scaffolding from [`hotel_booking_workflow_sample.py`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/hotel_booking_workflow_sample.py) rather than inventing new patterns
- **Document tool schemas**: Include input/output specifications for every `@ai_function`

### Code Quality

- **Add verification checks**: Include `assert` statements or simple test cases
- **Use environment variables**: Never commit secrets; follow `.env.example` patterns
- **Handle errors gracefully**: Wrap external API calls in try/except blocks

### Repository Integration

- **Update translation pipeline**: New lessons are automatically translated via [`co-op-translator.yml`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/co-op-translator.yml)
- **Test in DevContainer**: Use [`.devcontainer/devcontainer.json`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/.devcontainer/devcontainer.json) for consistent environment
- **Reference source files**: Link to specific line ranges in GitHub when documenting

---

## Summary

Extending the functionality of AI Agents for Beginners involves working with four core extension mechanisms:

- **Custom tools**: Add `@ai_function` decorated functions to expose new capabilities to LLMs
- **Middleware**: Implement `@executor` hooks for cross-cutting concerns like logging and authentication
- **Memory providers**: Configure `ChatMessageStore` or custom providers for persistent state
- **Workflows**: Use `WorkflowBuilder` to orchestrate multi-agent systems with conditional routing

The reference implementation in [`14-microsoft-agent-framework/code-samples/hotel_booking_workflow_sample.py`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/14-microsoft-agent-framework/code-samples/hotel_booking_workflow_sample.py) demonstrates all these patterns in production-ready code. New lessons should follow the established folder structure and pedagogical approach while leveraging the complete Microsoft Agent Framework API.

---

## Frequently Asked Questions

### How do I add a new tool to an existing agent?

Define a function with the `@ai_function` decorator, then include it in the `tools` list when calling `chat_client.create_agent()`. The LLM will automatically discover the tool's schema and can invoke it during conversation. See the `analyze_sentiment` example in Step 2 above.

### Can I use a different LLM provider instead of OpenAI?

Yes. The Microsoft Agent Framework provides chat clients for multiple providers. Replace `OpenAIChatClient` with `AzureOpenAIChatClient`, `AnthropicChatClient`, or implement the `ChatClient` protocol for custom providers. All agent code remains identical regardless of the underlying LLM.

### What's the difference between memory and context providers?

**Memory** (`chat_message_store_factory`) stores conversation history for the current session, enabling multi-turn context. **Context providers** (`context_providers`) inject additional information from external sources—such as user profiles, long-term memory services like Mem0, or enterprise databases—into each agent invocation.

### How do I test my extension without deploying to Azure?

Use the [`.devcontainer/devcontainer.json`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/.devcontainer/devcontainer.json) configuration for a consistent local environment. Set `OpenAIChatClient` with an OpenAI API key in `.env`, or use framework mocking utilities to test agent logic without live LLM calls. The [`sentiment_demo.py`](https://github.com/microsoft/ai-agents-for-beginners/blob/main/sentiment_demo.py) script runs entirely locally with no Azure dependencies.