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
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:
@@ -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.
|
||||
"""
|
||||
Reference in New Issue
Block a user