Python: AG-UI protocol support (#1826)

* Add AG-UI integration

* Fix tests. PR feedback

* Cleanup

* PR Feedback

* Improve README and getting started experience

* Fix links
This commit is contained in:
Evan Mattson
2025-11-05 14:25:24 +09:00
committed by GitHub
Unverified
parent 0c862e97a6
commit 35a8565495
51 changed files with 7677 additions and 163 deletions
@@ -0,0 +1,3 @@
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_API_KEY=your-api-key-here
PORT=8000
+5
View File
@@ -0,0 +1,5 @@
{
"python.analysis.extraPaths": [
"${workspaceFolder}/packages/ag-ui/examples"
]
}
+243
View File
@@ -0,0 +1,243 @@
# Agent Framework AG-UI Integration
AG-UI protocol integration for Agent Framework, enabling seamless integration with AG-UI's web interface and streaming protocol.
## Installation
```bash
pip install agent-framework-ag-ui
```
## Quick Start
```python
from fastapi import FastAPI
from agent_framework import ChatAgent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
# Create your agent
agent = ChatAgent(
name="my_agent",
instructions="You are a helpful assistant.",
chat_client=AzureOpenAIChatClient(model_id="gpt-4o"),
)
# Create FastAPI app and add AG-UI endpoint
app = FastAPI()
add_agent_framework_fastapi_endpoint(app, agent, "/agent")
# Run with: uvicorn main:app --reload
```
## Features
This integration supports all 7 AG-UI features:
1. **Agentic Chat**: Basic streaming chat with tool calling support
2. **Backend Tool Rendering**: Tools executed on backend with results streamed via ToolCallResultEvent
3. **Human in the Loop**: Function approval requests for user confirmation before tool execution
4. **Agentic Generative UI**: Async tools for long-running operations with progress updates
5. **Tool-based Generative UI**: Custom UI components rendered on frontend based on tool calls
6. **Shared State**: Bidirectional state sync using StateSnapshotEvent and StateDeltaEvent
7. **Predictive State Updates**: Stream tool arguments as optimistic state updates during execution
## Examples
Complete examples for all features are in the `examples/` directory:
- `examples/agents/simple_agent.py` - Basic agentic chat
- `examples/agents/weather_agent.py` - Backend tool rendering
- `examples/agents/task_planner_agent.py` - Human in the loop with approvals
- `examples/agents/research_assistant_agent.py` - Agentic generative UI
- `examples/agents/ui_generator_agent.py` - Tool-based generative UI
- `examples/agents/recipe_agent.py` - Shared state management
- `examples/agents/document_writer_agent.py` - Predictive state updates
- `examples/server/main.py` - FastAPI server with all endpoints
Run the example server:
```bash
cd examples/server
uvicorn main:app --reload
```
To enable debug logging:
```bash
ENABLE_DEBUG_LOGGING=1 uvicorn main:app --reload
```
The server exposes endpoints at:
- `/agentic_chat`
- `/backend_tool_rendering`
- `/human_in_the_loop`
- `/agentic_generative_ui`
- `/tool_based_generative_ui`
- `/shared_state`
- `/predictive_state_updates`
## Architecture
The package uses a clean, orchestrator-based architecture:
- **AgentFrameworkAgent**: Lightweight wrapper that delegates to orchestrators
- **Orchestrators**: Handle different execution flows (default, human-in-the-loop, etc.)
- **Confirmation Strategies**: Domain-specific confirmation messages (extensible)
- **AgentFrameworkEventBridge**: Converts AgentRunResponseUpdate to AG-UI events
- **Message Adapters**: Bidirectional conversion between AG-UI and Agent Framework message formats
- **FastAPI Endpoint**: Streaming HTTP endpoint with Server-Sent Events (SSE)
### Key Design Patterns
- **Orchestrator Pattern**: Separates flow control from protocol translation
- **Strategy Pattern**: Pluggable confirmation message strategies
- **Context Object**: Lazy-loaded execution context passed to orchestrators
- **Event Bridge**: Stateless translation of Agent Framework events to AG-UI events
## Advanced Usage
### Shared State
State is injected as system messages and updated via predictive state updates:
```python
from agent_framework import ChatAgent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
# Create your agent
agent = ChatAgent(
name="recipe_agent",
chat_client=AzureOpenAIChatClient(model_id="gpt-4o"),
)
state_schema = {
"recipe": {
"type": "object",
"properties": {
"name": {"type": "string"},
"ingredients": {"type": "array"}
}
}
}
# Configure which tool updates which state fields
predict_state_config = {
"recipe": {"tool": "update_recipe", "tool_argument": "recipe_data"}
}
wrapped_agent = AgentFrameworkAgent(
agent=agent,
state_schema=state_schema,
predict_state_config=predict_state_config,
)
```
### Predictive State Updates
Predictive state updates automatically stream tool arguments as optimistic state updates:
```python
from agent_framework import ChatAgent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
# Create your agent
agent = ChatAgent(
name="document_writer",
chat_client=AzureOpenAIChatClient(model_id="gpt-4o"),
)
predict_state_config = {
"current_title": {"tool": "write_document", "tool_argument": "title"},
"current_content": {"tool": "write_document", "tool_argument": "content"},
}
wrapped_agent = AgentFrameworkAgent(
agent=agent,
state_schema={"current_title": {"type": "string"}, "current_content": {"type": "string"}},
predict_state_config=predict_state_config,
require_confirmation=True, # User can approve/reject changes
)
```
### Custom Confirmation Strategies
Provide domain-specific confirmation messages:
```python
from typing import Any
from agent_framework import ChatAgent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_ag_ui import AgentFrameworkAgent, ConfirmationStrategy
class CustomConfirmationStrategy(ConfirmationStrategy):
def on_approval_accepted(self, steps: list[dict[str, Any]]) -> str:
return "Your custom approval message!"
def on_approval_rejected(self, steps: list[dict[str, Any]]) -> str:
return "Your custom rejection message!"
def on_state_confirmed(self) -> str:
return "State changes confirmed!"
def on_state_rejected(self) -> str:
return "State changes rejected!"
agent = ChatAgent(
name="custom_agent",
chat_client=AzureOpenAIChatClient(model_id="gpt-4o"),
)
wrapped_agent = AgentFrameworkAgent(
agent=agent,
confirmation_strategy=CustomConfirmationStrategy(),
)
```
### Human in the Loop
Human-in-the-loop is automatically handled when tools are marked for approval:
```python
from agent_framework import ai_function
@ai_function(approval_mode="always_require")
def sensitive_action(param: str) -> str:
"""This action requires user approval."""
return f"Executed with {param}"
# The orchestrator automatically detects approval responses and handles them
```
### Custom Orchestrators
Add custom execution flows by implementing the Orchestrator pattern:
```python
from agent_framework_ag_ui._orchestrators import Orchestrator, ExecutionContext
class MyCustomOrchestrator(Orchestrator):
def can_handle(self, context: ExecutionContext) -> bool:
# Return True if this orchestrator should handle the request
return context.input_data.get("custom_mode") == True
async def run(self, context: ExecutionContext):
# Custom execution logic
yield RunStartedEvent(...)
# ... your custom flow
yield RunFinishedEvent(...)
wrapped_agent = AgentFrameworkAgent(
agent=your_agent,
orchestrators=[MyCustomOrchestrator(), DefaultOrchestrator()],
)
## Documentation
For detailed documentation, see [DESIGN.md](DESIGN.md).
## License
MIT
@@ -0,0 +1 @@
# Copyright (c) Microsoft. All rights reserved.
@@ -0,0 +1,8 @@
# Copyright (c) Microsoft. All rights reserved.
"""Entry point for running the AG-UI examples server as a module."""
from .server.main import main
if __name__ == "__main__":
main()
@@ -0,0 +1,3 @@
# Copyright (c) Microsoft. All rights reserved.
"""Example agents for AG-UI demonstration."""
@@ -0,0 +1,58 @@
# Copyright (c) Microsoft. All rights reserved.
"""Example agent demonstrating predictive state updates with document writing."""
from agent_framework import ChatAgent, ai_function
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_ag_ui import AgentFrameworkAgent, DocumentWriterConfirmationStrategy
@ai_function
def write_document_local(document: str) -> str:
"""Write a document. Use markdown formatting to format the document.
It's good to format the document extensively so it's easy to read.
You can use all kinds of markdown.
However, do not use italic or strike-through formatting, it's reserved for another purpose.
You MUST write the full document, even when changing only a few words.
When making edits to the document, try to make them minimal - do not change every word.
Keep stories SHORT!
Args:
document: The complete document content in markdown format
Returns:
Confirmation that the document was written
"""
return "Document written."
agent = ChatAgent(
name="document_writer",
instructions=(
"You are a helpful assistant for writing documents. "
"To write the document, you MUST use the write_document_local tool. "
"You MUST write the full document, even when changing only a few words. "
"When you wrote the document, DO NOT repeat it as a message. "
"Just briefly summarize the changes you made. 2 sentences max. "
"\n\n"
"The current state of the document will be provided to you. "
"When editing, make minimal changes - do not change every word unless requested."
),
chat_client=AzureOpenAIChatClient(),
tools=[write_document_local],
)
document_writer_agent = AgentFrameworkAgent(
agent=agent,
name="DocumentWriter",
description="Writes and edits documents with predictive state updates",
state_schema={
"document": {"type": "string", "description": "The current document content"},
},
predict_state_config={
"document": {"tool": "write_document_local", "tool_argument": "document"},
},
confirmation_strategy=DocumentWriterConfirmationStrategy(),
)
@@ -0,0 +1,76 @@
# Copyright (c) Microsoft. All rights reserved.
"""Human-in-the-loop agent demonstrating step customization (Feature 5)."""
from enum import Enum
from agent_framework import ChatAgent, ai_function
from agent_framework.azure import AzureOpenAIChatClient
from pydantic import BaseModel, Field
class StepStatus(str, Enum):
"""Status of a task step."""
ENABLED = "enabled"
DISABLED = "disabled"
class TaskStep(BaseModel):
"""A single step in a task execution plan."""
description: str = Field(..., description="The text of the step in imperative form (e.g., 'Dig hole', 'Open door')")
status: StepStatus = Field(default=StepStatus.ENABLED, description="Whether the step is enabled or disabled")
@ai_function(
name="generate_task_steps",
description="Generate execution steps for a task",
approval_mode="always_require",
)
def generate_task_steps(steps: list[TaskStep]) -> str:
"""Make up 10 steps (only a couple of words per step) that are required for a task.
The step should be in imperative form (i.e. Dig hole, Open door, ...).
Each step will have status='enabled' by default.
Args:
steps: An array of 10 step objects, each containing description and status
Returns:
Confirmation message
"""
return f"Generated {len(steps)} execution steps for the task."
# Create the human-in-the-loop agent using tool-based approach for predictive state
human_in_the_loop_agent = ChatAgent(
name="human_in_the_loop_agent",
instructions="""You are a helpful assistant that can perform any task by breaking it down into steps.
When asked to perform a task, you MUST call the `generate_task_steps` function with the proper
number of steps per the request.
Rules for steps:
- Each step description should be in imperative form (e.g., "Dig hole", "Open door", "Prepare ingredients")
- Each step should be brief (only a couple of words)
- All steps must have status='enabled' initially
Example steps for "Build a robot":
1. "Design blueprint"
2. "Gather components"
3. "Assemble frame"
4. "Install motors"
5. "Wire electronics"
6. "Program controller"
7. "Test movements"
8. "Add sensors"
9. "Calibrate systems"
10. "Final testing"
After calling the function, provide a brief acknowledgment like:
"I've created a plan with 10 steps. You can customize which steps to enable before I proceed."
""",
chat_client=AzureOpenAIChatClient(),
tools=[generate_task_steps],
)
@@ -0,0 +1,122 @@
# Copyright (c) Microsoft. All rights reserved.
"""Recipe agent example demonstrating shared state management (Feature 3)."""
from enum import Enum
from agent_framework import ChatAgent, ai_function
from agent_framework.azure import AzureOpenAIChatClient
from pydantic import BaseModel, Field
from agent_framework_ag_ui import AgentFrameworkAgent, RecipeConfirmationStrategy
class SkillLevel(str, Enum):
"""The skill level required for the recipe."""
BEGINNER = "Beginner"
INTERMEDIATE = "Intermediate"
ADVANCED = "Advanced"
class CookingTime(str, Enum):
"""The cooking time of the recipe."""
FIVE_MIN = "5 min"
FIFTEEN_MIN = "15 min"
THIRTY_MIN = "30 min"
FORTY_FIVE_MIN = "45 min"
SIXTY_PLUS_MIN = "60+ min"
class Ingredient(BaseModel):
"""An ingredient with its details."""
icon: str = Field(..., description="Emoji icon representing the ingredient (e.g., 🥕)")
name: str = Field(..., description="Name of the ingredient")
amount: str = Field(..., description="Amount or quantity of the ingredient")
class Recipe(BaseModel):
"""A complete recipe."""
title: str = Field(..., description="The title of the recipe")
skill_level: SkillLevel = Field(..., description="The skill level required")
special_preferences: list[str] = Field(
default_factory=list, description="Dietary preferences (e.g., Vegetarian, Gluten-free)"
)
cooking_time: CookingTime = Field(..., description="The estimated cooking time")
ingredients: list[Ingredient] = Field(..., description="Complete list of ingredients")
instructions: list[str] = Field(..., description="Step-by-step cooking instructions")
@ai_function
def update_recipe(recipe: Recipe) -> str:
"""Update the recipe with new or modified content.
You MUST write the complete recipe with ALL fields, even when changing only a few items.
When modifying an existing recipe, include ALL existing ingredients and instructions plus your changes.
NEVER delete existing data - only add or modify.
Args:
recipe: The complete recipe object with all details
Returns:
Confirmation that the recipe was updated
"""
return "Recipe updated."
# Create the recipe agent using tool-based approach for streaming
agent = ChatAgent(
name="recipe_agent",
instructions="""You are a helpful recipe assistant that creates and modifies recipes.
CRITICAL RULES:
1. You will receive the current recipe state in the system context
2. To update the recipe, you MUST use the update_recipe tool
3. When modifying a recipe, ALWAYS include ALL existing data plus your changes in the tool call
4. NEVER delete existing ingredients or instructions - only add or modify
5. After calling the tool, provide a brief conversational message (1-2 sentences)
When creating a NEW recipe:
- Provide all required fields: title, skill_level, cooking_time, ingredients, instructions
- Use actual emojis for ingredient icons (🥕 🧄 🧅 🍅 🌿 🍗 🥩 🧀)
- Leave special_preferences empty unless specified
- Message: "Here's your recipe!" or similar
When MODIFYING or IMPROVING an existing recipe:
- Include ALL existing ingredients + any new ones
- Include ALL existing instructions + any new/modified ones
- Update other fields as needed
- Message: Explain what you improved (e.g., "I upgraded the ingredients to premium quality")
- When asked to "improve", enhance with:
* Better ingredients (upgrade quality, add complementary flavors)
* More detailed instructions
* Professional techniques
* Adjust skill_level if complexity changes
* Add relevant special_preferences
Example improvements:
- Upgrade "chicken""organic free-range chicken breast"
- Add herbs: basil, oregano, thyme
- Add aromatics: garlic, shallots
- Add finishing touches: lemon zest, fresh parsley
- Make instructions more detailed and professional
""",
chat_client=AzureOpenAIChatClient(),
tools=[update_recipe],
)
recipe_agent = AgentFrameworkAgent(
agent=agent,
name="RecipeAgent",
description="Creates and modifies recipes with streaming state updates",
state_schema={
"recipe": {"type": "object", "description": "The current recipe"},
},
predict_state_config={
"recipe": {"tool": "update_recipe", "tool_argument": "recipe"},
},
confirmation_strategy=RecipeConfirmationStrategy(),
)
@@ -0,0 +1,100 @@
# Copyright (c) Microsoft. All rights reserved.
"""Example agent demonstrating agentic generative UI with custom events during execution."""
import asyncio
from agent_framework import ChatAgent, ai_function
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
@ai_function
async def research_topic(topic: str) -> str:
"""Research a topic and generate a comprehensive report.
Args:
topic: The topic to research
Returns:
Research report
"""
# Simulate multi-step research process
steps = [
("Searching databases", 1.0),
("Analyzing sources", 1.5),
("Synthesizing information", 1.0),
("Generating report", 0.5),
]
results: list[str] = []
for step_name, duration in steps:
await asyncio.sleep(duration)
results.append(f"- {step_name}: completed")
return f"Research report on '{topic}':\n" + "\n".join(results)
@ai_function
async def create_presentation(title: str, num_slides: int) -> str:
"""Create a presentation with multiple slides.
Args:
title: Presentation title
num_slides: Number of slides to create
Returns:
Presentation summary
"""
# Simulate slide generation
slides: list[str] = []
for i in range(num_slides):
await asyncio.sleep(0.5)
slides.append(f"Slide {i + 1}: Content for {title}")
return f"Created presentation '{title}' with {num_slides} slides:\n" + "\n".join(slides)
@ai_function
async def analyze_data(dataset: str) -> str:
"""Analyze a dataset and produce insights.
Args:
dataset: The dataset name to analyze
Returns:
Analysis results
"""
# Simulate data analysis phases
phases = [
("Loading data", 0.8),
("Cleaning data", 1.0),
("Running statistical analysis", 1.2),
("Generating visualizations", 0.7),
]
insights: list[str] = []
for phase_name, duration in phases:
await asyncio.sleep(duration)
insights.append(f"- {phase_name}: done")
return f"Analysis of '{dataset}':\n" + "\n".join(insights)
agent = ChatAgent(
name="research_assistant",
instructions=(
"You are a research and analysis assistant. "
"You can research topics, create presentations, and analyze data. "
"Use the available tools to help users with their research needs."
),
chat_client=AzureOpenAIChatClient(),
tools=[research_topic, create_presentation, analyze_data],
)
research_assistant_agent = AgentFrameworkAgent(
agent=agent,
name="ResearchAssistant",
description="Research assistant that emits progress events during task execution",
)
@@ -0,0 +1,13 @@
# Copyright (c) Microsoft. All rights reserved.
"""Simple agentic chat example (Feature 1: Agentic Chat)."""
from agent_framework import ChatAgent
from agent_framework.azure import AzureOpenAIChatClient
# Create a simple chat agent
agent = ChatAgent(
name="simple_chat_agent",
instructions="You are a helpful assistant. Be concise and friendly.",
chat_client=AzureOpenAIChatClient(),
)
@@ -0,0 +1,73 @@
# Copyright (c) Microsoft. All rights reserved.
"""Example agent demonstrating human-in-the-loop with function approvals."""
from agent_framework import ChatAgent, ai_function
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_ag_ui import AgentFrameworkAgent, TaskPlannerConfirmationStrategy
@ai_function(approval_mode="always_require")
def create_calendar_event(title: str, date: str, time: str) -> str:
"""Create a calendar event.
Args:
title: The event title
date: The event date (YYYY-MM-DD)
time: The event time (HH:MM)
Returns:
Confirmation message
"""
return f"Calendar event '{title}' created for {date} at {time}"
@ai_function(approval_mode="always_require")
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email.
Args:
to: Recipient email address
subject: Email subject
body: Email body text
Returns:
Confirmation message
"""
return f"Email sent to {to} with subject '{subject}'"
@ai_function(approval_mode="always_require")
def book_meeting_room(room_name: str, date: str, start_time: str, end_time: str) -> str:
"""Book a meeting room.
Args:
room_name: The meeting room name
date: The booking date (YYYY-MM-DD)
start_time: Start time (HH:MM)
end_time: End time (HH:MM)
Returns:
Confirmation message
"""
return f"Meeting room '{room_name}' booked for {date} from {start_time} to {end_time}"
agent = ChatAgent(
name="task_planner",
instructions=(
"You are a helpful assistant that plans and executes tasks. "
"You have access to calendar, email, and meeting room booking functions. "
"All of these actions require user approval before execution."
),
chat_client=AzureOpenAIChatClient(),
tools=[create_calendar_event, send_email, book_meeting_room],
)
task_planner_agent = AgentFrameworkAgent(
agent=agent,
name="TaskPlanner",
description="Plans and executes tasks with user approval",
confirmation_strategy=TaskPlannerConfirmationStrategy(),
)
@@ -0,0 +1,318 @@
# Copyright (c) Microsoft. All rights reserved.
"""Task steps agent demonstrating agentic generative UI (Feature 6)."""
import asyncio
from collections.abc import AsyncGenerator
from enum import Enum
from typing import Any
from ag_ui.core import (
EventType,
MessagesSnapshotEvent,
RunFinishedEvent,
StateDeltaEvent,
StateSnapshotEvent,
TextMessageContentEvent,
TextMessageEndEvent,
TextMessageStartEvent,
ToolCallStartEvent,
)
from agent_framework import ChatAgent, ai_function
from agent_framework.azure import AzureOpenAIChatClient
from pydantic import BaseModel, Field
from agent_framework_ag_ui import AgentFrameworkAgent
class StepStatus(str, Enum):
"""Status of a task step."""
PENDING = "pending"
COMPLETED = "completed"
class TaskStep(BaseModel):
"""A single step in a task."""
description: str = Field(
..., description="The text of the step in gerund form (e.g., 'Digging hole', 'Opening door')"
)
status: StepStatus = Field(default=StepStatus.PENDING, description="The status of the step")
@ai_function
def generate_task_steps(steps: list[TaskStep]) -> str:
"""Generate a list of task steps for completing a task.
Args:
steps: Complete list of task steps with descriptions and status
Returns:
Confirmation that steps were generated
"""
return "Steps generated."
# Create the task steps agent using tool-based approach for streaming
agent = ChatAgent(
name="task_steps_agent",
instructions="""You are a helpful assistant that breaks down tasks into actionable steps.
When asked to perform a task, you MUST:
1. Use the generate_task_steps tool to create the steps
2. Pay attention to how many steps the user requests (if specified)
3. If no specific number is mentioned, use a reasonable number of steps (typically 5-10)
4. Each step description should be in gerund form (e.g., "Designing spacecraft", "Training astronauts")
5. Each step should be brief (only 2-4 words)
6. All steps must have status='pending'
7. After calling the tool, provide a brief conversational message (one sentence) saying you created the plan
Example steps for "Build a treehouse in 5 steps":
- "Selecting location"
- "Gathering materials"
- "Assembling frame"
- "Installing platform"
- "Adding finishing touches"
""",
chat_client=AzureOpenAIChatClient(),
tools=[generate_task_steps],
)
task_steps_agent = AgentFrameworkAgent(
agent=agent,
name="TaskStepsAgent",
description="Generates task steps with streaming state updates",
state_schema={
"steps": {"type": "array", "description": "The list of task steps"},
},
predict_state_config={
"steps": {
"tool": "generate_task_steps",
"tool_argument": "steps",
}
},
require_confirmation=False, # Agentic generative UI updates automatically without confirmation
)
# Wrap the agent's run method to add step execution simulation
class TaskStepsAgentWithExecution:
"""Wrapper that adds step execution simulation after plan generation.
This wrapper delegates to AgentFrameworkAgent but is recognized as compatible
by add_agent_framework_fastapi_endpoint since it implements run_agent().
"""
def __init__(self, base_agent: AgentFrameworkAgent):
"""Initialize wrapper with base agent."""
self._base_agent = base_agent
@property
def name(self) -> str:
"""Delegate to base agent."""
return self._base_agent.name
@property
def description(self) -> str:
"""Delegate to base agent."""
return self._base_agent.description
def __getattr__(self, name: str) -> Any:
"""Delegate all other attribute access to base agent."""
return getattr(self._base_agent, name)
async def run_agent(self, input_data: dict[str, Any]) -> AsyncGenerator[Any, None]:
"""Run the agent and then simulate step execution."""
import logging
import uuid
logger = logging.getLogger(__name__)
logger.info(">>> TaskStepsAgentWithExecution.run_agent() called - wrapper is active")
# First, run the base agent to generate the plan - buffer text messages
final_state: dict[str, Any] | None = None
run_finished_event: Any = None
tool_call_id: str | None = None
buffered_text_events: list[Any] = [] # Buffer text from first LLM call
async for event in self._base_agent.run_agent(input_data):
event_type_str = str(event.type) if hasattr(event, "type") else type(event).__name__
logger.info(f">>> Processing event: {event_type_str}")
match event:
case StateSnapshotEvent(snapshot=snapshot):
final_state = snapshot
logger.info(f">>> Captured STATE_SNAPSHOT event with state: {final_state}")
yield event
case RunFinishedEvent():
run_finished_event = event
logger.info(">>> Captured RUN_FINISHED event - will send after step execution and summary")
case ToolCallStartEvent(tool_call_id=call_id):
tool_call_id = call_id
logger.info(f">>> Captured tool_call_id: {tool_call_id}")
yield event
case TextMessageStartEvent() | TextMessageContentEvent() | TextMessageEndEvent():
buffered_text_events.append(event)
logger.info(f">>> Buffered {event_type_str} from first LLM call")
case _:
logger.info(f">>> Yielding event immediately: {event_type_str}")
yield event
logger.info(f">>> Base agent completed. Final state: {final_state}")
# Now simulate executing the steps
if final_state and "steps" in final_state:
steps = final_state["steps"]
logger.info(f">>> Starting step execution simulation for {len(steps)} steps")
for i in range(len(steps)):
logger.info(f">>> Simulating execution of step {i + 1}/{len(steps)}: {steps[i].get('description')}")
await asyncio.sleep(1.0) # Simulate work
# Update step to completed
steps[i]["status"] = "completed"
logger.info(f">>> Step {i + 1} marked as completed")
# Send delta event with manual JSON patch format
delta_event = StateDeltaEvent(
type=EventType.STATE_DELTA,
delta=[
{
"op": "replace",
"path": f"/steps/{i}/status",
"value": "completed",
}
],
)
logger.info(f">>> Yielding StateDeltaEvent for step {i + 1}")
yield delta_event
# Send final snapshot
final_snapshot = StateSnapshotEvent(
type=EventType.STATE_SNAPSHOT,
snapshot={"steps": steps},
)
logger.info(">>> Yielding final StateSnapshotEvent with all steps completed")
yield final_snapshot
# SECOND LLM call: Stream summary from chat client directly
logger.info(">>> Making SECOND LLM call to generate summary after step execution")
# Get the underlying chat agent and client
chat_agent = self._base_agent.agent # type: ignore
chat_client = chat_agent.chat_client # type: ignore
# Build messages for summary call
from agent_framework._types import ChatMessage, TextContent
original_messages = input_data.get("messages", [])
# Convert to ChatMessage objects if needed
messages: list[ChatMessage] = []
for msg in original_messages:
if isinstance(msg, dict):
content_str = msg.get("content", "")
if isinstance(content_str, str):
messages.append(
ChatMessage(
role=msg.get("role", "user"),
contents=[TextContent(text=content_str)],
)
)
elif isinstance(msg, ChatMessage):
messages.append(msg)
# Add completion message
messages.append(
ChatMessage(
role="user",
contents=[
TextContent(
text="The steps have been successfully executed. Provide a brief one-sentence summary."
)
],
)
)
# Stream the LLM response and manually emit text events
logger.info(">>> Calling chat client for summary")
message_id = str(uuid.uuid4())
try:
# Emit TEXT_MESSAGE_START
yield TextMessageStartEvent(
type=EventType.TEXT_MESSAGE_START,
message_id=message_id,
role="assistant",
)
# Small delay to ensure START event is processed before CONTENT events
await asyncio.sleep(0.01)
# Stream completion
accumulated_text = ""
async for chunk in chat_client.get_streaming_response(messages=messages):
# chunk is ChatResponseUpdate
if hasattr(chunk, "text") and chunk.text:
accumulated_text += chunk.text
# Emit TEXT_MESSAGE_CONTENT
yield TextMessageContentEvent(
type=EventType.TEXT_MESSAGE_CONTENT,
message_id=message_id,
delta=chunk.text,
)
# Emit TEXT_MESSAGE_END
yield TextMessageEndEvent(
type=EventType.TEXT_MESSAGE_END,
message_id=message_id,
)
logger.info(f">>> Summary complete: {accumulated_text}")
# Build complete message for persistence
summary_message = {
"role": "assistant",
"content": accumulated_text,
"id": message_id,
}
final_messages = list(original_messages)
final_messages.append(summary_message)
# Emit MessagesSnapshotEvent to persist in history
yield MessagesSnapshotEvent(
type=EventType.MESSAGES_SNAPSHOT,
messages=final_messages,
)
except Exception as e:
logger.error(f">>> Error generating summary: {e}")
# Generate a new message ID for the error
error_message_id = str(uuid.uuid4())
# Yield TEXT_MESSAGE_START for error
yield TextMessageStartEvent(
type=EventType.TEXT_MESSAGE_START,
message_id=error_message_id,
role="assistant",
)
# Yield error message content
yield TextMessageContentEvent(
type=EventType.TEXT_MESSAGE_CONTENT,
message_id=error_message_id,
delta=f"[Summary generation error: {e!s}]",
)
# Yield TEXT_MESSAGE_END for error
yield TextMessageEndEvent(
type=EventType.TEXT_MESSAGE_END,
message_id=error_message_id,
)
else:
logger.warning(f">>> No steps found in final_state to execute. final_state={final_state}")
# Finally send the original RUN_FINISHED event
if run_finished_event:
logger.info(">>> Yielding original RUN_FINISHED event")
yield run_finished_event
# Export the wrapped agent
task_steps_agent_wrapped = TaskStepsAgentWithExecution(task_steps_agent)
@@ -0,0 +1,119 @@
# Copyright (c) Microsoft. All rights reserved.
"""Example agent demonstrating Tool-based Generative UI (Feature 5)."""
from typing import Any
from agent_framework import ChatAgent, ai_function
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
@ai_function
def generate_haiku(english: list[str], japanese: list[str], image_name: str | None, gradient: str) -> str:
"""Generate a haiku with image and gradient background (FRONTEND_RENDER).
This tool generates UI for displaying a haiku with an image and gradient background.
The frontend should render this as a custom haiku component.
Args:
english: English haiku lines (exactly 3 lines)
japanese: Japanese haiku lines (exactly 3 lines)
image_name: Image filename for visual accompaniment. Must be one of:
- "Osaka_Castle_Turret_Stone_Wall_Pine_Trees_Daytime.jpg"
- "Tokyo_Skyline_Night_Tokyo_Tower_Mount_Fuji_View.jpg"
- "Itsukushima_Shrine_Miyajima_Floating_Torii_Gate_Sunset_Long_Exposure.jpg"
- "Takachiho_Gorge_Waterfall_River_Lush_Greenery_Japan.jpg"
- "Bonsai_Tree_Potted_Japanese_Art_Green_Foliage.jpeg"
- "Shirakawa-go_Gassho-zukuri_Thatched_Roof_Village_Aerial_View.jpg"
- "Ginkaku-ji_Silver_Pavilion_Kyoto_Japanese_Garden_Pond_Reflection.jpg"
- "Senso-ji_Temple_Asakusa_Cherry_Blossoms_Kimono_Umbrella.jpg"
- "Cherry_Blossoms_Sakura_Night_View_City_Lights_Japan.jpg"
- "Mount_Fuji_Lake_Reflection_Cherry_Blossoms_Sakura_Spring.jpg"
gradient: CSS gradient string for background (e.g., "linear-gradient(135deg, #667eea 0%, #764ba2 100%)")
Returns:
Haiku metadata for frontend rendering
"""
return f"Haiku generated with image: {image_name}"
@ai_function
def create_chart(chart_type: str, data_points: list[dict[str, Any]], title: str) -> str:
"""Create an interactive chart (FRONTEND_RENDER).
This tool creates chart specifications for frontend rendering.
The frontend should render this as an interactive chart component.
Args:
chart_type: Type of chart (bar, line, pie, scatter)
data_points: Data points for the chart
title: Chart title
Returns:
Chart specification for frontend rendering
"""
return f"Chart '{title}' created with {len(data_points)} data points"
@ai_function
def display_timeline(events: list[dict[str, Any]], start_date: str, end_date: str) -> str:
"""Display an interactive timeline (FRONTEND_RENDER).
This tool creates timeline specifications for frontend rendering.
The frontend should render this as an interactive timeline component.
Args:
events: Events to display on the timeline
start_date: Timeline start date
end_date: Timeline end date
Returns:
Timeline specification for frontend rendering
"""
return f"Timeline created with {len(events)} events from {start_date} to {end_date}"
@ai_function
def show_comparison_table(items: list[dict[str, Any]], columns: list[str]) -> str:
"""Show a comparison table (FRONTEND_RENDER).
This tool creates table specifications for frontend rendering.
The frontend should render this as an interactive comparison table.
Args:
items: Items to compare
columns: Column names
Returns:
Table specification for frontend rendering
"""
return f"Comparison table created with {len(items)} items and {len(columns)} columns"
# Create the UI generator agent using tool-based approach with forced tool usage
agent = ChatAgent(
name="ui_generator",
instructions="""You MUST use the provided tools to generate content. Never respond with plain text descriptions.
For haiku requests:
- Call generate_haiku tool with all 4 required parameters
- English: 3 lines
- Japanese: 3 lines
- image_name: Choose from available images
- gradient: CSS gradient string
For other requests, use the appropriate tool (create_chart, display_timeline, show_comparison_table).
""",
chat_client=AzureOpenAIChatClient(),
tools=[generate_haiku, create_chart, display_timeline, show_comparison_table],
# Force tool usage - the LLM MUST call a tool, cannot respond with plain text
chat_options={"tool_choice": "required"},
)
ui_generator_agent = AgentFrameworkAgent(
agent=agent,
name="UIGenerator",
description="Generates custom UI components through tool calls",
)
@@ -0,0 +1,71 @@
# Copyright (c) Microsoft. All rights reserved.
"""Weather agent example demonstrating backend tool rendering."""
from typing import Any
from agent_framework import ChatAgent, ai_function
from agent_framework.azure import AzureOpenAIChatClient
@ai_function
def get_weather(location: str) -> dict[str, Any]:
"""Get the current weather for a location.
Args:
location: The city or location to get weather for.
Returns:
Weather information as a dictionary with temperatures in Celsius.
"""
# Simulated weather data with structured format (temperatures in Celsius for dojo UI)
weather_data = {
"seattle": {"temperature": 11, "conditions": "rainy", "humidity": 75, "wind_speed": 12, "feels_like": 10},
"san francisco": {"temperature": 14, "conditions": "foggy", "humidity": 85, "wind_speed": 8, "feels_like": 13},
"new york city": {"temperature": 18, "conditions": "sunny", "humidity": 60, "wind_speed": 10, "feels_like": 17},
"miami": {"temperature": 29, "conditions": "hot and humid", "humidity": 90, "wind_speed": 5, "feels_like": 32},
"chicago": {"temperature": 9, "conditions": "windy", "humidity": 65, "wind_speed": 20, "feels_like": 6},
}
location_lower = location.lower()
if location_lower in weather_data:
return weather_data[location_lower]
return {
"temperature": 21,
"conditions": "partly cloudy",
"humidity": 50,
"wind_speed": 10,
"feels_like": 20,
}
@ai_function
def get_forecast(location: str, days: int = 3) -> str:
"""Get the weather forecast for a location.
Args:
location: The city or location to get forecast for.
days: Number of days to forecast (default: 3).
Returns:
Forecast information string.
"""
forecast: list[str] = []
for day in range(1, min(days, 7) + 1):
forecast.append(f"Day {day}: Partly cloudy, {60 + day * 2}°F")
return f"{days}-day forecast for {location}:\n" + "\n".join(forecast)
# Create the weather agent
weather_agent = ChatAgent(
name="weather_agent",
instructions=(
"You are a helpful weather assistant. "
"Use the get_weather and get_forecast functions to help users with weather information. "
"Always provide friendly and informative responses."
),
chat_client=AzureOpenAIChatClient(),
tools=[get_weather, get_forecast],
)
@@ -0,0 +1 @@
# Copyright (c) Microsoft. All rights reserved.
@@ -0,0 +1,3 @@
# Copyright (c) Microsoft. All rights reserved.
"""API endpoints for AG-UI examples."""
@@ -0,0 +1,22 @@
# Copyright (c) Microsoft. All rights reserved.
"""Backend tool rendering endpoint."""
from fastapi import FastAPI
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
from ...agents.weather_agent import weather_agent
def register_backend_tool_rendering(app: FastAPI) -> None:
"""Register the backend tool rendering endpoint.
Args:
app: The FastAPI application.
"""
add_agent_framework_fastapi_endpoint(
app,
weather_agent,
"/backend_tool_rendering",
)
@@ -0,0 +1,129 @@
# Copyright (c) Microsoft. All rights reserved.
"""Example FastAPI server with AG-UI endpoints."""
import logging
import os
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
from ..agents.document_writer_agent import document_writer_agent
from ..agents.human_in_the_loop_agent import human_in_the_loop_agent
from ..agents.recipe_agent import recipe_agent
from ..agents.simple_agent import agent as simple_agent
from ..agents.task_steps_agent import task_steps_agent_wrapped as task_steps_agent # Custom wrapper
from ..agents.ui_generator_agent import ui_generator_agent
from ..agents.weather_agent import weather_agent
# Configure logging to file and console (disabled by default - set ENABLE_DEBUG_LOGGING=1 to enable)
if os.getenv("ENABLE_DEBUG_LOGGING"):
log_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "ag_ui_server.log")
# Remove any existing handlers
root_logger = logging.getLogger()
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# Configure new handlers
file_handler = logging.FileHandler(log_file, mode="w")
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
root_logger.addHandler(file_handler)
root_logger.addHandler(console_handler)
root_logger.setLevel(logging.INFO)
# Explicitly set log levels for our modules
logging.getLogger("agent_framework_ag_ui").setLevel(logging.INFO)
logging.getLogger("agent_framework").setLevel(logging.INFO)
logger = logging.getLogger(__name__)
logger.info(f"AG-UI Examples Server starting... Logs writing to: {log_file}")
app = FastAPI(title="Agent Framework AG-UI Example Server")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Agentic Chat - basic chat agent
add_agent_framework_fastapi_endpoint(
app=app,
agent=simple_agent,
path="/agentic_chat",
)
# Backend Tool Rendering - agent with tools
add_agent_framework_fastapi_endpoint(
app=app,
agent=weather_agent,
path="/backend_tool_rendering",
)
# Shared State - recipe agent with structured output
add_agent_framework_fastapi_endpoint(
app=app,
agent=recipe_agent,
path="/shared_state",
)
# Predictive State Updates - document writer with predictive state
add_agent_framework_fastapi_endpoint(
app=app,
agent=document_writer_agent,
path="/predictive_state_updates",
)
# Human in the Loop - human-in-the-loop agent with step customization
add_agent_framework_fastapi_endpoint(
app=app,
agent=human_in_the_loop_agent,
path="/human_in_the_loop",
state_schema={"steps": {"type": "array"}},
predict_state_config={"steps": {"tool": "generate_task_steps", "tool_argument": "steps"}},
)
# Agentic Generative UI - task steps agent with streaming state updates
add_agent_framework_fastapi_endpoint(
app=app,
agent=task_steps_agent, # type: ignore[arg-type]
path="/agentic_generative_ui",
)
# Tool-based Generative UI - UI generator with frontend-rendered tools
add_agent_framework_fastapi_endpoint(
app=app,
agent=ui_generator_agent,
path="/tool_based_generative_ui",
)
def main():
"""Run the server."""
port = int(os.getenv("PORT", "8888"))
host = os.getenv("HOST", "127.0.0.1")
# Use log_config=None to prevent uvicorn from reconfiguring logging
# This preserves our file + console logging setup
uvicorn.run(
app,
host=host,
port=port,
log_config=None,
)
if __name__ == "__main__":
main()