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
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,9 @@ class AnthropicChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT],
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'.
Settings are resolved in this order: explicit keyword arguments, values from an
explicitly provided .env file, then environment variables with the prefix
'ANTHROPIC_'.

Keys:
api_key: The Anthropic API key.
Expand Down
7 changes: 2 additions & 5 deletions python/packages/anthropic/tests/test_anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ def create_test_anthropic_client(
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
Expand All @@ -72,7 +71,7 @@ def create_test_anthropic_client(

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

assert settings["api_key"] is not None
assert settings["api_key"].get_secret_value() == anthropic_unit_test_env["ANTHROPIC_API_KEY"]
Expand All @@ -86,7 +85,6 @@ def test_anthropic_settings_init_with_explicit_values() -> None:
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
Expand All @@ -97,7 +95,7 @@ def test_anthropic_settings_init_with_explicit_values() -> None:
@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 = load_settings(AnthropicSettings, env_prefix="ANTHROPIC_", env_file_path="test.env")
settings = load_settings(AnthropicSettings, env_prefix="ANTHROPIC_")
assert settings["api_key"] is None
assert settings["chat_model_id"] == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"]

Expand All @@ -119,7 +117,6 @@ def test_anthropic_client_init_auto_create_client(anthropic_unit_test_env: dict[
client = AnthropicClient(
api_key=anthropic_unit_test_env["ANTHROPIC_API_KEY"],
model_id=anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"],
env_file_path="test.env",
)

assert client.anthropic_client is not None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,9 @@
class AzureAISearchSettings(TypedDict, total=False):
"""Settings for Azure AI Search Context Provider with auto-loading from environment.

The settings are first loaded from environment variables with the prefix 'AZURE_SEARCH_'.
If the environment variables are not found, the settings can be loaded from a .env file.
Settings are resolved in this order: explicit keyword arguments, values from an
explicitly provided .env file, then environment variables with the prefix
'AZURE_SEARCH_'.

Keys:
endpoint: Azure AI Search endpoint URL.
Expand Down
7 changes: 3 additions & 4 deletions python/packages/azure-ai/agent_framework_azure_ai/_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,9 @@
class AzureAISettings(TypedDict, total=False):
"""Azure AI Project settings.

The settings are first loaded from environment variables with the prefix 'AZURE_AI_'.
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.
Settings are resolved in this order: explicit keyword arguments, values from an
explicitly provided .env file, then environment variables with the prefix
'AZURE_AI_'. If settings are missing after resolution, validation will fail.

Keyword Args:
project_endpoint: The Azure AI Project endpoint URL.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def create_test_azure_ai_chat_client(
) -> AzureAIAgentClient:
"""Helper function to create AzureAIAgentClient instances for testing, bypassing normal validation."""
if azure_ai_settings is None:
azure_ai_settings = load_settings(AzureAISettings, env_prefix="AZURE_AI_", env_file_path="test.env")
azure_ai_settings = load_settings(AzureAISettings, env_prefix="AZURE_AI_")

# Create client instance directly
client = object.__new__(AzureAIAgentClient)
Expand Down
2 changes: 1 addition & 1 deletion python/packages/azure-ai/tests/test_azure_ai_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def create_test_azure_ai_client(
) -> AzureAIClient:
"""Helper function to create AzureAIClient instances for testing, bypassing normal validation."""
if azure_ai_settings is None:
azure_ai_settings = load_settings(AzureAISettings, env_prefix="AZURE_AI_", env_file_path="test.env")
azure_ai_settings = load_settings(AzureAISettings, env_prefix="AZURE_AI_")

# Create client instance directly
client = object.__new__(AzureAIClient)
Expand Down
3 changes: 1 addition & 2 deletions python/packages/claude/agent_framework_claude/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

import importlib.metadata

from ._agent import ClaudeAgent, ClaudeAgentOptions
from ._settings import ClaudeAgentSettings
from ._agent import ClaudeAgent, ClaudeAgentOptions, ClaudeAgentSettings

try:
__version__ = importlib.metadata.version(__name__)
Expand Down
59 changes: 41 additions & 18 deletions python/packages/claude/agent_framework_claude/_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,19 @@
AgentMiddlewareTypes,
AgentResponse,
AgentResponseUpdate,
AgentRunInputs,
AgentSession,
BaseAgent,
BaseContextProvider,
Content,
FunctionTool,
Message,
ResponseStream,
ToolTypes,
load_settings,
normalize_messages,
normalize_tools,
)
from agent_framework._settings import load_settings
from agent_framework._tools import ToolTypes
from agent_framework._types import AgentRunInputs, normalize_tools
from agent_framework.exceptions import ServiceException
from claude_agent_sdk import (
AssistantMessage,
Expand All @@ -38,8 +39,6 @@
)
from claude_agent_sdk.types import StreamEvent, TextBlock

from ._settings import ClaudeAgentSettings

if sys.version_info >= (3, 13):
from typing import TypeVar # type: ignore # pragma: no cover
else:
Expand All @@ -60,16 +59,40 @@
SdkBeta,
)

__all__ = ["ClaudeAgent", "ClaudeAgentOptions"]

logger = logging.getLogger("agent_framework.claude")


# Name of the in-process MCP server that hosts Agent Framework tools.
# FunctionTool instances are converted to SDK MCP tools and served
# through this server, as Claude Code CLI only supports tools via MCP.
TOOLS_MCP_SERVER_NAME = "_agent_framework_tools"


class ClaudeAgentSettings(TypedDict, total=False):
"""Claude Agent settings.

Settings are resolved in this order: explicit keyword arguments, values from an
explicitly provided .env file, then environment variables with the prefix
'CLAUDE_AGENT_'.

Keys:
cli_path: The path to Claude CLI executable.
model: The model to use (sonnet, opus, haiku).
cwd: The working directory for Claude CLI.
permission_mode: Permission mode (default, acceptEdits, plan, bypassPermissions).
max_turns: Maximum number of conversation turns.
max_budget_usd: Maximum budget in USD.
"""

cli_path: str | None
model: str | None
cwd: str | None
permission_mode: str | None
max_turns: int | None
max_budget_usd: float | None


class ClaudeAgentOptions(TypedDict, total=False):
"""Claude Agent-specific options."""

Expand Down Expand Up @@ -402,18 +425,18 @@ def _prepare_client_options(self, resume_session_id: str | None = None) -> SDKOp
opts["resume"] = resume_session_id

# Apply settings from environment
if self._settings["cli_path"]:
opts["cli_path"] = self._settings["cli_path"]
if self._settings["model"]:
opts["model"] = self._settings["model"]
if self._settings["cwd"]:
opts["cwd"] = self._settings["cwd"]
if self._settings["permission_mode"]:
opts["permission_mode"] = self._settings["permission_mode"]
if self._settings["max_turns"]:
opts["max_turns"] = self._settings["max_turns"]
if self._settings["max_budget_usd"]:
opts["max_budget_usd"] = self._settings["max_budget_usd"]
if cli_path := self._settings.get("cli_path"):
opts["cli_path"] = cli_path
if model := self._settings.get("model"):
opts["model"] = model
if cwd := self._settings.get("cwd"):
opts["cwd"] = cwd
if permission_mode := self._settings.get("permission_mode"):
opts["permission_mode"] = permission_mode
if max_turns := self._settings.get("max_turns"):
opts["max_turns"] = max_turns
if max_budget_usd := self._settings.get("max_budget_usd"):
opts["max_budget_usd"] = max_budget_usd

# Apply default options
for key, value in self._default_options.items():
Expand Down
29 changes: 0 additions & 29 deletions python/packages/claude/agent_framework_claude/_settings.py

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
class CopilotStudioSettings(TypedDict, total=False):
"""Copilot Studio model settings.

The settings are first loaded from environment variables with the prefix 'COPILOTSTUDIOAGENT__'.
If the environment variables are not found, the settings can be loaded from a .env file
with the encoding 'utf-8'.
Settings are resolved in this order: explicit keyword arguments, values from an
explicitly provided .env file, then environment variables with the prefix
'COPILOTSTUDIOAGENT__'.

Keys:
environmentid: Environment ID of environment with the Copilot Studio App.
Expand Down
5 changes: 5 additions & 0 deletions python/packages/core/agent_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
SessionContext,
register_state_type,
)
from ._settings import SecretString, load_settings
from ._telemetry import (
AGENT_FRAMEWORK_USER_AGENT,
APP_INFO,
Expand All @@ -67,6 +68,7 @@
FunctionInvocationConfiguration,
FunctionInvocationLayer,
FunctionTool,
ToolTypes,
normalize_function_invocation_configuration,
tool,
)
Expand Down Expand Up @@ -234,6 +236,7 @@
"RoleLiteral",
"Runner",
"RunnerContext",
"SecretString",
"SessionContext",
"SingleEdgeGroup",
"SubWorkflowRequestMessage",
Expand All @@ -250,6 +253,7 @@
"SwitchCaseEdgeGroupDefault",
"TextSpanRegion",
"ToolMode",
"ToolTypes",
"TypeCompatibilityError",
"UpdateT",
"UsageDetails",
Expand Down Expand Up @@ -282,6 +286,7 @@
"executor",
"function_middleware",
"handler",
"load_settings",
"map_chat_to_agent_update",
"merge_chat_options",
"normalize_function_invocation_configuration",
Expand Down
40 changes: 29 additions & 11 deletions python/packages/core/agent_framework/_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class MySettings(TypedDict, total=False):
from contextlib import suppress
from typing import Any, Union, get_args, get_origin, get_type_hints

from dotenv import load_dotenv
from dotenv import dotenv_values

from .exceptions import SettingNotFoundError

Expand Down Expand Up @@ -172,14 +172,14 @@ def load_settings(
required_fields: Sequence[str | tuple[str, ...]] | None = None,
**overrides: Any,
) -> SettingsT:
"""Load settings from environment variables, a ``.env`` file, and explicit overrides.
"""Load settings from explicit overrides, an optional ``.env`` file, and environment variables.

The *settings_type* must be a ``TypedDict`` subclass. Values are resolved in
this order (highest priority first):

1. Explicit keyword *overrides* (``None`` values are filtered out).
2. Environment variables (``<env_prefix><FIELD_NAME>``).
3. A ``.env`` file (loaded via ``python-dotenv``; existing env vars take precedence).
2. A ``.env`` file (when *env_file_path* is explicitly provided).
3. Environment variables (``<env_prefix><FIELD_NAME>``).
4. Default values — fields with class-level defaults on the TypedDict, or
``None`` for optional fields.

Expand All @@ -192,7 +192,8 @@ def load_settings(
Args:
settings_type: A ``TypedDict`` class describing the settings schema.
env_prefix: Prefix for environment variable lookup (e.g. ``"OPENAI_"``).
env_file_path: Path to ``.env`` file. Defaults to ``".env"`` when omitted.
env_file_path: Path to ``.env`` file. When provided, the file is required
and values are resolved before process environment variables.
env_file_encoding: Encoding for reading the ``.env`` file. Defaults to ``"utf-8"``.
required_fields: Field names (``str``) that must resolve to a non-``None``
value, or tuples of field names where exactly one must be set.
Expand All @@ -203,16 +204,22 @@ def load_settings(
A populated dict matching *settings_type*.

Raises:
FileNotFoundError: If *env_file_path* was provided but the file does not exist.
SettingNotFoundError: If a required field could not be resolved from any
source, or if a mutually exclusive constraint is violated.
ServiceInitializationError: If an override value has an incompatible type.
"""
encoding = env_file_encoding or "utf-8"

# Load .env file if it exists (existing env vars take precedence by default)
env_path = env_file_path or ".env"
if os.path.isfile(env_path):
load_dotenv(dotenv_path=env_path, encoding=encoding)
loaded_dotenv_values: dict[str, str] = {}
if env_file_path is not None:
if not os.path.exists(env_file_path):
raise FileNotFoundError(env_file_path)

raw_dotenv_values = dotenv_values(dotenv_path=env_file_path, encoding=encoding)
loaded_dotenv_values = {
key: value for key, value in raw_dotenv_values.items() if key is not None and value is not None
}

# Filter out None overrides so defaults / env vars are preserved
overrides = {k: v for k, v in overrides.items() if v is not None}
Expand All @@ -235,8 +242,19 @@ def load_settings(
result[field_name] = override_value
continue

# 2. Environment variable
env_var_name = f"{env_prefix}{field_name.upper()}"

# 2. Optional .env value (only when env_file_path is explicitly provided)
if loaded_dotenv_values:
dotenv_value = loaded_dotenv_values.get(env_var_name)
if dotenv_value is not None:
try:
result[field_name] = _coerce_value(dotenv_value, field_type)
except (ValueError, TypeError):
result[field_name] = dotenv_value
continue

# 3. Environment variable
env_value = os.getenv(env_var_name)
if env_value is not None:
try:
Expand All @@ -245,7 +263,7 @@ def load_settings(
result[field_name] = env_value
continue

# 3. Default from TypedDict class-level defaults, or None for optional fields
# 4. Default from TypedDict class-level defaults, or None for optional fields
if hasattr(settings_type, field_name):
result[field_name] = getattr(settings_type, field_name)
else:
Expand Down
Loading