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,28 @@
|
||||
# Anthropic Package (agent-framework-anthropic)
|
||||
|
||||
Integration with Anthropic's Claude API.
|
||||
|
||||
## Main Classes
|
||||
|
||||
- **`AnthropicClient`** - Chat client for Anthropic Claude models
|
||||
- **`AnthropicFoundryClient`** - Anthropic chat client for Azure AI Foundry's Anthropic-compatible endpoint
|
||||
- **`AnthropicBedrockClient`** - Anthropic chat client for Amazon Bedrock
|
||||
- **`AnthropicVertexClient`** - Anthropic chat client for Google Vertex AI
|
||||
- **`AnthropicChatOptions`** - Options TypedDict for Anthropic-specific parameters
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from agent_framework.anthropic import AnthropicClient
|
||||
|
||||
client = AnthropicClient(model="claude-sonnet-4-20250514")
|
||||
response = await client.get_response("Hello")
|
||||
```
|
||||
|
||||
## Import Path
|
||||
|
||||
```python
|
||||
from agent_framework.anthropic import AnthropicClient
|
||||
# or directly:
|
||||
from agent_framework_anthropic import AnthropicClient
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
||||
@@ -0,0 +1,42 @@
|
||||
# Get Started with Microsoft Agent Framework Anthropic
|
||||
|
||||
Please install this package via pip:
|
||||
|
||||
```bash
|
||||
pip install agent-framework-anthropic --pre
|
||||
```
|
||||
|
||||
## Anthropic Integration
|
||||
|
||||
The Anthropic integration enables communication with the Anthropic API, allowing your Agent Framework applications to leverage Anthropic's capabilities.
|
||||
|
||||
The package also includes Anthropic-hosted transport wrappers for:
|
||||
|
||||
- Azure AI Foundry via `AnthropicFoundryClient`
|
||||
- Amazon Bedrock via `AnthropicBedrockClient`
|
||||
- Google Vertex AI via `AnthropicVertexClient`
|
||||
|
||||
### Basic Usage Example
|
||||
|
||||
See the [Anthropic agent examples](../../samples/02-agents/providers/anthropic/) which demonstrate:
|
||||
|
||||
- Connecting to a Anthropic endpoint with an agent
|
||||
- Streaming and non-streaming responses
|
||||
|
||||
### Structured system blocks for prompt caching
|
||||
|
||||
Use `instructions` with Anthropic-native system blocks when you need structured system prompt content, such as
|
||||
prompt-cache `cache_control` metadata. Do not combine structured `instructions` blocks with a leading system message.
|
||||
|
||||
```python
|
||||
from anthropic.types.beta import BetaTextBlockParam
|
||||
|
||||
from agent_framework_anthropic import AnthropicClient
|
||||
|
||||
client = AnthropicClient()
|
||||
system_blocks: list[BetaTextBlockParam] = [
|
||||
{"type": "text", "text": "Stable instructions", "cache_control": {"type": "ephemeral", "ttl": "1h"}},
|
||||
]
|
||||
|
||||
response = await client.get_response("Hello", options={"instructions": system_blocks})
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
from ._bedrock_client import AnthropicBedrockClient, RawAnthropicBedrockClient
|
||||
from ._chat_client import (
|
||||
AnthropicChatOptions,
|
||||
AnthropicClient,
|
||||
RawAnthropicClient,
|
||||
)
|
||||
from ._foundry_client import AnthropicFoundryClient, RawAnthropicFoundryClient
|
||||
from ._vertex_client import AnthropicVertexClient, RawAnthropicVertexClient
|
||||
|
||||
try:
|
||||
__version__ = importlib.metadata.version(__name__)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
__version__ = "0.0.0" # Fallback for development mode
|
||||
|
||||
__all__ = [
|
||||
"AnthropicBedrockClient",
|
||||
"AnthropicChatOptions",
|
||||
"AnthropicClient",
|
||||
"AnthropicFoundryClient",
|
||||
"AnthropicVertexClient",
|
||||
"RawAnthropicBedrockClient",
|
||||
"RawAnthropicClient",
|
||||
"RawAnthropicFoundryClient",
|
||||
"RawAnthropicVertexClient",
|
||||
"__version__",
|
||||
]
|
||||
@@ -0,0 +1,168 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, ClassVar, Generic, TypedDict
|
||||
|
||||
from agent_framework import (
|
||||
ChatAndFunctionMiddlewareTypes,
|
||||
ChatMiddlewareLayer,
|
||||
FunctionInvocationConfiguration,
|
||||
FunctionInvocationLayer,
|
||||
)
|
||||
from agent_framework._settings import SecretString, load_settings
|
||||
from agent_framework._telemetry import get_user_agent
|
||||
from agent_framework.observability import ChatTelemetryLayer
|
||||
from anthropic.lib.bedrock import AsyncAnthropicBedrock
|
||||
|
||||
from ._chat_client import AnthropicOptionsT, RawAnthropicClient
|
||||
|
||||
|
||||
class AnthropicBedrockSettings(TypedDict, total=False):
|
||||
"""Resolved settings for Anthropic Bedrock wrappers."""
|
||||
|
||||
aws_access_key_id: SecretString | None
|
||||
aws_secret_access_key: SecretString | None
|
||||
aws_region: str | None
|
||||
aws_profile: str | None
|
||||
aws_session_token: SecretString | None
|
||||
anthropic_bedrock_base_url: str | None
|
||||
anthropic_chat_model: str | None
|
||||
|
||||
|
||||
class RawAnthropicBedrockClient(RawAnthropicClient[AnthropicOptionsT], Generic[AnthropicOptionsT]):
|
||||
"""Raw Anthropic Bedrock chat client without middleware, telemetry, or function invocation support."""
|
||||
|
||||
OTEL_PROVIDER_NAME: ClassVar[str] = "aws.bedrock"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
model: str | None = None,
|
||||
aws_secret_key: str | None = None,
|
||||
aws_access_key: str | None = None,
|
||||
aws_region: str | None = None,
|
||||
aws_profile: str | None = None,
|
||||
aws_session_token: str | None = None,
|
||||
base_url: str | None = None,
|
||||
anthropic_client: AsyncAnthropicBedrock | None = None,
|
||||
additional_beta_flags: list[str] | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize a raw Anthropic Bedrock client.
|
||||
|
||||
Keyword Args:
|
||||
model: The Anthropic model to use.
|
||||
aws_secret_key: AWS secret access key.
|
||||
aws_access_key: AWS access key ID.
|
||||
aws_region: AWS region.
|
||||
aws_profile: AWS profile name.
|
||||
aws_session_token: AWS session token.
|
||||
base_url: Optional custom Anthropic Bedrock base URL.
|
||||
anthropic_client: Existing AsyncAnthropicBedrock client to reuse.
|
||||
additional_beta_flags: Additional beta flags to enable on the client.
|
||||
additional_properties: Additional properties stored on the client instance.
|
||||
env_file_path: Path to environment file for loading settings.
|
||||
env_file_encoding: Encoding of the environment file.
|
||||
"""
|
||||
settings = load_settings(
|
||||
AnthropicBedrockSettings,
|
||||
env_prefix="",
|
||||
aws_access_key_id=aws_access_key,
|
||||
aws_secret_access_key=aws_secret_key,
|
||||
aws_region=aws_region,
|
||||
aws_profile=aws_profile,
|
||||
aws_session_token=aws_session_token,
|
||||
anthropic_bedrock_base_url=base_url,
|
||||
anthropic_chat_model=model,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
model_setting = settings.get("anthropic_chat_model")
|
||||
access_key_secret = settings.get("aws_access_key_id")
|
||||
secret_key_secret = settings.get("aws_secret_access_key")
|
||||
session_token_secret = settings.get("aws_session_token")
|
||||
|
||||
if anthropic_client is None:
|
||||
anthropic_client = AsyncAnthropicBedrock(
|
||||
aws_secret_key=secret_key_secret.get_secret_value() if secret_key_secret is not None else None,
|
||||
aws_access_key=access_key_secret.get_secret_value() if access_key_secret is not None else None,
|
||||
aws_region=settings.get("aws_region"),
|
||||
aws_profile=settings.get("aws_profile"),
|
||||
aws_session_token=session_token_secret.get_secret_value() if session_token_secret is not None else None,
|
||||
base_url=settings.get("anthropic_bedrock_base_url"),
|
||||
default_headers={"User-Agent": get_user_agent()},
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
model=model_setting,
|
||||
anthropic_client=anthropic_client,
|
||||
additional_beta_flags=additional_beta_flags,
|
||||
additional_properties=additional_properties,
|
||||
)
|
||||
|
||||
|
||||
class AnthropicBedrockClient(
|
||||
FunctionInvocationLayer[AnthropicOptionsT],
|
||||
ChatMiddlewareLayer[AnthropicOptionsT],
|
||||
ChatTelemetryLayer[AnthropicOptionsT],
|
||||
RawAnthropicBedrockClient[AnthropicOptionsT],
|
||||
Generic[AnthropicOptionsT],
|
||||
):
|
||||
"""Anthropic Bedrock chat client with middleware, telemetry, and function invocation support."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
model: str | None = None,
|
||||
aws_secret_key: str | None = None,
|
||||
aws_access_key: str | None = None,
|
||||
aws_region: str | None = None,
|
||||
aws_profile: str | None = None,
|
||||
aws_session_token: str | None = None,
|
||||
base_url: str | None = None,
|
||||
anthropic_client: AsyncAnthropicBedrock | None = None,
|
||||
additional_beta_flags: list[str] | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
|
||||
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize an Anthropic Bedrock client.
|
||||
|
||||
Keyword Args:
|
||||
model: The Anthropic model to use.
|
||||
aws_secret_key: AWS secret access key.
|
||||
aws_access_key: AWS access key ID.
|
||||
aws_region: AWS region.
|
||||
aws_profile: AWS profile name.
|
||||
aws_session_token: AWS session token.
|
||||
base_url: Optional custom Anthropic Bedrock base URL.
|
||||
anthropic_client: Existing AsyncAnthropicBedrock client to reuse.
|
||||
additional_beta_flags: Additional beta flags to enable on the client.
|
||||
additional_properties: Additional properties stored on the client instance.
|
||||
middleware: Optional middleware to apply to the client.
|
||||
function_invocation_configuration: Optional function invocation configuration override.
|
||||
env_file_path: Path to environment file for loading settings.
|
||||
env_file_encoding: Encoding of the environment file.
|
||||
"""
|
||||
super().__init__(
|
||||
model=model,
|
||||
aws_secret_key=aws_secret_key,
|
||||
aws_access_key=aws_access_key,
|
||||
aws_region=aws_region,
|
||||
aws_profile=aws_profile,
|
||||
aws_session_token=aws_session_token,
|
||||
base_url=base_url,
|
||||
anthropic_client=anthropic_client,
|
||||
additional_beta_flags=additional_beta_flags,
|
||||
additional_properties=additional_properties,
|
||||
middleware=middleware,
|
||||
function_invocation_configuration=function_invocation_configuration,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
from typing import Any, ClassVar, Generic, TypedDict
|
||||
|
||||
from agent_framework import (
|
||||
ChatAndFunctionMiddlewareTypes,
|
||||
ChatMiddlewareLayer,
|
||||
FunctionInvocationConfiguration,
|
||||
FunctionInvocationLayer,
|
||||
)
|
||||
from agent_framework._settings import SecretString, load_settings
|
||||
from agent_framework._telemetry import get_user_agent
|
||||
from agent_framework.observability import ChatTelemetryLayer
|
||||
from anthropic import AsyncAnthropicFoundry
|
||||
|
||||
from ._chat_client import AnthropicOptionsT, RawAnthropicClient
|
||||
|
||||
AnthropicFoundryAzureADTokenProvider = Callable[[], str | Awaitable[str]]
|
||||
|
||||
|
||||
class AnthropicFoundrySettings(TypedDict, total=False):
|
||||
"""Resolved settings for Anthropic Foundry wrappers."""
|
||||
|
||||
anthropic_foundry_api_key: SecretString | None
|
||||
anthropic_foundry_resource: str | None
|
||||
anthropic_foundry_base_url: str | None
|
||||
anthropic_chat_model: str | None
|
||||
|
||||
|
||||
class RawAnthropicFoundryClient(RawAnthropicClient[AnthropicOptionsT], Generic[AnthropicOptionsT]):
|
||||
"""Raw Anthropic Foundry chat client without middleware, telemetry, or function invocation support."""
|
||||
|
||||
OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai.foundry"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
model: str | None = None,
|
||||
resource: str | None = None,
|
||||
api_key: str | None = None,
|
||||
azure_ad_token_provider: AnthropicFoundryAzureADTokenProvider | None = None,
|
||||
base_url: str | None = None,
|
||||
anthropic_client: AsyncAnthropicFoundry | None = None,
|
||||
additional_beta_flags: list[str] | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize a raw Anthropic Foundry client.
|
||||
|
||||
Keyword Args:
|
||||
model: The Anthropic model to use.
|
||||
resource: The Foundry resource name.
|
||||
api_key: The Foundry Anthropic API key.
|
||||
azure_ad_token_provider: Azure AD token provider used by the Anthropic SDK.
|
||||
base_url: Full Anthropic-compatible Foundry base URL.
|
||||
anthropic_client: Existing AsyncAnthropicFoundry client to reuse.
|
||||
additional_beta_flags: Additional beta flags to enable on the client.
|
||||
additional_properties: Additional properties stored on the client instance.
|
||||
env_file_path: Path to environment file for loading settings.
|
||||
env_file_encoding: Encoding of the environment file.
|
||||
"""
|
||||
settings = load_settings(
|
||||
AnthropicFoundrySettings,
|
||||
env_prefix="",
|
||||
anthropic_foundry_api_key=api_key,
|
||||
anthropic_foundry_resource=resource,
|
||||
anthropic_foundry_base_url=base_url,
|
||||
anthropic_chat_model=model,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
api_key_secret = settings.get("anthropic_foundry_api_key")
|
||||
model_setting = settings.get("anthropic_chat_model")
|
||||
resource_setting = settings.get("anthropic_foundry_resource")
|
||||
base_url_setting = settings.get("anthropic_foundry_base_url")
|
||||
api_key_value = api_key_secret.get_secret_value() if api_key_secret is not None else None
|
||||
|
||||
if anthropic_client is None:
|
||||
if base_url_setting is None and resource_setting is None:
|
||||
message = (
|
||||
"Anthropic Foundry requires either `resource`/`ANTHROPIC_FOUNDRY_RESOURCE` "
|
||||
"or `base_url`/`ANTHROPIC_FOUNDRY_BASE_URL`."
|
||||
)
|
||||
raise ValueError(message)
|
||||
if base_url_setting is not None:
|
||||
anthropic_client = AsyncAnthropicFoundry(
|
||||
base_url=base_url_setting,
|
||||
api_key=api_key_value,
|
||||
azure_ad_token_provider=azure_ad_token_provider,
|
||||
default_headers={"User-Agent": get_user_agent()},
|
||||
)
|
||||
else:
|
||||
anthropic_client = AsyncAnthropicFoundry(
|
||||
resource=resource_setting,
|
||||
api_key=api_key_value,
|
||||
azure_ad_token_provider=azure_ad_token_provider,
|
||||
default_headers={"User-Agent": get_user_agent()},
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
model=model_setting,
|
||||
anthropic_client=anthropic_client,
|
||||
additional_beta_flags=additional_beta_flags,
|
||||
additional_properties=additional_properties,
|
||||
)
|
||||
|
||||
|
||||
class AnthropicFoundryClient(
|
||||
FunctionInvocationLayer[AnthropicOptionsT],
|
||||
ChatMiddlewareLayer[AnthropicOptionsT],
|
||||
ChatTelemetryLayer[AnthropicOptionsT],
|
||||
RawAnthropicFoundryClient[AnthropicOptionsT],
|
||||
Generic[AnthropicOptionsT],
|
||||
):
|
||||
"""Anthropic Foundry chat client with middleware, telemetry, and function invocation support."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
model: str | None = None,
|
||||
resource: str | None = None,
|
||||
api_key: str | None = None,
|
||||
azure_ad_token_provider: AnthropicFoundryAzureADTokenProvider | None = None,
|
||||
base_url: str | None = None,
|
||||
anthropic_client: AsyncAnthropicFoundry | None = None,
|
||||
additional_beta_flags: list[str] | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
|
||||
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize an Anthropic Foundry client.
|
||||
|
||||
Keyword Args:
|
||||
model: The Anthropic model to use.
|
||||
resource: The Foundry resource name.
|
||||
api_key: The Foundry Anthropic API key.
|
||||
azure_ad_token_provider: Azure AD token provider used by the Anthropic SDK.
|
||||
base_url: Full Anthropic-compatible Foundry base URL.
|
||||
anthropic_client: Existing AsyncAnthropicFoundry client to reuse.
|
||||
additional_beta_flags: Additional beta flags to enable on the client.
|
||||
additional_properties: Additional properties stored on the client instance.
|
||||
middleware: Optional middleware to apply to the client.
|
||||
function_invocation_configuration: Optional function invocation configuration override.
|
||||
env_file_path: Path to environment file for loading settings.
|
||||
env_file_encoding: Encoding of the environment file.
|
||||
"""
|
||||
super().__init__(
|
||||
model=model,
|
||||
resource=resource,
|
||||
api_key=api_key,
|
||||
azure_ad_token_provider=azure_ad_token_provider,
|
||||
base_url=base_url,
|
||||
anthropic_client=anthropic_client,
|
||||
additional_beta_flags=additional_beta_flags,
|
||||
additional_properties=additional_properties,
|
||||
middleware=middleware,
|
||||
function_invocation_configuration=function_invocation_configuration,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
@@ -0,0 +1,161 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypedDict
|
||||
|
||||
from agent_framework import (
|
||||
ChatAndFunctionMiddlewareTypes,
|
||||
ChatMiddlewareLayer,
|
||||
FunctionInvocationConfiguration,
|
||||
FunctionInvocationLayer,
|
||||
)
|
||||
from agent_framework._settings import load_settings
|
||||
from agent_framework._telemetry import get_user_agent
|
||||
from agent_framework.observability import ChatTelemetryLayer
|
||||
from anthropic import NOT_GIVEN
|
||||
from anthropic.lib.vertex import AsyncAnthropicVertex
|
||||
|
||||
from ._chat_client import AnthropicOptionsT, RawAnthropicClient
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from google.auth.credentials import Credentials as GoogleCredentials
|
||||
|
||||
|
||||
class AnthropicVertexSettings(TypedDict, total=False):
|
||||
"""Resolved settings for Anthropic Vertex wrappers."""
|
||||
|
||||
cloud_ml_region: str | None
|
||||
anthropic_vertex_project_id: str | None
|
||||
anthropic_vertex_base_url: str | None
|
||||
anthropic_chat_model: str | None
|
||||
|
||||
|
||||
class RawAnthropicVertexClient(RawAnthropicClient[AnthropicOptionsT], Generic[AnthropicOptionsT]):
|
||||
"""Raw Anthropic Vertex chat client without middleware, telemetry, or function invocation support."""
|
||||
|
||||
OTEL_PROVIDER_NAME: ClassVar[str] = "google.vertex.ai"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
model: str | None = None,
|
||||
region: str | None = None,
|
||||
project_id: str | None = None,
|
||||
access_token: str | None = None,
|
||||
credentials: GoogleCredentials | None = None,
|
||||
base_url: str | None = None,
|
||||
anthropic_client: AsyncAnthropicVertex | None = None,
|
||||
additional_beta_flags: list[str] | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize a raw Anthropic Vertex client.
|
||||
|
||||
Keyword Args:
|
||||
model: The Anthropic model to use.
|
||||
region: Vertex region. Falls back to `CLOUD_ML_REGION`.
|
||||
project_id: Vertex project ID. Falls back to `ANTHROPIC_VERTEX_PROJECT_ID`.
|
||||
access_token: Explicit OAuth access token.
|
||||
credentials: Google credentials object.
|
||||
base_url: Optional custom Anthropic Vertex base URL.
|
||||
anthropic_client: Existing AsyncAnthropicVertex client to reuse.
|
||||
additional_beta_flags: Additional beta flags to enable on the client.
|
||||
additional_properties: Additional properties stored on the client instance.
|
||||
env_file_path: Path to environment file for loading settings.
|
||||
env_file_encoding: Encoding of the environment file.
|
||||
"""
|
||||
settings = load_settings(
|
||||
AnthropicVertexSettings,
|
||||
env_prefix="",
|
||||
cloud_ml_region=region,
|
||||
anthropic_vertex_project_id=project_id,
|
||||
anthropic_vertex_base_url=base_url,
|
||||
anthropic_chat_model=model,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
model_setting = settings.get("anthropic_chat_model")
|
||||
region_setting = settings.get("cloud_ml_region")
|
||||
project_id_setting = settings.get("anthropic_vertex_project_id")
|
||||
|
||||
if anthropic_client is None:
|
||||
resolved_region = region_setting if region_setting is not None else NOT_GIVEN
|
||||
resolved_project_id = project_id_setting if project_id_setting is not None else NOT_GIVEN
|
||||
anthropic_client = AsyncAnthropicVertex(
|
||||
region=resolved_region,
|
||||
project_id=resolved_project_id,
|
||||
access_token=access_token,
|
||||
credentials=credentials,
|
||||
base_url=settings.get("anthropic_vertex_base_url"),
|
||||
default_headers={"User-Agent": get_user_agent()},
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
model=model_setting,
|
||||
anthropic_client=anthropic_client,
|
||||
additional_beta_flags=additional_beta_flags,
|
||||
additional_properties=additional_properties,
|
||||
)
|
||||
|
||||
|
||||
class AnthropicVertexClient(
|
||||
FunctionInvocationLayer[AnthropicOptionsT],
|
||||
ChatMiddlewareLayer[AnthropicOptionsT],
|
||||
ChatTelemetryLayer[AnthropicOptionsT],
|
||||
RawAnthropicVertexClient[AnthropicOptionsT],
|
||||
Generic[AnthropicOptionsT],
|
||||
):
|
||||
"""Anthropic Vertex chat client with middleware, telemetry, and function invocation support."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
model: str | None = None,
|
||||
region: str | None = None,
|
||||
project_id: str | None = None,
|
||||
access_token: str | None = None,
|
||||
credentials: GoogleCredentials | None = None,
|
||||
base_url: str | None = None,
|
||||
anthropic_client: AsyncAnthropicVertex | None = None,
|
||||
additional_beta_flags: list[str] | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
|
||||
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize an Anthropic Vertex client.
|
||||
|
||||
Keyword Args:
|
||||
model: The Anthropic model to use.
|
||||
region: Vertex region. Falls back to `CLOUD_ML_REGION`.
|
||||
project_id: Vertex project ID. Falls back to `ANTHROPIC_VERTEX_PROJECT_ID`.
|
||||
access_token: Explicit OAuth access token.
|
||||
credentials: Google credentials object.
|
||||
base_url: Optional custom Anthropic Vertex base URL.
|
||||
anthropic_client: Existing AsyncAnthropicVertex client to reuse.
|
||||
additional_beta_flags: Additional beta flags to enable on the client.
|
||||
additional_properties: Additional properties stored on the client instance.
|
||||
middleware: Optional middleware to apply to the client.
|
||||
function_invocation_configuration: Optional function invocation configuration override.
|
||||
env_file_path: Path to environment file for loading settings.
|
||||
env_file_encoding: Encoding of the environment file.
|
||||
"""
|
||||
super().__init__(
|
||||
model=model,
|
||||
region=region,
|
||||
project_id=project_id,
|
||||
access_token=access_token,
|
||||
credentials=credentials,
|
||||
base_url=base_url,
|
||||
anthropic_client=anthropic_client,
|
||||
additional_beta_flags=additional_beta_flags,
|
||||
additional_properties=additional_properties,
|
||||
middleware=middleware,
|
||||
function_invocation_configuration=function_invocation_configuration,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
@@ -0,0 +1,98 @@
|
||||
[project]
|
||||
name = "agent-framework-anthropic"
|
||||
description = "Anthropic integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260709"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true"
|
||||
urls.issues = "https://github.com/microsoft/agent-framework/issues"
|
||||
classifiers = [
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.11.0,<2",
|
||||
"anthropic>=0.80.0,<0.117.0",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
prerelease = "if-necessary-or-explicit"
|
||||
environments = [
|
||||
"sys_platform == 'darwin'",
|
||||
"sys_platform == 'linux'",
|
||||
"sys_platform == 'win32'"
|
||||
]
|
||||
|
||||
[tool.uv-dynamic-versioning]
|
||||
fallback-version = "0.0.0"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = 'tests'
|
||||
addopts = "-ra -q -r fEX"
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
filterwarnings = [
|
||||
"ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*"
|
||||
]
|
||||
timeout = 120
|
||||
markers = [
|
||||
"integration: marks tests as integration tests that require external services",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
|
||||
[tool.coverage.run]
|
||||
omit = [
|
||||
"**/__init__.py"
|
||||
]
|
||||
|
||||
[tool.pyright]
|
||||
extends = "../../pyproject.toml"
|
||||
exclude = ['tests']
|
||||
|
||||
[tool.mypy]
|
||||
plugins = ['pydantic.mypy']
|
||||
strict = true
|
||||
python_version = "3.10"
|
||||
ignore_missing_imports = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
check_untyped_defs = true
|
||||
warn_return_any = true
|
||||
show_error_codes = true
|
||||
warn_unused_ignores = false
|
||||
disallow_incomplete_defs = true
|
||||
disallow_untyped_decorators = true
|
||||
|
||||
[tool.bandit]
|
||||
targets = ["agent_framework_anthropic"]
|
||||
exclude_dirs = ["tests"]
|
||||
|
||||
[tool.poe]
|
||||
executor.type = "uv"
|
||||
include = "../../shared_tasks.toml"
|
||||
|
||||
[tool.poe.tasks.mypy]
|
||||
help = "Run MyPy for this package."
|
||||
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_anthropic"
|
||||
|
||||
[tool.poe.tasks.test]
|
||||
help = "Run the default unit test suite for this package."
|
||||
cmd = 'pytest -m "not integration" --cov=agent_framework_anthropic --cov-report=term-missing:skip-covered -n auto --dist worksteal tests'
|
||||
|
||||
[build-system]
|
||||
requires = ["flit-core >= 3.11,<4.0"]
|
||||
build-backend = "flit_core.buildapi"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
@@ -0,0 +1,56 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from pytest import fixture
|
||||
|
||||
|
||||
@fixture
|
||||
def exclude_list(request: Any) -> list[str]:
|
||||
"""Fixture that returns a list of environment variables to exclude."""
|
||||
return request.param if hasattr(request, "param") else []
|
||||
|
||||
|
||||
@fixture
|
||||
def override_env_param_dict(request: Any) -> dict[str, str]:
|
||||
"""Fixture that returns a dict of environment variables to override."""
|
||||
return request.param if hasattr(request, "param") else {}
|
||||
|
||||
|
||||
@fixture
|
||||
def anthropic_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): # type: ignore
|
||||
"""Fixture to set environment variables for AnthropicSettings."""
|
||||
if exclude_list is None:
|
||||
exclude_list = []
|
||||
|
||||
if override_env_param_dict is None:
|
||||
override_env_param_dict = {}
|
||||
|
||||
env_vars = {
|
||||
"ANTHROPIC_API_KEY": "test-api-key-12345",
|
||||
"ANTHROPIC_CHAT_MODEL": "claude-3-5-sonnet-20241022",
|
||||
}
|
||||
|
||||
env_vars.update(override_env_param_dict) # type: ignore
|
||||
|
||||
for key, value in env_vars.items():
|
||||
if key in exclude_list:
|
||||
monkeypatch.delenv(key, raising=False) # type: ignore
|
||||
continue
|
||||
monkeypatch.setenv(key, value) # type: ignore
|
||||
|
||||
return env_vars
|
||||
|
||||
|
||||
@fixture
|
||||
def mock_anthropic_client() -> MagicMock:
|
||||
"""Fixture that provides a mock AsyncAnthropic client."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.base_url = "https://api.anthropic.com"
|
||||
|
||||
# Mock beta.messages property
|
||||
mock_client.beta = MagicMock()
|
||||
mock_client.beta.messages = MagicMock()
|
||||
mock_client.beta.messages.create = AsyncMock()
|
||||
|
||||
return mock_client
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,222 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from agent_framework import Agent, ChatMiddlewareLayer, FunctionInvocationLayer
|
||||
from agent_framework._telemetry import get_user_agent
|
||||
from agent_framework.observability import ChatTelemetryLayer
|
||||
|
||||
from agent_framework_anthropic import (
|
||||
AnthropicBedrockClient,
|
||||
AnthropicFoundryClient,
|
||||
AnthropicVertexClient,
|
||||
RawAnthropicBedrockClient,
|
||||
RawAnthropicFoundryClient,
|
||||
RawAnthropicVertexClient,
|
||||
)
|
||||
|
||||
|
||||
def _create_mock_transport(base_url: str) -> MagicMock:
|
||||
transport = MagicMock()
|
||||
transport.base_url = base_url
|
||||
transport.beta = MagicMock()
|
||||
transport.beta.messages = MagicMock()
|
||||
transport.beta.messages.create = AsyncMock()
|
||||
return transport
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("public_client", "raw_client"),
|
||||
[
|
||||
(AnthropicFoundryClient, RawAnthropicFoundryClient),
|
||||
(AnthropicBedrockClient, RawAnthropicBedrockClient),
|
||||
(AnthropicVertexClient, RawAnthropicVertexClient),
|
||||
],
|
||||
)
|
||||
def test_provider_client_wraps_raw_client_with_standard_layer_order(public_client, raw_client) -> None:
|
||||
assert issubclass(public_client, raw_client)
|
||||
mro = public_client.__mro__
|
||||
assert mro.index(FunctionInvocationLayer) < mro.index(ChatMiddlewareLayer)
|
||||
assert mro.index(ChatMiddlewareLayer) < mro.index(ChatTelemetryLayer)
|
||||
assert mro.index(ChatTelemetryLayer) < mro.index(raw_client)
|
||||
|
||||
|
||||
def test_agent_accepts_anthropic_foundry_clients() -> None:
|
||||
mock_transport = _create_mock_transport("https://test-resource.services.ai.azure.com/anthropic/")
|
||||
with patch("agent_framework_anthropic._foundry_client.AsyncAnthropicFoundry", return_value=mock_transport):
|
||||
raw_client = RawAnthropicFoundryClient(
|
||||
model="claude-foundry-test",
|
||||
resource="test-resource",
|
||||
api_key="test-key",
|
||||
)
|
||||
raw_agent = Agent(client=raw_client, instructions="test agent")
|
||||
assert raw_agent.client is raw_client
|
||||
|
||||
with patch("agent_framework_anthropic._foundry_client.AsyncAnthropicFoundry", return_value=mock_transport):
|
||||
client = AnthropicFoundryClient(
|
||||
model="claude-foundry-test",
|
||||
resource="test-resource",
|
||||
api_key="test-key",
|
||||
)
|
||||
agent = Agent(client=client, instructions="test agent")
|
||||
assert agent.client is client
|
||||
|
||||
|
||||
def test_agent_accepts_anthropic_bedrock_clients() -> None:
|
||||
mock_transport = _create_mock_transport("https://bedrock-runtime.us-east-1.amazonaws.com")
|
||||
with patch("agent_framework_anthropic._bedrock_client.AsyncAnthropicBedrock", return_value=mock_transport):
|
||||
raw_client = RawAnthropicBedrockClient(
|
||||
model="claude-bedrock-test",
|
||||
aws_access_key="access-key",
|
||||
aws_secret_key="secret-key",
|
||||
aws_region="us-east-1",
|
||||
)
|
||||
raw_agent = Agent(client=raw_client, instructions="test agent")
|
||||
assert raw_agent.client is raw_client
|
||||
|
||||
with patch("agent_framework_anthropic._bedrock_client.AsyncAnthropicBedrock", return_value=mock_transport):
|
||||
client = AnthropicBedrockClient(
|
||||
model="claude-bedrock-test",
|
||||
aws_access_key="access-key",
|
||||
aws_secret_key="secret-key",
|
||||
aws_region="us-east-1",
|
||||
)
|
||||
agent = Agent(client=client, instructions="test agent")
|
||||
assert agent.client is client
|
||||
|
||||
|
||||
def test_agent_accepts_anthropic_vertex_clients() -> None:
|
||||
mock_transport = _create_mock_transport("https://us-central1-aiplatform.googleapis.com/v1")
|
||||
with patch("agent_framework_anthropic._vertex_client.AsyncAnthropicVertex", return_value=mock_transport):
|
||||
raw_client = RawAnthropicVertexClient(
|
||||
model="claude-vertex-test",
|
||||
region="us-central1",
|
||||
project_id="test-project",
|
||||
)
|
||||
raw_agent = Agent(client=raw_client, instructions="test agent")
|
||||
assert raw_agent.client is raw_client
|
||||
|
||||
with patch("agent_framework_anthropic._vertex_client.AsyncAnthropicVertex", return_value=mock_transport):
|
||||
client = AnthropicVertexClient(
|
||||
model="claude-vertex-test",
|
||||
region="us-central1",
|
||||
project_id="test-project",
|
||||
)
|
||||
agent = Agent(client=client, instructions="test agent")
|
||||
assert agent.client is client
|
||||
|
||||
|
||||
def test_raw_anthropic_foundry_client_creates_sdk_client_from_settings(tmp_path) -> None:
|
||||
env_file = tmp_path / ".env"
|
||||
env_file.write_text(
|
||||
"ANTHROPIC_CHAT_MODEL=claude-foundry-test\n"
|
||||
"ANTHROPIC_FOUNDRY_API_KEY=test-key\n"
|
||||
"ANTHROPIC_FOUNDRY_RESOURCE=test-resource\n"
|
||||
)
|
||||
mock_transport = _create_mock_transport("https://test-resource.services.ai.azure.com/anthropic/")
|
||||
|
||||
with patch(
|
||||
"agent_framework_anthropic._foundry_client.AsyncAnthropicFoundry", return_value=mock_transport
|
||||
) as factory:
|
||||
client = RawAnthropicFoundryClient(env_file_path=str(env_file))
|
||||
|
||||
assert client.model == "claude-foundry-test"
|
||||
assert client.anthropic_client is mock_transport
|
||||
factory.assert_called_once_with(
|
||||
resource="test-resource",
|
||||
api_key="test-key",
|
||||
azure_ad_token_provider=None,
|
||||
default_headers={"User-Agent": get_user_agent()},
|
||||
)
|
||||
|
||||
|
||||
def test_raw_anthropic_foundry_client_creates_sdk_client_from_base_url_settings(tmp_path) -> None:
|
||||
env_file = tmp_path / ".env"
|
||||
env_file.write_text(
|
||||
"ANTHROPIC_CHAT_MODEL=claude-foundry-test\n"
|
||||
"ANTHROPIC_FOUNDRY_API_KEY=test-key\n"
|
||||
"ANTHROPIC_FOUNDRY_BASE_URL=https://test-resource.services.ai.azure.com/anthropic/\n"
|
||||
)
|
||||
mock_transport = _create_mock_transport("https://test-resource.services.ai.azure.com/anthropic/")
|
||||
|
||||
with patch(
|
||||
"agent_framework_anthropic._foundry_client.AsyncAnthropicFoundry", return_value=mock_transport
|
||||
) as factory:
|
||||
client = RawAnthropicFoundryClient(env_file_path=str(env_file))
|
||||
|
||||
assert client.model == "claude-foundry-test"
|
||||
assert client.anthropic_client is mock_transport
|
||||
factory.assert_called_once_with(
|
||||
base_url="https://test-resource.services.ai.azure.com/anthropic/",
|
||||
api_key="test-key",
|
||||
azure_ad_token_provider=None,
|
||||
default_headers={"User-Agent": get_user_agent()},
|
||||
)
|
||||
|
||||
|
||||
def test_raw_anthropic_foundry_client_requires_resource_or_base_url() -> None:
|
||||
with patch("agent_framework_anthropic._foundry_client.load_settings") as mock_load:
|
||||
mock_load.return_value = {
|
||||
"anthropic_foundry_api_key": None,
|
||||
"anthropic_foundry_resource": None,
|
||||
"anthropic_foundry_base_url": None,
|
||||
"anthropic_chat_model": None,
|
||||
}
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=(
|
||||
"Anthropic Foundry requires either `resource`/`ANTHROPIC_FOUNDRY_RESOURCE` "
|
||||
"or `base_url`/`ANTHROPIC_FOUNDRY_BASE_URL`\\."
|
||||
),
|
||||
):
|
||||
RawAnthropicFoundryClient()
|
||||
|
||||
|
||||
def test_raw_anthropic_bedrock_client_creates_sdk_client_from_arguments() -> None:
|
||||
mock_transport = _create_mock_transport("https://bedrock-runtime.us-east-1.amazonaws.com")
|
||||
|
||||
with patch(
|
||||
"agent_framework_anthropic._bedrock_client.AsyncAnthropicBedrock", return_value=mock_transport
|
||||
) as factory:
|
||||
client = RawAnthropicBedrockClient(
|
||||
model="claude-bedrock-test",
|
||||
aws_access_key="access-key",
|
||||
aws_secret_key="secret-key",
|
||||
aws_region="us-east-1",
|
||||
)
|
||||
|
||||
assert client.model == "claude-bedrock-test"
|
||||
assert client.anthropic_client is mock_transport
|
||||
factory.assert_called_once_with(
|
||||
aws_secret_key="secret-key",
|
||||
aws_access_key="access-key",
|
||||
aws_region="us-east-1",
|
||||
aws_profile=None,
|
||||
aws_session_token=None,
|
||||
base_url=None,
|
||||
default_headers={"User-Agent": get_user_agent()},
|
||||
)
|
||||
|
||||
|
||||
def test_raw_anthropic_vertex_client_creates_sdk_client_from_arguments() -> None:
|
||||
mock_transport = _create_mock_transport("https://us-central1-aiplatform.googleapis.com/v1")
|
||||
|
||||
with patch("agent_framework_anthropic._vertex_client.AsyncAnthropicVertex", return_value=mock_transport) as factory:
|
||||
client = RawAnthropicVertexClient(
|
||||
model="claude-vertex-test",
|
||||
region="us-central1",
|
||||
project_id="test-project",
|
||||
)
|
||||
|
||||
assert client.model == "claude-vertex-test"
|
||||
assert client.anthropic_client is mock_transport
|
||||
factory.assert_called_once_with(
|
||||
region="us-central1",
|
||||
project_id="test-project",
|
||||
access_token=None,
|
||||
credentials=None,
|
||||
base_url=None,
|
||||
default_headers={"User-Agent": get_user_agent()},
|
||||
)
|
||||
Reference in New Issue
Block a user