chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
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

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,78 @@
# Chat Client Examples
This folder contains examples for direct chat client usage patterns.
## Examples
| File | Description |
|------|-------------|
| [`built_in_chat_clients.py`](built_in_chat_clients.py) | Consolidated sample for built-in chat clients. Uses `get_client()` to create the selected client and pass it to `main()`. |
| [`chat_response_cancellation.py`](chat_response_cancellation.py) | Demonstrates how to cancel chat responses during streaming, showing proper cancellation handling and cleanup. |
| [`custom_chat_client.py`](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. |
| [`require_per_service_call_history_persistence.py`](require_per_service_call_history_persistence.py) | Compares two otherwise identical `FoundryChatClient` agents with `store=False`; the only difference is whether `require_per_service_call_history_persistence` is enabled, and only the run without it stores the synthesized tool result when middleware terminates the loop early. |
## Selecting a built-in client
`built_in_chat_clients.py` starts with:
```python
asyncio.run(main("openai_responses"))
```
Change the argument to pick a client:
- `openai_responses`
- `openai_chat_completion`
- `anthropic`
- `ollama`
- `bedrock`
- `azure_openai_responses`
- `azure_openai_chat_completion`
- `foundry_chat`
Example:
```bash
uv run samples/02-agents/chat_client/built_in_chat_clients.py
```
The `require_per_service_call_history_persistence.py` sample uses `FoundryChatClient`, so set the usual Foundry settings first and sign in with the Azure CLI:
```bash
export FOUNDRY_PROJECT_ENDPOINT="https://<your-project>.services.ai.azure.com/api/projects/<project-name>"
export FOUNDRY_MODEL="<your-model-deployment-name>"
az login
uv run samples/02-agents/chat_client/require_per_service_call_history_persistence.py
```
## Environment Variables
Depending on the selected client, set the appropriate environment variables:
**For Azure OpenAI clients (`azure_openai_responses` and `azure_openai_chat_completion`):**
- `AZURE_OPENAI_ENDPOINT`: Your Azure OpenAI endpoint
- `AZURE_OPENAI_MODEL`: The Azure OpenAI deployment used by the sample
- `AZURE_OPENAI_API_VERSION` (optional): Azure OpenAI API version override
- `AZURE_OPENAI_API_KEY` (optional): Azure OpenAI API key if you are not using `AzureCliCredential`
**For Foundry client (`foundry_chat`):**
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
- `FOUNDRY_MODEL`: The Foundry deployment used by the sample
**For OpenAI clients:**
- `OPENAI_API_KEY`: Your OpenAI API key
- `OPENAI_CHAT_COMPLETION_MODEL`: The OpenAI model for `openai_chat_completion`
- `OPENAI_CHAT_MODEL`: The OpenAI model for `openai_responses`
**For Anthropic client (`anthropic`):**
- `ANTHROPIC_API_KEY`: Your Anthropic API key
- `ANTHROPIC_CHAT_MODEL`: The Anthropic model to use (for example, `claude-sonnet-4-5`)
**For Ollama client (`ollama`):**
- `OLLAMA_HOST`: Ollama server URL (defaults to `http://localhost:11434` if unset)
- `OLLAMA_MODEL`: Ollama model name (for example, `mistral`, `qwen2.5:8b`)
**For Bedrock client (`bedrock`):**
- `BEDROCK_CHAT_MODEL`: Bedrock model ID (for example, `anthropic.claude-3-5-sonnet-20240620-v1:0`)
- `BEDROCK_REGION`: AWS region (defaults to `us-east-1` if unset)
- AWS credentials via standard environment variables (for example, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
@@ -0,0 +1,112 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from random import randint
from typing import Annotated, Any, Literal
from agent_framework import Message, SupportsChatGetResponse, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework.openai import OpenAIChatClient, OpenAIChatCompletionClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import Field
# Load environment variables from .env file
load_dotenv()
"""
Built-in Chat Clients Example
This sample demonstrates how to run the same prompt flow against different built-in
chat clients using a single `get_client` factory.
Select one of these client names:
- openai_chat
- openai_chat_completion
- anthropic
- ollama
- bedrock
- azure_openai_chat
- azure_openai_chat_completion
- foundry_chat
"""
ClientName = Literal[
"openai_chat",
"openai_chat_completion",
"anthropic",
"ollama",
"bedrock",
"azure_openai_chat",
"azure_openai_chat_completion",
"foundry_chat",
]
# NOTE: approval_mode="never_require" is for sample brevity.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
def get_client(client_name: ClientName) -> SupportsChatGetResponse[Any]:
"""Create a built-in chat client from a name."""
from agent_framework.amazon import BedrockChatClient
from agent_framework.anthropic import AnthropicClient
from agent_framework.ollama import OllamaChatClient
if client_name == "openai_chat":
return OpenAIChatClient()
if client_name == "openai_chat_completion":
return OpenAIChatCompletionClient()
if client_name == "anthropic":
return AnthropicClient()
if client_name == "ollama":
return OllamaChatClient()
if client_name == "bedrock":
return BedrockChatClient()
if client_name == "azure_openai_chat":
return OpenAIChatClient(credential=AzureCliCredential())
if client_name == "azure_openai_chat_completion":
return OpenAIChatCompletionClient(credential=AzureCliCredential())
if client_name == "foundry_chat":
return FoundryChatClient(credential=AzureCliCredential())
raise ValueError(f"Unsupported client name: {client_name}")
async def main(client_name: ClientName = "openai_chat") -> None:
"""Run a basic prompt using a selected built-in client."""
client = get_client(client_name)
message = Message("user", contents=["What's the weather in Amsterdam and in Paris?"])
stream = os.getenv("STREAM", "false").lower() == "true"
print(f"Client: {client_name}")
print(f"User: {message.text}")
if stream:
response_stream = client.get_response([message], stream=True, options={"tools": get_weather})
print("Assistant: ", end="")
async for chunk in response_stream:
if chunk.text:
print(chunk.text, end="")
print("")
else:
print(f"Assistant: {await client.get_response([message], stream=False, options={'tools': get_weather})}")
if __name__ == "__main__":
asyncio.run(main("openai_chat"))
"""
Sample output:
User: What's the weather in Amsterdam and in Paris?
Assistant: The weather in Amsterdam is sunny with a high of 25°C.
...and in Paris it is cloudy with a high of 19°C.
"""
@@ -0,0 +1,46 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import Message
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Chat Response Cancellation Example
Demonstrates proper cancellation of streaming chat responses during execution.
Shows asyncio task cancellation and resource cleanup techniques.
"""
async def main() -> None:
"""
Demonstrates cancelling a chat request after 1 second.
Creates a task for the chat request, waits briefly, then cancels it to show proper cleanup.
Configuration:
- FOUNDRY_PROJECT_ENDPOINT: Azure AI Foundry project endpoint URL
- FOUNDRY_MODEL: Model deployment name (e.g. gpt-4o)
- Authentication: Run `az login` to authenticate via AzureCliCredential
"""
client = FoundryChatClient(credential=AzureCliCredential())
async def get_story_response() -> None:
await client.get_response(messages=[Message(role="user", contents=["Tell me a fantasy story."])])
try:
task = asyncio.create_task(get_story_response())
await asyncio.sleep(1)
task.cancel()
await task
except asyncio.CancelledError:
print("Request was cancelled")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,213 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import random
import sys
from collections.abc import AsyncIterable, Awaitable, Mapping, Sequence
from typing import Any, ClassVar, TypeAlias, TypedDict
from agent_framework import (
Agent,
BaseChatClient,
ChatMiddlewareLayer,
ChatResponse,
ChatResponseUpdate,
Content,
FunctionInvocationLayer,
InMemoryHistoryProvider,
Message,
ResponseStream,
)
from agent_framework.observability import ChatTelemetryLayer
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
from typing_extensions import override # type: ignore[import] # pragma: no cover
"""
Custom Chat Client Implementation Example
This sample demonstrates implementing a custom chat client and optionally composing
middleware, telemetry, and function invocation layers explicitly. The recommended
layer order is `FunctionInvocationLayer -> ChatMiddlewareLayer -> ChatTelemetryLayer`
so chat middleware runs within each tool-loop iteration while telemetry records
per-call spans without middleware latency.
"""
class EchoingChatClientOptions(TypedDict, total=False):
"""Custom options for EchoingChatClient."""
uppercase: bool
suffix: str
stream_delay_seconds: float
OptionsT: TypeAlias = EchoingChatClientOptions
class EchoingChatClient(BaseChatClient[OptionsT]):
"""A custom chat client that echoes messages back with modifications.
This demonstrates how to implement a custom chat client by extending BaseChatClient
and implementing the required _inner_get_response() method.
"""
OTEL_PROVIDER_NAME: ClassVar[str] = "EchoingChatClient"
def __init__(self, *, prefix: str = "Echo:", **kwargs: Any) -> None:
"""Initialize the EchoingChatClient.
Args:
prefix: Prefix to add to echoed messages.
**kwargs: Additional keyword arguments passed to BaseChatClient.
"""
super().__init__(**kwargs)
self.prefix = prefix
@override
def _inner_get_response(
self,
*,
messages: Sequence[Message],
stream: bool = False,
options: Mapping[str, Any],
**kwargs: Any,
) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]:
"""Echo back the user's message with a prefix."""
if not messages:
response_text = "No messages to echo!"
else:
# Echo the last user message
last_user_message = None
for message in reversed(messages):
if message.role == "user":
last_user_message = message
break
if last_user_message and last_user_message.text:
response_text = f"{self.prefix} {last_user_message.text}"
else:
response_text = f"{self.prefix} [No text message found]"
if options.get("uppercase"):
response_text = response_text.upper()
if suffix := options.get("suffix"):
response_text = f"{response_text} {suffix}"
stream_delay_seconds = float(options.get("stream_delay_seconds", 0.05))
response_message = Message(role="assistant", contents=[response_text])
response = ChatResponse(
messages=[response_message],
model="echo-model-v1",
response_id=f"echo-resp-{random.randint(1000, 9999)}",
)
if not stream:
async def _get_response() -> ChatResponse:
return response
return _get_response()
async def _stream() -> AsyncIterable[ChatResponseUpdate]:
response_text_local = response_message.text or ""
for char in response_text_local:
yield ChatResponseUpdate(
contents=[Content.from_text(char)],
role="assistant",
response_id=f"echo-stream-resp-{random.randint(1000, 9999)}",
model="echo-model-v1",
)
await asyncio.sleep(stream_delay_seconds)
return ResponseStream(_stream(), finalizer=lambda updates: response)
class EchoingChatClientWithLayers( # type: ignore[misc]
FunctionInvocationLayer[OptionsT],
ChatMiddlewareLayer[OptionsT],
ChatTelemetryLayer[OptionsT],
EchoingChatClient,
):
"""Echoing chat client that explicitly composes middleware, telemetry, and function layers."""
OTEL_PROVIDER_NAME: ClassVar[str] = "EchoingChatClientWithLayers"
async def main() -> None:
"""Demonstrates how to implement and use a custom chat client with Agent."""
print("=== Custom Chat Client Example ===\n")
# Create the custom chat client
print("--- EchoingChatClient Example ---")
echo_client = EchoingChatClientWithLayers(prefix="🔊 Echo:")
# Use the chat client directly
print("Using chat client directly:")
direct_response = await echo_client.get_response(
[Message(role="user", contents=["Hello, custom chat client!"])],
options={
"uppercase": True,
"suffix": "(CUSTOM OPTIONS)",
"stream_delay_seconds": 0.02,
},
)
print(f"Direct response: {direct_response.messages[0].text}")
# Create an agent using the custom chat client
echo_agent = Agent(
client=echo_client,
name="EchoAgent",
instructions="You are a helpful assistant that echoes back what users say.",
)
print(f"\nAgent Name: {echo_agent.name}")
# Test non-streaming with agent
query = "This is a test message"
print(f"\nUser: {query}")
result = await echo_agent.run(query)
print(f"Agent: {result.messages[0].text}")
# Test streaming with agent
query2 = "Stream this message back to me"
print(f"\nUser: {query2}")
print("Agent: ", end="", flush=True)
async for chunk in echo_agent.run(query2, stream=True):
if chunk.text:
print(chunk.text, end="", flush=True)
print()
# Example: Using with sessions and conversation history
print("\n--- Using Custom Chat Client with Session ---")
session = echo_agent.create_session()
# Multiple messages in conversation
messages = [
"Hello, I'm starting a conversation",
"How are you doing?",
"Thanks for chatting!",
]
for msg in messages:
result = await echo_agent.run(msg, session=session)
print(f"User: {msg}")
print(f"Agent: {result.messages[0].text}\n")
# Check conversation history
memory_state = session.state.get(InMemoryHistoryProvider.DEFAULT_SOURCE_ID, {})
session_messages = memory_state.get("messages", [])
if session_messages:
print(f"Session contains {len(session_messages)} messages")
else:
print("Session has no messages stored")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,194 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
from typing import Annotated
from agent_framework import (
Agent,
FunctionInvocationContext,
FunctionMiddleware,
InMemoryHistoryProvider,
Message,
MiddlewareTermination,
)
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import Field
"""
Compare Foundry agents with and without per-service-call chat history persistence.
This sample runs two otherwise identical Foundry agents with ``store=False`` so
history stays local for both runs.
The sample adds a function middleware that raises ``MiddlewareTermination``
immediately after the tool runs, so the request stops before a second model
call.
That early termination is the important difference:
- Without per-service-call chat history persistence, the synthesized tool result is
still written to local history.
- With ``require_per_service_call_history_persistence=True``, that synthesized tool result is
not written to local history.
The per-service-call persistence case matches service-side storage behavior. When a terminated
request never sends the tool result back to the service, that result also never
becomes part of the service-managed history.
"""
# Load environment variables from .env file
load_dotenv()
def lookup_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Return a deterministic weather result for the requested location."""
return f"The weather in {location} is sunny."
class TerminateAfterToolMiddleware(FunctionMiddleware):
"""Stop the tool loop after the first tool finishes."""
async def process(
self,
context: FunctionInvocationContext,
call_next: Callable[[], Awaitable[None]],
) -> None:
"""Run the tool, then terminate the loop with that tool result."""
await call_next()
raise MiddlewareTermination(result=context.result)
def _describe_message(message: Message) -> str:
"""Render one stored message in a compact, readable format."""
parts: list[str] = []
for content in message.contents:
if content.type == "text" and content.text:
parts.append(content.text)
elif content.type == "function_call":
parts.append(f"function_call -> {content.name}({content.arguments})")
elif content.type == "function_result":
parts.append(f"function_result -> {content.result}")
else:
parts.append(content.type)
return f"{message.role}: {' | '.join(parts)}"
def _includes_tool_result(messages: list[Message]) -> bool:
"""Return whether any stored message contains a tool result."""
return any(content.type == "function_result" for message in messages for content in message.contents)
async def main() -> None:
"""Run both comparison scenarios."""
print("=== require_per_service_call_history_persistence when middleware terminates the tool loop ===\n")
# 1. Create one Foundry chat client that both agents will share.
client = FoundryChatClient(credential=AzureCliCredential())
query = "What is the weather in Seattle, and should I bring sunglasses?"
# 2. Create and run the agent without per-service-call persistence.
agent_without_persistence = Agent(
client=client,
instructions=(
"You are a weather assistant. Call lookup_weather exactly once before answering "
"any weather question, then summarize the tool result in one short paragraph."
),
tools=[lookup_weather],
context_providers=[InMemoryHistoryProvider()],
middleware=[TerminateAfterToolMiddleware()],
default_options={"tool_choice": "required", "store": False},
)
session_without_persistence = agent_without_persistence.create_session()
await agent_without_persistence.run(
query,
session=session_without_persistence,
)
stored_messages_without_persistence = session_without_persistence.state[InMemoryHistoryProvider.DEFAULT_SOURCE_ID][
"messages"
]
print("=== Without per-service-call persistence ===")
print("Loop terminated immediately after the tool finished.")
print(f"Stored synthesized tool result: {_includes_tool_result(stored_messages_without_persistence)}")
print("Stored history:")
for index, message in enumerate(stored_messages_without_persistence, start=1):
print(f" {index}. {_describe_message(message)}")
print()
# 3. Create and run the agent with per-service-call persistence enabled.
agent_with_persistence = Agent(
client=client,
instructions=(
"You are a weather assistant. Call lookup_weather exactly once before answering "
"any weather question, then summarize the tool result in one short paragraph."
),
tools=[lookup_weather],
context_providers=[InMemoryHistoryProvider()],
middleware=[TerminateAfterToolMiddleware()],
require_per_service_call_history_persistence=True,
default_options={"tool_choice": "required", "store": False},
)
session_with_persistence = agent_with_persistence.create_session()
await agent_with_persistence.run(
query,
session=session_with_persistence,
)
stored_messages_with_persistence = session_with_persistence.state[InMemoryHistoryProvider.DEFAULT_SOURCE_ID][
"messages"
]
print("=== With per-service-call persistence ===")
print("Loop terminated immediately after the tool finished.")
print(f"Stored synthesized tool result: {_includes_tool_result(stored_messages_with_persistence)}")
print("Stored history:")
for index, message in enumerate(stored_messages_with_persistence, start=1):
print(f" {index}. {_describe_message(message)}")
print()
# 4. Summarize the effect of the flag.
print(
"Both runs used FoundryChatClient with store=False and terminated right after the tool. "
"Without per-service-call persistence, local history still stored the synthesized tool result. "
"With per-service-call persistence, local history stopped at the assistant function-call message instead, "
"which matches service-side storage because the terminated tool result is never sent back to the service."
)
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
=== require_per_service_call_history_persistence when middleware terminates the tool loop ===
=== Without per-service-call persistence ===
Loop terminated immediately after the tool finished.
Stored synthesized tool result: True
Stored history:
1. user: What is the weather in Seattle, and should I bring sunglasses?
2. assistant: function_call -> lookup_weather({"location":"Seattle"})
3. tool: function_result -> The weather in Seattle is sunny.
=== With per-service-call persistence ===
Loop terminated immediately after the tool finished.
Stored synthesized tool result: False
Stored history:
1. user: What is the weather in Seattle, and should I bring sunglasses?
2. assistant: function_call -> lookup_weather({"location":"Seattle"})
Both runs used FoundryChatClient with store=False and terminated right after
the tool. Without per-service-call persistence, local history still stored the
synthesized tool result. With per-service-call persistence, local history
stopped at the assistant function-call message instead, which matches
service-side storage because the terminated tool result is never sent back to
the service.
"""