mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
0c862e97a6
commit
35a8565495
@@ -0,0 +1,3 @@
|
||||
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
|
||||
AZURE_OPENAI_API_KEY=your-api-key-here
|
||||
PORT=8000
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"python.analysis.extraPaths": [
|
||||
"${workspaceFolder}/packages/ag-ui/examples"
|
||||
]
|
||||
}
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user