mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [BREAKING]: Introducing Options as TypedDict and Generic (#3140)
* WIP typeddict for options * updated all clients and ChatAgents * updated everything * added ADR * fix mypy * proper typevar imports * fixed import * fixed other imports * slight update in the sample * updated from feedback * fixes * fixed missing covariants and test fixes * fixed typing * updated anthropic thinking config * ruff fixes * fixed int tests * fix tests and mypy * updated integration tests * updated docstring and test fix * improved options handling in obser * mypy fix * updated a host of integration tests * fix tests * bedrock fix
This commit is contained in:
committed by
GitHub
Unverified
parent
5faa2851bb
commit
3e97425245
@@ -289,8 +289,10 @@ class WeatherChatKitServer(ChatKitServer[dict[str, Any]]):
|
||||
# Use the chat client directly for a quick, lightweight call
|
||||
response = await self.weather_agent.chat_client.get_response(
|
||||
messages=title_prompt,
|
||||
temperature=0.3,
|
||||
max_tokens=20,
|
||||
options={
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 20,
|
||||
},
|
||||
)
|
||||
|
||||
if response.messages and response.messages[-1].text:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
|
||||
from agent_framework import HostedMCPTool, HostedWebSearchTool, TextReasoningContent, UsageContent
|
||||
from agent_framework.anthropic import AnthropicClient
|
||||
from agent_framework.anthropic import AnthropicChatOptions, AnthropicClient
|
||||
|
||||
"""
|
||||
Anthropic Chat Agent Example
|
||||
@@ -15,9 +15,9 @@ This sample demonstrates using Anthropic with:
|
||||
"""
|
||||
|
||||
|
||||
async def streaming_example() -> None:
|
||||
async def main() -> None:
|
||||
"""Example of streaming response (get results as they are generated)."""
|
||||
agent = AnthropicClient().create_agent(
|
||||
agent = AnthropicClient[AnthropicChatOptions]().create_agent(
|
||||
name="DocsAgent",
|
||||
instructions="You are a helpful agent for both Microsoft docs questions and general questions.",
|
||||
tools=[
|
||||
@@ -27,10 +27,12 @@ async def streaming_example() -> None:
|
||||
),
|
||||
HostedWebSearchTool(),
|
||||
],
|
||||
# anthropic needs a value for the max_tokens parameter
|
||||
# we set it to 1024, but you can override like this:
|
||||
max_tokens=20000,
|
||||
additional_chat_options={"thinking": {"type": "enabled", "budget_tokens": 10000}},
|
||||
default_options={
|
||||
# anthropic needs a value for the max_tokens parameter
|
||||
# we set it to 1024, but you can override like this:
|
||||
"max_tokens": 20000,
|
||||
"thinking": {"type": "enabled", "budget_tokens": 10000},
|
||||
},
|
||||
)
|
||||
|
||||
query = "Can you compare Python decorators with C# attributes?"
|
||||
@@ -48,11 +50,5 @@ async def streaming_example() -> None:
|
||||
print("\n")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=== Anthropic Example ===")
|
||||
|
||||
await streaming_example()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -38,10 +38,12 @@ async def main() -> None:
|
||||
),
|
||||
HostedWebSearchTool(),
|
||||
],
|
||||
# anthropic needs a value for the max_tokens parameter
|
||||
# we set it to 1024, but you can override like this:
|
||||
max_tokens=20000,
|
||||
additional_chat_options={"thinking": {"type": "enabled", "budget_tokens": 10000}},
|
||||
default_options={
|
||||
# anthropic needs a value for the max_tokens parameter
|
||||
# we set it to 1024, but you can override like this:
|
||||
"max_tokens": 20000,
|
||||
"thinking": {"type": "enabled", "budget_tokens": 10000},
|
||||
},
|
||||
)
|
||||
|
||||
query = "Can you compare Python decorators with C# attributes?"
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
from pathlib import Path
|
||||
|
||||
from agent_framework import HostedCodeInterpreterTool, HostedFileContent
|
||||
from agent_framework.anthropic import AnthropicClient
|
||||
from agent_framework.anthropic import AnthropicChatOptions, AnthropicClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
"""
|
||||
@@ -22,7 +22,7 @@ This sample demonstrates using Anthropic with:
|
||||
|
||||
async def main() -> None:
|
||||
"""Example of streaming response (get results as they are generated)."""
|
||||
client = AnthropicClient(additional_beta_flags=["skills-2025-10-02"])
|
||||
client = AnthropicClient[AnthropicChatOptions](additional_beta_flags=["skills-2025-10-02"])
|
||||
|
||||
# List Anthropic-managed Skills
|
||||
skills = await client.anthropic_client.beta.skills.list(source="anthropic", betas=["skills-2025-10-02"])
|
||||
@@ -35,8 +35,8 @@ async def main() -> None:
|
||||
name="DocsAgent",
|
||||
instructions="You are a helpful agent for creating powerpoint presentations.",
|
||||
tools=HostedCodeInterpreterTool(),
|
||||
max_tokens=20000,
|
||||
additional_chat_options={
|
||||
default_options={
|
||||
"max_tokens": 20000,
|
||||
"thinking": {"type": "enabled", "budget_tokens": 10000},
|
||||
"container": {"skills": [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}]},
|
||||
},
|
||||
|
||||
@@ -44,7 +44,7 @@ async def main() -> None:
|
||||
result = await agent.run(
|
||||
query,
|
||||
# These additional options are required for image generation
|
||||
additional_chat_options={
|
||||
options={
|
||||
"extra_headers": {"x-ms-oai-image-generation-deployment": "gpt-image-1-mini"},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ async def main() -> None:
|
||||
result = await agent.run(
|
||||
query,
|
||||
# Specify type to use as response
|
||||
additional_chat_options={
|
||||
options={
|
||||
"response_format": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import sys
|
||||
from collections.abc import AsyncIterable, MutableSequence
|
||||
from typing import Any, ClassVar
|
||||
from typing import Any, ClassVar, Generic
|
||||
|
||||
from agent_framework import (
|
||||
BaseChatClient,
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Role,
|
||||
@@ -16,6 +16,12 @@ from agent_framework import (
|
||||
use_chat_middleware,
|
||||
use_function_invocation,
|
||||
)
|
||||
from agent_framework._clients import TOptions_co
|
||||
|
||||
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
|
||||
@@ -27,7 +33,7 @@ showing integration with ChatAgent and both streaming and non-streaming response
|
||||
|
||||
@use_function_invocation
|
||||
@use_chat_middleware
|
||||
class EchoingChatClient(BaseChatClient):
|
||||
class EchoingChatClient(BaseChatClient[TOptions_co], Generic[TOptions_co]):
|
||||
"""A custom chat client that echoes messages back with modifications.
|
||||
|
||||
This demonstrates how to implement a custom chat client by extending BaseChatClient
|
||||
@@ -46,11 +52,12 @@ class EchoingChatClient(BaseChatClient):
|
||||
super().__init__(**kwargs)
|
||||
self.prefix = prefix
|
||||
|
||||
@override
|
||||
async def _inner_get_response(
|
||||
self,
|
||||
*,
|
||||
messages: MutableSequence[ChatMessage],
|
||||
chat_options: ChatOptions,
|
||||
options: dict[str, Any],
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
"""Echo back the user's message with a prefix."""
|
||||
@@ -77,16 +84,17 @@ class EchoingChatClient(BaseChatClient):
|
||||
response_id=f"echo-resp-{random.randint(1000, 9999)}",
|
||||
)
|
||||
|
||||
@override
|
||||
async def _inner_get_streaming_response(
|
||||
self,
|
||||
*,
|
||||
messages: MutableSequence[ChatMessage],
|
||||
chat_options: ChatOptions,
|
||||
options: dict[str, Any],
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
"""Stream back the echoed message character by character."""
|
||||
# Get the complete response first
|
||||
response = await self._inner_get_response(messages=messages, chat_options=chat_options, **kwargs)
|
||||
response = await self._inner_get_response(messages=messages, options=options, **kwargs)
|
||||
|
||||
if response.messages:
|
||||
response_text = response.messages[0].text or ""
|
||||
|
||||
@@ -24,7 +24,7 @@ async def reasoning_example() -> None:
|
||||
agent = OllamaChatClient().create_agent(
|
||||
name="TimeAgent",
|
||||
instructions="You are a helpful agent answer in one sentence.",
|
||||
additional_chat_options={"think": True}, # Enable Reasoning on agent level
|
||||
default_options={"think": True}, # Enable Reasoning on agent level
|
||||
)
|
||||
query = "Hey what is 3+4? Can you explain how you got to that answer?"
|
||||
print(f"User: {query}")
|
||||
|
||||
+4
-4
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.openai import OpenAIChatClient, OpenAIChatOptions
|
||||
|
||||
"""
|
||||
OpenAI Chat Client Runtime JSON Schema Example
|
||||
@@ -32,7 +32,7 @@ runtime_schema = {
|
||||
async def non_streaming_example() -> None:
|
||||
print("=== Non-streaming runtime JSON schema example ===")
|
||||
|
||||
agent = OpenAIChatClient().create_agent(
|
||||
agent = OpenAIChatClient[OpenAIChatOptions]().create_agent(
|
||||
name="RuntimeSchemaAgent",
|
||||
instructions="Return only JSON that matches the provided schema. Do not add commentary.",
|
||||
)
|
||||
@@ -42,7 +42,7 @@ async def non_streaming_example() -> None:
|
||||
|
||||
response = await agent.run(
|
||||
query,
|
||||
additional_chat_options={
|
||||
options={
|
||||
"response_format": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
@@ -76,7 +76,7 @@ async def streaming_example() -> None:
|
||||
chunks: list[str] = []
|
||||
async for chunk in agent.run_stream(
|
||||
query,
|
||||
additional_chat_options={
|
||||
options={
|
||||
"response_format": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
from agent_framework.openai import OpenAIResponsesClient, OpenAIResponsesOptions
|
||||
|
||||
"""
|
||||
OpenAI Responses Client Reasoning Example
|
||||
@@ -10,19 +10,20 @@ OpenAI Responses Client Reasoning Example
|
||||
This sample demonstrates advanced reasoning capabilities using OpenAI's gpt-5 models,
|
||||
showing step-by-step reasoning process visualization and complex problem-solving.
|
||||
|
||||
This uses the additional_chat_options parameter to enable reasoning with high effort and detailed summaries.
|
||||
You can also set these options at the run level, since they are api and/or provider specific, you will need to lookup
|
||||
the correct values for your provider, since these are passed through as-is.
|
||||
This uses the default_options parameter to enable reasoning with high effort and detailed summaries.
|
||||
You can also set these options at the run level using the options parameter.
|
||||
Since these are api and/or provider specific, you will need to lookup
|
||||
the correct values for your provider, as they are passed through as-is.
|
||||
|
||||
In this case they are here: https://platform.openai.com/docs/api-reference/responses/create#responses-create-reasoning
|
||||
"""
|
||||
|
||||
|
||||
agent = OpenAIResponsesClient(model_id="gpt-5").create_agent(
|
||||
agent = OpenAIResponsesClient[OpenAIResponsesOptions](model_id="gpt-5").create_agent(
|
||||
name="MathHelper",
|
||||
instructions="You are a personal math tutor. When asked a math question, "
|
||||
"reason over how best to approach the problem and share your thought process.",
|
||||
additional_chat_options={"reasoning": {"effort": "high", "summary": "detailed"}},
|
||||
default_options={"reasoning": {"effort": "high", "summary": "detailed"}},
|
||||
)
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -42,7 +42,7 @@ async def non_streaming_example() -> None:
|
||||
|
||||
response = await agent.run(
|
||||
query,
|
||||
additional_chat_options={
|
||||
options={
|
||||
"response_format": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
@@ -76,7 +76,7 @@ async def streaming_example() -> None:
|
||||
chunks: list[str] = []
|
||||
async for chunk in agent.run_stream(
|
||||
query,
|
||||
additional_chat_options={
|
||||
options={
|
||||
"response_format": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Literal
|
||||
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework.anthropic import AnthropicClient
|
||||
from agent_framework.openai import OpenAIChatClient, OpenAIChatOptions
|
||||
|
||||
"""TypedDict-based Chat Options.
|
||||
|
||||
In Agent Framework, we have made ChatClient and ChatAgent generic over a ChatOptions typeddict, this means that
|
||||
you can override which options are available for a given client or agent by providing your own TypedDict subclass.
|
||||
And we include the most common options for all ChatClient providers out of the box.
|
||||
|
||||
This sample demonstrates the TypedDict-based approach for chat client and agent options,
|
||||
which provides:
|
||||
1. IDE autocomplete for available options
|
||||
2. Type checking to catch errors at development time
|
||||
3. An example of defining provider-specific options by extending the base options,
|
||||
including overriding unsupported options.
|
||||
|
||||
The sample shows usage with both OpenAI and Anthropic clients, demonstrating
|
||||
how provider-specific options work for ChatClient and ChatAgent. But the same approach works for other providers too.
|
||||
"""
|
||||
|
||||
|
||||
async def demo_anthropic_chat_client() -> None:
|
||||
"""Demonstrate Anthropic ChatClient with typed options and validation."""
|
||||
print("\n=== Anthropic ChatClient with TypedDict Options ===\n")
|
||||
|
||||
# Create Anthropic client
|
||||
client = AnthropicClient(model_id="claude-sonnet-4-5-20250929")
|
||||
|
||||
# Standard options work great:
|
||||
response = await client.get_response(
|
||||
"What is the capital of France?",
|
||||
options={
|
||||
"temperature": 0.5,
|
||||
"max_tokens": 1000,
|
||||
# Anthropic-specific options:
|
||||
"thinking": {"type": "enabled", "budget_tokens": 1000},
|
||||
# "top_k": 40, # <-- Uncomment for Anthropic-specific option
|
||||
},
|
||||
)
|
||||
|
||||
print(f"Anthropic Response: {response.text}")
|
||||
print(f"Model used: {response.model_id}")
|
||||
|
||||
|
||||
async def demo_anthropic_agent() -> None:
|
||||
"""Demonstrate ChatAgent with Anthropic client and typed options."""
|
||||
print("\n=== ChatAgent with Anthropic and Typed Options ===\n")
|
||||
|
||||
client = AnthropicClient(model_id="claude-sonnet-4-5-20250929")
|
||||
|
||||
# Create a typed agent for Anthropic - IDE knows Anthropic-specific options!
|
||||
agent = ChatAgent(
|
||||
chat_client=client,
|
||||
name="claude-assistant",
|
||||
instructions="You are a helpful assistant powered by Claude. Be concise.",
|
||||
default_options={
|
||||
"temperature": 0.5,
|
||||
"max_tokens": 200,
|
||||
"top_k": 40, # Anthropic-specific option, uncomment to try
|
||||
},
|
||||
)
|
||||
|
||||
# Run the agent
|
||||
response = await agent.run("Explain quantum computing in one sentence.")
|
||||
|
||||
print(f"Agent Response: {response.text}")
|
||||
|
||||
|
||||
class OpenAIReasoningChatOptions(OpenAIChatOptions, total=False):
|
||||
"""Chat options for OpenAI reasoning models (o1, o3, o4-mini, etc.).
|
||||
|
||||
Reasoning models have different parameter support compared to standard models.
|
||||
This TypedDict marks unsupported parameters with ``None`` type.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.openai import OpenAIReasoningChatOptions
|
||||
|
||||
options: OpenAIReasoningChatOptions = {
|
||||
"model_id": "o3",
|
||||
"reasoning_effort": "high",
|
||||
"max_tokens": 4096,
|
||||
}
|
||||
"""
|
||||
|
||||
# Reasoning-specific parameters
|
||||
reasoning_effort: Literal["none", "minimal", "low", "medium", "high", "xhigh"]
|
||||
|
||||
# Unsupported parameters for reasoning models (override with None)
|
||||
temperature: None
|
||||
top_p: None
|
||||
frequency_penalty: None
|
||||
presence_penalty: None
|
||||
logit_bias: None
|
||||
logprobs: None
|
||||
top_logprobs: None
|
||||
stop: None # Not supported for o3 and o4-mini
|
||||
|
||||
|
||||
async def demo_openai_chat_client_reasoning_models() -> None:
|
||||
"""Demonstrate OpenAI ChatClient with typed options for reasoning models."""
|
||||
print("\n=== OpenAI ChatClient with TypedDict Options ===\n")
|
||||
|
||||
# Create OpenAI client
|
||||
client = OpenAIChatClient[OpenAIReasoningChatOptions]()
|
||||
|
||||
# With specific options, you get full IDE autocomplete!
|
||||
# Try typing `client.get_response("Hello", options={` and see the suggestions
|
||||
response = await client.get_response(
|
||||
"What is 2 + 2?",
|
||||
options={
|
||||
"model_id": "o3",
|
||||
"max_tokens": 100,
|
||||
"allow_multiple_tool_calls": True,
|
||||
# OpenAI-specific options work:
|
||||
"reasoning_effort": "medium",
|
||||
# Unsupported options are caught by type checker (uncomment to see):
|
||||
# "temperature": 0.7,
|
||||
# "random": 234,
|
||||
},
|
||||
)
|
||||
|
||||
print(f"OpenAI Response: {response.text}")
|
||||
print(f"Model used: {response.model_id}")
|
||||
|
||||
|
||||
async def demo_openai_agent() -> None:
|
||||
"""Demonstrate ChatAgent with OpenAI client and typed options."""
|
||||
print("\n=== ChatAgent with OpenAI and Typed Options ===\n")
|
||||
|
||||
# Create a typed agent - IDE will autocomplete options!
|
||||
# The type annotation can be done either on the agent like below,
|
||||
# or on the client when constructing the client instance:
|
||||
# client = OpenAIChatClient[OpenAIReasoningChatOptions]()
|
||||
agent = ChatAgent[OpenAIReasoningChatOptions](
|
||||
chat_client=OpenAIChatClient(),
|
||||
name="weather-assistant",
|
||||
instructions="You are a helpful assistant. Answer concisely.",
|
||||
# Options can be set at construction time
|
||||
default_options={
|
||||
"model_id": "o3",
|
||||
"max_tokens": 100,
|
||||
"allow_multiple_tool_calls": True,
|
||||
# OpenAI-specific options work:
|
||||
"reasoning_effort": "medium",
|
||||
# Unsupported options are caught by type checker (uncomment to see):
|
||||
# "temperature": 0.7,
|
||||
# "random": 234,
|
||||
},
|
||||
)
|
||||
|
||||
# Or pass options at runtime - they override construction options
|
||||
response = await agent.run(
|
||||
"What is 25 * 47?",
|
||||
options={
|
||||
"reasoning_effort": "high", # Override for a run
|
||||
},
|
||||
)
|
||||
|
||||
print(f"Agent Response: {response.text}")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run all Typed Options demonstrations."""
|
||||
# # Anthropic demos (requires ANTHROPIC_API_KEY)
|
||||
await demo_anthropic_chat_client()
|
||||
await demo_anthropic_agent()
|
||||
|
||||
# OpenAI demos (requires OPENAI_API_KEY)
|
||||
await demo_openai_chat_client_reasoning_models()
|
||||
await demo_openai_agent()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
from collections.abc import MutableSequence, Sequence
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, ChatMessage, ChatOptions, Context, ContextProvider
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, ChatMessage, Context, ContextProvider
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from pydantic import BaseModel
|
||||
@@ -46,11 +46,9 @@ class UserInfoMemory(ContextProvider):
|
||||
# Use the chat client to extract structured information
|
||||
result = await self._chat_client.get_response(
|
||||
messages=request_messages, # type: ignore
|
||||
chat_options=ChatOptions(
|
||||
instructions="Extract the user's name and age from the message if present. "
|
||||
"If not present return nulls.",
|
||||
response_format=UserInfo,
|
||||
),
|
||||
instructions="Extract the user's name and age from the message if present. "
|
||||
"If not present return nulls.",
|
||||
response_format=UserInfo,
|
||||
)
|
||||
|
||||
# Update user info with extracted data
|
||||
|
||||
+1
-2
@@ -17,7 +17,6 @@ from agent_framework import (
|
||||
FunctionResultContent,
|
||||
RequestInfoEvent,
|
||||
Role,
|
||||
ToolMode,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
@@ -177,7 +176,7 @@ def create_writer_agent() -> ChatAgent:
|
||||
"produce a 3-sentence draft."
|
||||
),
|
||||
tools=[fetch_product_brief, get_brand_voice_profile],
|
||||
tool_choice=ToolMode.REQUIRED_ANY,
|
||||
tool_choice="required",
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user