Python: improve .env handling and observability samples (#4032)

* Python: improve .env precedence and observability samples

- Switch load_settings to explicit precedence: overrides -> explicit .env -> environment -> defaults\n- Raise when env_file_path is provided but missing\n- Update settings docs and tests for new behavior\n- Refresh observability samples and README guidance for env loading options\n\nCloses #3864\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fixed some imports

* Fix load_settings CI regressions

Allow explicit env_file_path values that exist but are not regular files (for example /dev/null) by checking path existence before dotenv parsing, and restore a dict accumulator with typed return cast to satisfy mypy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Avoid implicit dotenv in observability

Only load dotenv in observability helpers when env_file_path is explicitly provided, and remove test os.devnull workarounds that are no longer necessary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-02-18 12:18:52 +01:00
committed by GitHub
Unverified
parent f900febb6f
commit 534e5f5bf7
41 changed files with 250 additions and 221 deletions
@@ -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__)
@@ -13,6 +13,7 @@ from agent_framework import (
AgentMiddlewareTypes,
AgentResponse,
AgentResponseUpdate,
AgentRunInputs,
AgentSession,
BaseAgent,
BaseContextProvider,
@@ -20,11 +21,11 @@ from agent_framework import (
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,
@@ -38,8 +39,6 @@ from claude_agent_sdk import (
)
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:
@@ -60,16 +59,40 @@ if TYPE_CHECKING:
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."""
@@ -402,18 +425,18 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
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():
@@ -1,29 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import TypedDict
__all__ = ["ClaudeAgentSettings"]
class ClaudeAgentSettings(TypedDict, total=False):
"""Claude Agent settings.
The settings are first loaded from environment variables with the prefix 'CLAUDE_AGENT_'.
If the environment variables are not found, the settings can be loaded from a .env file
with the encoding 'utf-8'.
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