Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 18 additions & 39 deletions python/packages/anthropic/agent_framework_anthropic/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
get_logger,
prepare_function_call_results,
)
from agent_framework._pydantic import AFBaseSettings
from agent_framework._settings import SecretString, load_settings
from agent_framework._types import _get_data_bytes_as_str # type: ignore
from agent_framework.exceptions import ServiceInitializationError
from agent_framework.observability import ChatTelemetryLayer
Expand All @@ -47,7 +47,7 @@
from anthropic.types.beta.beta_code_execution_tool_result_error import (
BetaCodeExecutionToolResultError,
)
from pydantic import BaseModel, SecretStr, ValidationError
from pydantic import BaseModel

if sys.version_info >= (3, 11):
from typing import TypedDict # type: ignore # pragma: no cover
Expand Down Expand Up @@ -192,40 +192,20 @@ class AnthropicChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT],
}


class AnthropicSettings(AFBaseSettings):
class AnthropicSettings(TypedDict, total=False):
"""Anthropic Project settings.

The settings are first loaded from environment variables with the prefix 'ANTHROPIC_'.
If the environment variables are not found, the settings can be loaded from a .env file
with the encoding 'utf-8'. If the settings are not found in the .env file, the settings
are ignored; however, validation will fail alerting that the settings are missing.
with the encoding 'utf-8'.

Keyword Args:
Keys:
api_key: The Anthropic API key.
chat_model_id: The Anthropic chat model ID.
env_file_path: If provided, the .env settings are read from this file path location.
env_file_encoding: The encoding of the .env file, defaults to 'utf-8'.

Examples:
.. code-block:: python

from agent_framework.anthropic import AnthropicSettings

# Using environment variables
# Set ANTHROPIC_API_KEY=your_anthropic_api_key
# ANTHROPIC_CHAT_MODEL_ID=claude-sonnet-4-5-20250929

# Or passing parameters directly
settings = AnthropicSettings(chat_model_id="claude-sonnet-4-5-20250929")

# Or loading from a .env file
settings = AnthropicSettings(env_file_path="path/to/.env")
"""

env_prefix: ClassVar[str] = "ANTHROPIC_"

api_key: SecretStr | None = None
chat_model_id: str | None = None
api_key: SecretString | None
chat_model_id: str | None


class AnthropicClient(
Expand Down Expand Up @@ -311,25 +291,24 @@ class MyOptions(AnthropicChatOptions, total=False):
response = await client.get_response("Hello", options={"my_custom_option": "value"})

"""
try:
anthropic_settings = AnthropicSettings(
api_key=api_key, # type: ignore[arg-type]
chat_model_id=model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to create Anthropic settings.", ex) from ex
anthropic_settings = load_settings(
AnthropicSettings,
env_prefix="ANTHROPIC_",
api_key=api_key,
chat_model_id=model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)

if anthropic_client is None:
if not anthropic_settings.api_key:
if not anthropic_settings["api_key"]:
raise ServiceInitializationError(
"Anthropic API key is required. Set via 'api_key' parameter "
"or 'ANTHROPIC_API_KEY' environment variable."
)

anthropic_client = AsyncAnthropic(
api_key=anthropic_settings.api_key.get_secret_value(),
api_key=anthropic_settings["api_key"].get_secret_value(),
default_headers={"User-Agent": AGENT_FRAMEWORK_USER_AGENT},
)

Expand All @@ -343,7 +322,7 @@ class MyOptions(AnthropicChatOptions, total=False):
# Initialize instance variables
self.anthropic_client = anthropic_client
self.additional_beta_flags = additional_beta_flags or []
self.model_id = anthropic_settings.chat_model_id
self.model_id = anthropic_settings["chat_model_id"]
# streaming requires tracking the last function call ID and name
self._last_call_id_name: tuple[str, str] | None = None

Expand Down
55 changes: 27 additions & 28 deletions python/packages/anthropic/tests/test_anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@
SupportsChatGetResponse,
tool,
)
from agent_framework._settings import load_settings
from agent_framework.exceptions import ServiceInitializationError
from anthropic.types.beta import (
BetaMessage,
BetaTextBlock,
BetaToolUseBlock,
BetaUsage,
)
from pydantic import Field, ValidationError
from pydantic import Field

from agent_framework_anthropic import AnthropicClient
from agent_framework_anthropic._chat_client import AnthropicSettings
Expand All @@ -41,16 +42,20 @@ def create_test_anthropic_client(
) -> AnthropicClient:
"""Helper function to create AnthropicClient instances for testing, bypassing normal validation."""
if anthropic_settings is None:
anthropic_settings = AnthropicSettings(
api_key="test-api-key-12345", chat_model_id="claude-3-5-sonnet-20241022", env_file_path="test.env"
anthropic_settings = load_settings(
AnthropicSettings,
env_prefix="ANTHROPIC_",
api_key="test-api-key-12345",
chat_model_id="claude-3-5-sonnet-20241022",
env_file_path="test.env",
)

# Create client instance directly
client = object.__new__(AnthropicClient)

# Set attributes directly
client.anthropic_client = mock_anthropic_client
client.model_id = model_id or anthropic_settings.chat_model_id
client.model_id = model_id or anthropic_settings["chat_model_id"]
client._last_call_id_name = None
client.additional_properties = {}
client.middleware = None
Expand All @@ -64,30 +69,34 @@ def create_test_anthropic_client(

def test_anthropic_settings_init(anthropic_unit_test_env: dict[str, str]) -> None:
"""Test AnthropicSettings initialization."""
settings = AnthropicSettings(env_file_path="test.env")
settings = load_settings(AnthropicSettings, env_prefix="ANTHROPIC_", env_file_path="test.env")

assert settings.api_key is not None
assert settings.api_key.get_secret_value() == anthropic_unit_test_env["ANTHROPIC_API_KEY"]
assert settings.chat_model_id == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"]
assert settings["api_key"] is not None
assert settings["api_key"].get_secret_value() == anthropic_unit_test_env["ANTHROPIC_API_KEY"]
assert settings["chat_model_id"] == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"]


def test_anthropic_settings_init_with_explicit_values() -> None:
"""Test AnthropicSettings initialization with explicit values."""
settings = AnthropicSettings(
api_key="custom-api-key", chat_model_id="claude-3-opus-20240229", env_file_path="test.env"
settings = load_settings(
AnthropicSettings,
env_prefix="ANTHROPIC_",
api_key="custom-api-key",
chat_model_id="claude-3-opus-20240229",
env_file_path="test.env",
)

assert settings.api_key is not None
assert settings.api_key.get_secret_value() == "custom-api-key"
assert settings.chat_model_id == "claude-3-opus-20240229"
assert settings["api_key"] is not None
assert settings["api_key"].get_secret_value() == "custom-api-key"
assert settings["chat_model_id"] == "claude-3-opus-20240229"


@pytest.mark.parametrize("exclude_list", [["ANTHROPIC_API_KEY"]], indirect=True)
def test_anthropic_settings_missing_api_key(anthropic_unit_test_env: dict[str, str]) -> None:
"""Test AnthropicSettings when API key is missing."""
settings = AnthropicSettings(env_file_path="test.env")
assert settings.api_key is None
assert settings.chat_model_id == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"]
settings = load_settings(AnthropicSettings, env_prefix="ANTHROPIC_", env_file_path="test.env")
assert settings["api_key"] is None
assert settings["chat_model_id"] == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"]


# Client Initialization Tests
Expand Down Expand Up @@ -116,23 +125,13 @@ def test_anthropic_client_init_auto_create_client(anthropic_unit_test_env: dict[

def test_anthropic_client_init_missing_api_key() -> None:
"""Test AnthropicClient initialization when API key is missing."""
with patch("agent_framework_anthropic._chat_client.AnthropicSettings") as mock_settings:
mock_settings.return_value.api_key = None
mock_settings.return_value.chat_model_id = "claude-3-5-sonnet-20241022"
with patch("agent_framework_anthropic._chat_client.load_settings") as mock_load:
mock_load.return_value = {"api_key": None, "chat_model_id": "claude-3-5-sonnet-20241022"}

with pytest.raises(ServiceInitializationError, match="Anthropic API key is required"):
AnthropicClient()


def test_anthropic_client_init_validation_error() -> None:
"""Test that ValidationError in AnthropicSettings is properly handled."""
with patch("agent_framework_anthropic._chat_client.AnthropicSettings") as mock_settings:
mock_settings.side_effect = ValidationError.from_exception_data("test", [])

with pytest.raises(ServiceInitializationError, match="Failed to create Anthropic settings"):
AnthropicClient()


def test_anthropic_client_service_url(mock_anthropic_client: MagicMock) -> None:
"""Test service_url method."""
client = create_test_anthropic_client(mock_anthropic_client)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from agent_framework import AGENT_FRAMEWORK_USER_AGENT, Message
from agent_framework._logging import get_logger
from agent_framework._sessions import AgentSession, BaseContextProvider, SessionContext
from agent_framework._settings import load_settings
from agent_framework.exceptions import ServiceInitializationError
from azure.core.credentials import AzureKeyCredential
from azure.core.credentials_async import AsyncTokenCredential
Expand All @@ -41,7 +42,6 @@
VectorizableTextQuery,
VectorizedQuery,
)
from pydantic import ValidationError

from ._search_provider import AzureAISearchSettings

Expand Down Expand Up @@ -180,40 +180,39 @@ def __init__(
super().__init__(source_id)

# Load settings from environment/file
try:
settings = AzureAISearchSettings(
endpoint=endpoint,
index_name=index_name,
knowledge_base_name=knowledge_base_name,
api_key=api_key if isinstance(api_key, str) else None,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to create Azure AI Search settings.", ex) from ex
settings = load_settings(
AzureAISearchSettings,
env_prefix="AZURE_SEARCH_",
endpoint=endpoint,
index_name=index_name,
knowledge_base_name=knowledge_base_name,
api_key=api_key if isinstance(api_key, str) else None,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)

if not settings.endpoint:
if not settings.get("endpoint"):
raise ServiceInitializationError(
"Azure AI Search endpoint is required. Set via 'endpoint' parameter "
"or 'AZURE_SEARCH_ENDPOINT' environment variable."
)

if mode == "semantic":
if not settings.index_name:
if not settings.get("index_name"):
raise ServiceInitializationError(
"Azure AI Search index name is required for semantic mode. "
"Set via 'index_name' parameter or 'AZURE_SEARCH_INDEX_NAME' environment variable."
)
elif mode == "agentic":
if settings.index_name and settings.knowledge_base_name:
if settings.get("index_name") and settings.get("knowledge_base_name"):
raise ServiceInitializationError(
"For agentic mode, provide either 'index_name' OR 'knowledge_base_name', not both."
)
if not settings.index_name and not settings.knowledge_base_name:
if not settings.get("index_name") and not settings.get("knowledge_base_name"):
raise ServiceInitializationError(
"For agentic mode, provide either 'index_name' or 'knowledge_base_name'."
)
if settings.index_name and not model_deployment_name:
if settings.get("index_name") and not model_deployment_name:
raise ServiceInitializationError(
"model_deployment_name is required for agentic mode when creating Knowledge Base from index."
)
Expand All @@ -223,16 +222,16 @@ def __init__(
resolved_credential = credential
elif isinstance(api_key, AzureKeyCredential):
resolved_credential = api_key
elif settings.api_key:
resolved_credential = AzureKeyCredential(settings.api_key.get_secret_value())
elif settings.get("api_key"):
resolved_credential = AzureKeyCredential(settings["api_key"].get_secret_value()) # type: ignore[union-attr]
else:
raise ServiceInitializationError(
"Azure credential is required. Provide 'api_key' or 'credential' parameter "
"or set 'AZURE_SEARCH_API_KEY' environment variable."
)

self.endpoint = settings.endpoint
self.index_name = settings.index_name
self.endpoint: str = settings["endpoint"] # type: ignore[assignment] # validated above
self.index_name = settings.get("index_name")
self.credential = resolved_credential
self.mode = mode
self.top_k = top_k
Expand All @@ -244,7 +243,7 @@ def __init__(
self.azure_openai_resource_url = azure_openai_resource_url
self.azure_openai_deployment_name = model_deployment_name
self.model_name = model_name or model_deployment_name
self.knowledge_base_name = settings.knowledge_base_name
self.knowledge_base_name = settings.get("knowledge_base_name")
self.retrieval_instructions = retrieval_instructions
self.azure_openai_api_key = azure_openai_api_key
self.knowledge_base_output_mode = knowledge_base_output_mode
Expand All @@ -253,10 +252,10 @@ def __init__(

self._use_existing_knowledge_base = False
if mode == "agentic":
if settings.knowledge_base_name:
if settings.get("knowledge_base_name"):
self._use_existing_knowledge_base = True
else:
self.knowledge_base_name = f"{settings.index_name}-kb"
self.knowledge_base_name = f"{settings.get('index_name', '')}-kb"

self._auto_discovered_vector_field = False
self._use_vectorizable_query = False
Expand Down
Loading
Loading