chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,69 @@
# Custom Agent and Chat Client Examples
This folder contains examples demonstrating how to implement custom agents and chat clients using the Microsoft Agent Framework.
## Examples
| File | Description |
|------|-------------|
| [`custom_agent.py`](custom_agent.py) | Shows how to create custom agents by extending the `BaseAgent` class. Demonstrates the `EchoAgent` implementation with both streaming and non-streaming responses, proper session management, and message history handling. |
| [`custom_chat_client.py`](../../chat_client/custom_chat_client.py) | Demonstrates how to create custom chat clients by extending the `BaseChatClient` class. Shows a `EchoingChatClient` implementation and how to integrate it with `Agent` using the `as_agent()` method. |
## Key Takeaways
### Custom Agents
- Custom agents give you complete control over the agent's behavior
- You must implement both `run()` for both the `stream=True` and `stream=False` cases
- Use `self._normalize_messages()` to handle different input message formats
- Store messages in `session.state` to properly manage conversation history
### Custom Chat Clients
- Custom chat clients allow you to integrate any backend service or create new LLM providers
- You must implement `_inner_get_response()` with a stream parameter to handle both streaming and non-streaming responses
- Custom chat clients can be used with `Agent` to leverage all agent framework features
- Use the `as_agent()` method to easily create agents from your custom chat clients
Both approaches allow you to extend the framework for your specific use cases while maintaining compatibility with the broader Agent Framework ecosystem.
## Understanding Raw Client Classes
The framework provides `Raw...Client` classes (e.g., `RawOpenAIChatClient`, `RawOpenAIChatCompletionClient`, `RawFoundryChatClient`) that are intermediate implementations without middleware, telemetry, or function invocation support.
### Warning: Raw Clients Should Not Normally Be Used Directly
**The `Raw...Client` classes should not normally be used directly.** They do not include the middleware, telemetry, or function invocation support that you most likely need. If you do use them, you should carefully consider which additional layers to apply.
### Layer Ordering
There is a defined ordering for applying layers that you should follow:
1. **FunctionInvocationLayer** - Handles the tool/function calling loop and should stay outermost
2. **ChatMiddlewareLayer** - Wraps each model call in the loop and stays outside telemetry
3. **ChatTelemetryLayer** - Must be inside the function calling loop so each model call gets its own telemetry span
4. **Raw...Client** - The base implementation (e.g., `RawOpenAIChatClient`)
Example of correct layer composition:
```python
class MyCustomClient(
FunctionInvocationLayer[TOptions],
ChatMiddlewareLayer[TOptions],
ChatTelemetryLayer[TOptions],
RawOpenAIChatClient[TOptions], # or BaseChatClient for custom implementations
Generic[TOptions],
):
"""Custom client with all layers correctly applied."""
pass
```
### Use Fully-Featured Clients Instead
For most use cases, use the fully-featured public client classes which already have all layers correctly composed:
- `OpenAIChatCompletionClient` - OpenAI Chat Completions API with all layers
- `OpenAIChatClient` - OpenAI Responses API with all layers
- `OpenAIChatCompletionClient` - Azure OpenAI Chat Completions with all layers
- `OpenAIChatClient` - Azure OpenAI Responses with all layers
- `FoundryChatClient` - Azure AI Foundry project-backed chat with all layers
These clients handle the layer composition correctly and provide the full feature set out of the box.
@@ -0,0 +1,235 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from collections.abc import AsyncIterable, Awaitable
from typing import Any, Literal, overload
from agent_framework import (
AgentResponse,
AgentResponseUpdate,
AgentSession,
BaseAgent,
Content,
InMemoryHistoryProvider,
Message,
normalize_messages,
)
"""
Custom Agent Implementation Example
This sample demonstrates implementing a custom agent by extending BaseAgent class,
showing the minimal requirements for both streaming and non-streaming responses.
"""
class EchoAgent(BaseAgent):
"""A simple custom agent that echoes user messages with a prefix.
This demonstrates how to create a fully custom agent by extending BaseAgent
and implementing the required run() method with stream support.
"""
echo_prefix: str = "Echo: "
def __init__(
self,
*,
name: str | None = None,
description: str | None = None,
echo_prefix: str = "Echo: ",
**kwargs: Any,
) -> None:
"""Initialize the EchoAgent.
Args:
name: The name of the agent.
description: The description of the agent.
echo_prefix: The prefix to add to echoed messages.
**kwargs: Additional keyword arguments passed to BaseAgent.
"""
super().__init__(
name=name,
description=description,
**kwargs,
)
self.echo_prefix = echo_prefix
@overload
def run(
self,
messages: str | Message | list[str] | list[Message] | None = None,
*,
stream: Literal[False] = False,
session: AgentSession | None = None,
**kwargs: Any,
) -> asyncio.Future[AgentResponse]: ...
@overload
def run(
self,
messages: str | Message | list[str] | list[Message] | None = None,
*,
stream: Literal[True],
session: AgentSession | None = None,
**kwargs: Any,
) -> AsyncIterable[AgentResponseUpdate]: ...
def run(
self,
messages: str | Message | list[str] | list[Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
**kwargs: Any,
) -> "AsyncIterable[AgentResponseUpdate] | Awaitable[AgentResponse]":
"""Execute the agent and return a response.
Args:
messages: The message(s) to process.
stream: If True, return an async iterable of updates. If False, return an awaitable response.
session: The conversation session (optional).
**kwargs: Additional keyword arguments.
Returns:
When stream=False: An awaitable AgentResponse containing the agent's reply.
When stream=True: An async iterable of AgentResponseUpdate objects.
"""
if stream:
return self._run_stream(messages=messages, session=session, **kwargs)
return self._run(messages=messages, session=session, **kwargs)
async def _run(
self,
messages: str | Message | list[str] | list[Message] | None = None,
*,
session: AgentSession | None = None,
**kwargs: Any,
) -> AgentResponse:
"""Non-streaming implementation."""
# Normalize input messages to a list
normalized_messages = normalize_messages(messages)
if not normalized_messages:
response_message = Message(
role="assistant",
contents=[
Content.from_text(text="Hello! I'm a custom echo agent. Send me a message and I'll echo it back.")
],
)
else:
# For simplicity, echo the last user message
last_message = normalized_messages[-1]
if last_message.text:
echo_text = f"{self.echo_prefix}{last_message.text}"
else:
echo_text = f"{self.echo_prefix}[Non-text message received]"
response_message = Message(role="assistant", contents=[Content.from_text(text=echo_text)])
# Store messages in session state if provided
if session is not None:
stored = session.state.setdefault(InMemoryHistoryProvider.DEFAULT_SOURCE_ID, {}).setdefault("messages", [])
stored.extend(normalized_messages)
stored.append(response_message)
return AgentResponse(messages=[response_message])
async def _run_stream(
self,
messages: str | Message | list[str] | list[Message] | None = None,
*,
session: AgentSession | None = None,
**kwargs: Any,
) -> AsyncIterable[AgentResponseUpdate]:
"""Streaming implementation."""
# Normalize input messages to a list
normalized_messages = normalize_messages(messages)
if not normalized_messages:
response_text = "Hello! I'm a custom echo agent. Send me a message and I'll echo it back."
else:
# For simplicity, echo the last user message
last_message = normalized_messages[-1]
if last_message.text:
response_text = f"{self.echo_prefix}{last_message.text}"
else:
response_text = f"{self.echo_prefix}[Non-text message received]"
# Simulate streaming by yielding the response word by word
words = response_text.split()
for i, word in enumerate(words):
# Add space before word except for the first one
chunk_text = f" {word}" if i > 0 else word
yield AgentResponseUpdate(
contents=[Content.from_text(text=chunk_text)],
role="assistant",
)
# Small delay to simulate streaming
await asyncio.sleep(0.1)
# Store messages in session state if provided
if session is not None:
complete_response = Message(role="assistant", contents=[Content.from_text(text=response_text)])
stored = session.state.setdefault(InMemoryHistoryProvider.DEFAULT_SOURCE_ID, {}).setdefault("messages", [])
stored.extend(normalized_messages)
stored.append(complete_response)
async def main() -> None:
"""Demonstrates how to use the custom EchoAgent."""
print("=== Custom Agent Example ===\n")
# Create EchoAgent
print("--- EchoAgent Example ---")
echo_agent = EchoAgent(
name="EchoBot", description="A simple agent that echoes messages with a prefix", echo_prefix="🔊 Echo: "
)
# Test non-streaming
print(f"Agent Name: {echo_agent.name}")
print(f"Agent ID: {echo_agent.id}")
query = "Hello, custom agent!"
print(f"\nUser: {query}")
result = await echo_agent.run(query)
print(f"Agent: {result.messages[0].text}")
# Test streaming
query2 = "This is a streaming test"
print(f"\nUser: {query2}")
print("Agent: ", end="", flush=True)
stream = echo_agent.run(query2, stream=True)
assert isinstance(stream, AsyncIterable)
async for chunk in stream:
if chunk.text:
print(chunk.text, end="", flush=True)
print()
# Example with sessions
print("\n--- Using Custom Agent with Session ---")
session = echo_agent.create_session()
# First message
result1 = await echo_agent.run("First message", session=session)
print("User: First message")
print(f"Agent: {result1.messages[0].text}")
# Second message in same thread
result2 = await echo_agent.run("Second message", session=session)
print("User: Second message")
print(f"Agent: {result2.messages[0].text}")
# Check conversation history
memory_state = session.state.get(InMemoryHistoryProvider.DEFAULT_SOURCE_ID, {})
messages = memory_state.get("messages", [])
if messages:
print(f"\nSession contains {len(messages)} messages in history")
else:
print("\nSession has no messages stored")
if __name__ == "__main__":
asyncio.run(main())