mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
f900febb6f
commit
534e5f5bf7
@@ -196,9 +196,9 @@ FINISH_REASON_MAP: dict[str, FinishReasonLiteral] = {
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-2
@@ -111,8 +111,9 @@ _DEFAULT_AGENTIC_MESSAGE_HISTORY_COUNT = 10
|
||||
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.
|
||||
|
||||
@@ -43,10 +43,9 @@ logger = logging.getLogger("agent_framework.azure")
|
||||
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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -28,9 +28,9 @@ from ._acquire_token import acquire_token
|
||||
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.
|
||||
|
||||
@@ -56,6 +56,7 @@ from ._sessions import (
|
||||
SessionContext,
|
||||
register_state_type,
|
||||
)
|
||||
from ._settings import SecretString, load_settings
|
||||
from ._telemetry import (
|
||||
AGENT_FRAMEWORK_USER_AGENT,
|
||||
APP_INFO,
|
||||
@@ -67,6 +68,7 @@ from ._tools import (
|
||||
FunctionInvocationConfiguration,
|
||||
FunctionInvocationLayer,
|
||||
FunctionTool,
|
||||
ToolTypes,
|
||||
normalize_function_invocation_configuration,
|
||||
tool,
|
||||
)
|
||||
@@ -234,6 +236,7 @@ __all__ = [
|
||||
"RoleLiteral",
|
||||
"Runner",
|
||||
"RunnerContext",
|
||||
"SecretString",
|
||||
"SessionContext",
|
||||
"SingleEdgeGroup",
|
||||
"SubWorkflowRequestMessage",
|
||||
@@ -250,6 +253,7 @@ __all__ = [
|
||||
"SwitchCaseEdgeGroupDefault",
|
||||
"TextSpanRegion",
|
||||
"ToolMode",
|
||||
"ToolTypes",
|
||||
"TypeCompatibilityError",
|
||||
"UpdateT",
|
||||
"UsageDetails",
|
||||
@@ -282,6 +286,7 @@ __all__ = [
|
||||
"executor",
|
||||
"function_middleware",
|
||||
"handler",
|
||||
"load_settings",
|
||||
"map_chat_to_agent_update",
|
||||
"merge_chat_options",
|
||||
"normalize_function_invocation_configuration",
|
||||
|
||||
@@ -36,7 +36,7 @@ from collections.abc import Callable, Sequence
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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}
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
|
||||
@@ -33,10 +33,9 @@ DEFAULT_AZURE_TOKEN_ENDPOINT: Final[str] = "https://cognitiveservices.azure.com/
|
||||
class AzureOpenAISettings(TypedDict, total=False):
|
||||
"""AzureOpenAI model settings.
|
||||
|
||||
The settings are first loaded from environment variables with the prefix 'AZURE_OPENAI_'.
|
||||
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_OPENAI_'. If settings are missing after resolution, validation will fail.
|
||||
|
||||
Keyword Args:
|
||||
endpoint: The endpoint of the Azure deployment. This value
|
||||
|
||||
@@ -440,7 +440,7 @@ def _get_exporters_from_env(
|
||||
|
||||
Args:
|
||||
env_file_path: Path to a .env file to load environment variables from.
|
||||
Default is None, which loads from '.env' if present.
|
||||
Default is None, which does not load a .env file.
|
||||
env_file_encoding: Encoding to use when reading the .env file.
|
||||
Default is None, which uses the system default encoding.
|
||||
|
||||
@@ -451,8 +451,9 @@ def _get_exporters_from_env(
|
||||
- https://opentelemetry.io/docs/languages/sdk-configuration/general/
|
||||
- https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/
|
||||
"""
|
||||
# Load environment variables from .env file if present
|
||||
load_dotenv(dotenv_path=env_file_path, encoding=env_file_encoding)
|
||||
# Load environment variables from a .env file only when explicitly provided
|
||||
if env_file_path is not None:
|
||||
load_dotenv(dotenv_path=env_file_path, encoding=env_file_encoding)
|
||||
|
||||
# Get base endpoint
|
||||
base_endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
|
||||
@@ -513,7 +514,7 @@ def create_resource(
|
||||
service_version: Override the service version. If not provided, reads from
|
||||
OTEL_SERVICE_VERSION environment variable or defaults to the package version.
|
||||
env_file_path: Path to a .env file to load environment variables from.
|
||||
Default is None, which loads from '.env' if present.
|
||||
Default is None, which does not load a .env file.
|
||||
env_file_encoding: Encoding to use when reading the .env file.
|
||||
Default is None, which uses the system default encoding.
|
||||
**attributes: Additional resource attributes to include. These will be merged
|
||||
@@ -541,8 +542,9 @@ def create_resource(
|
||||
# Load from custom .env file
|
||||
resource = create_resource(env_file_path="config/.env")
|
||||
"""
|
||||
# Load environment variables from .env file if present
|
||||
load_dotenv(dotenv_path=env_file_path, encoding=env_file_encoding)
|
||||
# Load environment variables from a .env file only when explicitly provided
|
||||
if env_file_path is not None:
|
||||
load_dotenv(dotenv_path=env_file_path, encoding=env_file_encoding)
|
||||
|
||||
# Start with provided attributes
|
||||
resource_attributes: dict[str, Any] = dict(attributes)
|
||||
|
||||
@@ -78,10 +78,9 @@ def _check_openai_version_for_callable_api_key() -> None:
|
||||
class OpenAISettings(TypedDict, total=False):
|
||||
"""OpenAI environment settings.
|
||||
|
||||
The settings are first loaded from environment variables with the prefix 'OPENAI_'.
|
||||
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
|
||||
'OPENAI_'. If settings are missing after resolution, validation will fail.
|
||||
|
||||
Keyword Args:
|
||||
api_key: OpenAI API key, see https://platform.openai.com/account/api-keys.
|
||||
|
||||
@@ -130,7 +130,7 @@ def test_azure_assistants_client_init_missing_deployment_name(azure_openai_unit_
|
||||
"""Test AzureOpenAIAssistantsClient initialization with missing deployment name."""
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
AzureOpenAIAssistantsClient(
|
||||
api_key=azure_openai_unit_test_env.get("AZURE_OPENAI_API_KEY", "test-key"), env_file_path="nonexistent.env"
|
||||
api_key=azure_openai_unit_test_env.get("AZURE_OPENAI_API_KEY", "test-key")
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -95,7 +95,6 @@ def test_init_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None:
|
||||
def test_init_with_empty_deployment_name(azure_openai_unit_test_env: dict[str, str]) -> None:
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
AzureOpenAIChatClient(
|
||||
env_file_path="test.env",
|
||||
)
|
||||
|
||||
|
||||
@@ -103,7 +102,6 @@ def test_init_with_empty_deployment_name(azure_openai_unit_test_env: dict[str, s
|
||||
def test_init_with_empty_endpoint_and_base_url(azure_openai_unit_test_env: dict[str, str]) -> None:
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
AzureOpenAIChatClient(
|
||||
env_file_path="test.env",
|
||||
)
|
||||
|
||||
|
||||
@@ -126,7 +124,6 @@ def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None:
|
||||
"api_key": azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"],
|
||||
"api_version": azure_openai_unit_test_env["AZURE_OPENAI_API_VERSION"],
|
||||
"default_headers": default_headers,
|
||||
"env_file_path": "test.env",
|
||||
}
|
||||
|
||||
azure_chat_client = AzureOpenAIChatClient.from_dict(settings)
|
||||
|
||||
@@ -112,7 +112,6 @@ def test_init_with_default_header(azure_openai_unit_test_env: dict[str, str]) ->
|
||||
def test_init_with_empty_model_id(azure_openai_unit_test_env: dict[str, str]) -> None:
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
AzureOpenAIResponsesClient(
|
||||
env_file_path="test.env",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ def span_exporter(monkeypatch, enable_instrumentation: bool, enable_sensitive_da
|
||||
importlib.reload(observability)
|
||||
|
||||
# recreate observability settings with values from above and no file.
|
||||
observability_settings = observability.ObservabilitySettings(env_file_path="test.env")
|
||||
observability_settings = observability.ObservabilitySettings()
|
||||
|
||||
# Configure providers manually without calling _configure() to avoid OTLP imports
|
||||
if enable_instrumentation or enable_sensitive_data:
|
||||
|
||||
@@ -901,7 +901,7 @@ def test_console_exporters_opt_in_false(monkeypatch):
|
||||
monkeypatch.setenv("ENABLE_CONSOLE_EXPORTERS", "false")
|
||||
monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False)
|
||||
|
||||
settings = ObservabilitySettings(env_file_path="test.env")
|
||||
settings = ObservabilitySettings()
|
||||
assert settings.enable_console_exporters is False
|
||||
|
||||
|
||||
@@ -911,7 +911,7 @@ def test_console_exporters_opt_in_true(monkeypatch):
|
||||
|
||||
monkeypatch.setenv("ENABLE_CONSOLE_EXPORTERS", "true")
|
||||
|
||||
settings = ObservabilitySettings(env_file_path="test.env")
|
||||
settings = ObservabilitySettings()
|
||||
assert settings.enable_console_exporters is True
|
||||
|
||||
|
||||
@@ -921,7 +921,7 @@ def test_console_exporters_default_false(monkeypatch):
|
||||
|
||||
monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False)
|
||||
|
||||
settings = ObservabilitySettings(env_file_path="test.env")
|
||||
settings = ObservabilitySettings()
|
||||
assert settings.enable_console_exporters is False
|
||||
|
||||
|
||||
@@ -996,7 +996,7 @@ def test_observability_settings_is_setup_initial(monkeypatch):
|
||||
from agent_framework.observability import ObservabilitySettings
|
||||
|
||||
monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False)
|
||||
settings = ObservabilitySettings(env_file_path="test.env")
|
||||
settings = ObservabilitySettings()
|
||||
assert settings.is_setup is False
|
||||
|
||||
|
||||
@@ -1464,7 +1464,7 @@ def test_observability_settings_configure_not_enabled(monkeypatch):
|
||||
from agent_framework.observability import ObservabilitySettings
|
||||
|
||||
monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false")
|
||||
settings = ObservabilitySettings(env_file_path="test.env")
|
||||
settings = ObservabilitySettings()
|
||||
|
||||
# Should not raise, should just return early
|
||||
settings._configure()
|
||||
@@ -1485,7 +1485,7 @@ def test_observability_settings_configure_already_setup(monkeypatch):
|
||||
]:
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
|
||||
settings = ObservabilitySettings(env_file_path="test.env")
|
||||
settings = ObservabilitySettings()
|
||||
|
||||
# Manually mark as set up
|
||||
settings._executed_setup = True
|
||||
@@ -2021,7 +2021,7 @@ def test_configure_providers_with_span_exporters(monkeypatch):
|
||||
]:
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
|
||||
settings = ObservabilitySettings(env_file_path="test.env")
|
||||
settings = ObservabilitySettings()
|
||||
|
||||
# Create mock span exporter
|
||||
mock_span_exporter = Mock(spec=SpanExporter)
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import TypedDict
|
||||
|
||||
import pytest
|
||||
|
||||
from agent_framework._settings import SecretString, load_settings
|
||||
from agent_framework import SecretString, load_settings
|
||||
|
||||
|
||||
class SimpleSettings(TypedDict, total=False):
|
||||
@@ -106,7 +106,7 @@ class TestDotenvFile:
|
||||
finally:
|
||||
os.unlink(env_path)
|
||||
|
||||
def test_env_vars_override_dotenv(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_dotenv_overrides_env_vars_when_env_file_path_is_set(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("TEST_APP_API_KEY", "real-env-key")
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=False) as f:
|
||||
@@ -117,15 +117,34 @@ class TestDotenvFile:
|
||||
try:
|
||||
settings = load_settings(SimpleSettings, env_prefix="TEST_APP_", env_file_path=env_path)
|
||||
|
||||
assert settings["api_key"] == "real-env-key"
|
||||
assert settings["api_key"] == "dotenv-key"
|
||||
finally:
|
||||
os.unlink(env_path)
|
||||
|
||||
def test_missing_dotenv_file(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("TEST_APP_API_KEY", raising=False)
|
||||
settings = load_settings(SimpleSettings, env_prefix="TEST_APP_", env_file_path="/nonexistent/.env")
|
||||
def test_env_vars_are_used_when_env_file_path_is_not_set(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("TEST_APP_API_KEY", "real-env-key")
|
||||
settings = load_settings(SimpleSettings, env_prefix="TEST_APP_")
|
||||
|
||||
assert settings["api_key"] is None
|
||||
assert settings["api_key"] == "real-env-key"
|
||||
|
||||
def test_overrides_beat_dotenv_and_env_vars(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("TEST_APP_TIMEOUT", "120")
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=False) as f:
|
||||
f.write("TEST_APP_TIMEOUT=90\n")
|
||||
f.flush()
|
||||
env_path = f.name
|
||||
|
||||
try:
|
||||
settings = load_settings(SimpleSettings, env_prefix="TEST_APP_", env_file_path=env_path, timeout=60)
|
||||
|
||||
assert settings["timeout"] == 60
|
||||
finally:
|
||||
os.unlink(env_path)
|
||||
|
||||
def test_missing_dotenv_file_raises(self) -> None:
|
||||
with pytest.raises(FileNotFoundError):
|
||||
load_settings(SimpleSettings, env_prefix="TEST_APP_", env_file_path="/nonexistent/.env")
|
||||
|
||||
|
||||
class TestSecretString:
|
||||
|
||||
@@ -155,7 +155,7 @@ def test_init_missing_model_id(openai_unit_test_env: dict[str, str]) -> None:
|
||||
"""Test OpenAIAssistantsClient initialization with missing model ID."""
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
OpenAIAssistantsClient(
|
||||
api_key=openai_unit_test_env.get("OPENAI_API_KEY", "test-key"), env_file_path="nonexistent.env"
|
||||
api_key=openai_unit_test_env.get("OPENAI_API_KEY", "test-key")
|
||||
)
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ def test_init_missing_model_id(openai_unit_test_env: dict[str, str]) -> None:
|
||||
def test_init_missing_api_key(openai_unit_test_env: dict[str, str]) -> None:
|
||||
"""Test OpenAIAssistantsClient initialization with missing API key."""
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
OpenAIAssistantsClient(model_id="gpt-4", env_file_path="nonexistent.env")
|
||||
OpenAIAssistantsClient(model_id="gpt-4")
|
||||
|
||||
|
||||
def test_init_with_default_headers(openai_unit_test_env: dict[str, str]) -> None:
|
||||
|
||||
@@ -98,7 +98,6 @@ def test_init_base_url_from_settings_env() -> None:
|
||||
def test_init_with_empty_model_id(openai_unit_test_env: dict[str, str]) -> None:
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
OpenAIChatClient(
|
||||
env_file_path="test.env",
|
||||
)
|
||||
|
||||
|
||||
@@ -109,7 +108,6 @@ def test_init_with_empty_api_key(openai_unit_test_env: dict[str, str]) -> None:
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
OpenAIChatClient(
|
||||
model_id=model_id,
|
||||
env_file_path="test.env",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -140,7 +140,6 @@ def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None:
|
||||
def test_init_with_empty_model_id(openai_unit_test_env: dict[str, str]) -> None:
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
OpenAIResponsesClient(
|
||||
env_file_path="test.env",
|
||||
)
|
||||
|
||||
|
||||
@@ -151,7 +150,6 @@ def test_init_with_empty_api_key(openai_unit_test_env: dict[str, str]) -> None:
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
OpenAIResponsesClient(
|
||||
model_id=model_id,
|
||||
env_file_path="test.env",
|
||||
)
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -118,9 +118,9 @@ FoundryLocalChatOptionsT = TypeVar(
|
||||
class FoundryLocalSettings(TypedDict, total=False):
|
||||
"""Foundry local model settings.
|
||||
|
||||
The settings are first loaded from environment variables with the prefix 'FOUNDRY_LOCAL_'.
|
||||
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
|
||||
'FOUNDRY_LOCAL_'.
|
||||
|
||||
Keys:
|
||||
model_id: The name of the model deployment to use.
|
||||
|
||||
@@ -15,7 +15,7 @@ from agent_framework_foundry_local._foundry_local_client import FoundryLocalSett
|
||||
|
||||
def test_foundry_local_settings_init_from_env(foundry_local_unit_test_env: dict[str, str]) -> None:
|
||||
"""Test FoundryLocalSettings initialization from environment variables."""
|
||||
settings = load_settings(FoundryLocalSettings, env_prefix="FOUNDRY_LOCAL_", env_file_path="test.env")
|
||||
settings = load_settings(FoundryLocalSettings, env_prefix="FOUNDRY_LOCAL_")
|
||||
|
||||
assert settings["model_id"] == foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL_ID"]
|
||||
|
||||
@@ -26,7 +26,6 @@ def test_foundry_local_settings_init_with_explicit_values() -> None:
|
||||
FoundryLocalSettings,
|
||||
env_prefix="FOUNDRY_LOCAL_",
|
||||
model_id="custom-model-id",
|
||||
env_file_path="test.env",
|
||||
)
|
||||
|
||||
assert settings["model_id"] == "custom-model-id"
|
||||
@@ -40,14 +39,13 @@ def test_foundry_local_settings_missing_model_id(foundry_local_unit_test_env: di
|
||||
FoundryLocalSettings,
|
||||
env_prefix="FOUNDRY_LOCAL_",
|
||||
required_fields=["model_id"],
|
||||
env_file_path="test.env",
|
||||
)
|
||||
|
||||
|
||||
def test_foundry_local_settings_explicit_overrides_env(foundry_local_unit_test_env: dict[str, str]) -> None:
|
||||
"""Test that explicit values override environment variables."""
|
||||
settings = load_settings(
|
||||
FoundryLocalSettings, env_prefix="FOUNDRY_LOCAL_", model_id="override-model-id", env_file_path="test.env"
|
||||
FoundryLocalSettings, env_prefix="FOUNDRY_LOCAL_", model_id="override-model-id"
|
||||
)
|
||||
|
||||
assert settings["model_id"] == "override-model-id"
|
||||
@@ -63,7 +61,7 @@ def test_foundry_local_client_init(mock_foundry_local_manager: MagicMock) -> Non
|
||||
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
|
||||
return_value=mock_foundry_local_manager,
|
||||
):
|
||||
client = FoundryLocalClient(model_id="test-model-id", env_file_path="test.env")
|
||||
client = FoundryLocalClient(model_id="test-model-id")
|
||||
|
||||
assert client.model_id == "test-model-id"
|
||||
assert client.manager is mock_foundry_local_manager
|
||||
@@ -76,7 +74,7 @@ def test_foundry_local_client_init_with_bootstrap_false(mock_foundry_local_manag
|
||||
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
|
||||
return_value=mock_foundry_local_manager,
|
||||
) as mock_manager_class:
|
||||
FoundryLocalClient(model_id="test-model-id", bootstrap=False, env_file_path="test.env")
|
||||
FoundryLocalClient(model_id="test-model-id", bootstrap=False)
|
||||
|
||||
mock_manager_class.assert_called_once_with(
|
||||
bootstrap=False,
|
||||
@@ -90,7 +88,7 @@ def test_foundry_local_client_init_with_timeout(mock_foundry_local_manager: Magi
|
||||
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
|
||||
return_value=mock_foundry_local_manager,
|
||||
) as mock_manager_class:
|
||||
FoundryLocalClient(model_id="test-model-id", timeout=60.0, env_file_path="test.env")
|
||||
FoundryLocalClient(model_id="test-model-id", timeout=60.0)
|
||||
|
||||
mock_manager_class.assert_called_once_with(
|
||||
bootstrap=True,
|
||||
@@ -109,7 +107,7 @@ def test_foundry_local_client_init_model_not_found(mock_foundry_local_manager: M
|
||||
),
|
||||
pytest.raises(ServiceInitializationError, match="not found in Foundry Local"),
|
||||
):
|
||||
FoundryLocalClient(model_id="unknown-model", env_file_path="test.env")
|
||||
FoundryLocalClient(model_id="unknown-model")
|
||||
|
||||
|
||||
def test_foundry_local_client_uses_model_info_id(mock_foundry_local_manager: MagicMock) -> None:
|
||||
@@ -122,7 +120,7 @@ def test_foundry_local_client_uses_model_info_id(mock_foundry_local_manager: Mag
|
||||
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
|
||||
return_value=mock_foundry_local_manager,
|
||||
):
|
||||
client = FoundryLocalClient(model_id="model-alias", env_file_path="test.env")
|
||||
client = FoundryLocalClient(model_id="model-alias")
|
||||
|
||||
assert client.model_id == "resolved-model-id"
|
||||
|
||||
@@ -135,7 +133,7 @@ def test_foundry_local_client_init_from_env(
|
||||
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
|
||||
return_value=mock_foundry_local_manager,
|
||||
):
|
||||
client = FoundryLocalClient(env_file_path="test.env")
|
||||
client = FoundryLocalClient()
|
||||
|
||||
assert client.model_id == foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL_ID"]
|
||||
|
||||
@@ -148,7 +146,7 @@ def test_foundry_local_client_init_with_device(mock_foundry_local_manager: Magic
|
||||
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
|
||||
return_value=mock_foundry_local_manager,
|
||||
):
|
||||
FoundryLocalClient(model_id="test-model-id", device=DeviceType.CPU, env_file_path="test.env")
|
||||
FoundryLocalClient(model_id="test-model-id", device=DeviceType.CPU)
|
||||
|
||||
mock_foundry_local_manager.get_model_info.assert_called_once_with(
|
||||
alias_or_model_id="test-model-id",
|
||||
@@ -177,7 +175,7 @@ def test_foundry_local_client_init_model_not_found_with_device(mock_foundry_loca
|
||||
),
|
||||
pytest.raises(ServiceInitializationError, match="unknown-model:GPU.*not found"),
|
||||
):
|
||||
FoundryLocalClient(model_id="unknown-model", device=DeviceType.GPU, env_file_path="test.env")
|
||||
FoundryLocalClient(model_id="unknown-model", device=DeviceType.GPU)
|
||||
|
||||
|
||||
def test_foundry_local_client_init_with_prepare_model_false(mock_foundry_local_manager: MagicMock) -> None:
|
||||
@@ -186,7 +184,7 @@ def test_foundry_local_client_init_with_prepare_model_false(mock_foundry_local_m
|
||||
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
|
||||
return_value=mock_foundry_local_manager,
|
||||
):
|
||||
FoundryLocalClient(model_id="test-model-id", prepare_model=False, env_file_path="test.env")
|
||||
FoundryLocalClient(model_id="test-model-id", prepare_model=False)
|
||||
|
||||
mock_foundry_local_manager.download_model.assert_not_called()
|
||||
mock_foundry_local_manager.load_model.assert_not_called()
|
||||
@@ -198,7 +196,7 @@ def test_foundry_local_client_init_calls_download_and_load(mock_foundry_local_ma
|
||||
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
|
||||
return_value=mock_foundry_local_manager,
|
||||
):
|
||||
FoundryLocalClient(model_id="test-model-id", env_file_path="test.env")
|
||||
FoundryLocalClient(model_id="test-model-id")
|
||||
|
||||
mock_foundry_local_manager.download_model.assert_called_once_with(
|
||||
alias_or_model_id="test-model-id",
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
from ._agent import GitHubCopilotAgent, GitHubCopilotOptions
|
||||
from ._settings import GitHubCopilotSettings
|
||||
from ._agent import GitHubCopilotAgent, GitHubCopilotOptions, GitHubCopilotSettings
|
||||
|
||||
try:
|
||||
__version__ = importlib.metadata.version(__name__)
|
||||
|
||||
@@ -40,8 +40,6 @@ from copilot.types import (
|
||||
)
|
||||
from copilot.types import Tool as CopilotTool
|
||||
|
||||
from ._settings import GitHubCopilotSettings
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from typing import TypeVar
|
||||
else:
|
||||
@@ -57,6 +55,30 @@ PermissionHandlerType = Callable[[PermissionRequest, dict[str, str]], Permission
|
||||
logger = logging.getLogger("agent_framework.github_copilot")
|
||||
|
||||
|
||||
class GitHubCopilotSettings(TypedDict, total=False):
|
||||
"""GitHub Copilot model settings.
|
||||
|
||||
Settings are resolved in this order: explicit keyword arguments, values from an
|
||||
explicitly provided .env file, then environment variables with the prefix
|
||||
'GITHUB_COPILOT_'.
|
||||
|
||||
Keys:
|
||||
cli_path: Path to the Copilot CLI executable.
|
||||
Can be set via environment variable GITHUB_COPILOT_CLI_PATH.
|
||||
model: Model to use (e.g., "gpt-5", "claude-sonnet-4").
|
||||
Can be set via environment variable GITHUB_COPILOT_MODEL.
|
||||
timeout: Request timeout in seconds.
|
||||
Can be set via environment variable GITHUB_COPILOT_TIMEOUT.
|
||||
log_level: CLI log level.
|
||||
Can be set via environment variable GITHUB_COPILOT_LOG_LEVEL.
|
||||
"""
|
||||
|
||||
cli_path: str | None
|
||||
model: str | None
|
||||
timeout: float | None
|
||||
log_level: str | None
|
||||
|
||||
|
||||
class GitHubCopilotOptions(TypedDict, total=False):
|
||||
"""GitHub Copilot-specific options."""
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
class GitHubCopilotSettings(TypedDict, total=False):
|
||||
"""GitHub Copilot model settings.
|
||||
|
||||
The settings are first loaded from environment variables with the prefix 'GITHUB_COPILOT_'.
|
||||
If the environment variables are not found, the settings can be loaded from a .env file
|
||||
with the encoding 'utf-8'.
|
||||
|
||||
Keys:
|
||||
cli_path: Path to the Copilot CLI executable.
|
||||
Can be set via environment variable GITHUB_COPILOT_CLI_PATH.
|
||||
model: Model to use (e.g., "gpt-5", "claude-sonnet-4").
|
||||
Can be set via environment variable GITHUB_COPILOT_MODEL.
|
||||
timeout: Request timeout in seconds.
|
||||
Can be set via environment variable GITHUB_COPILOT_TIMEOUT.
|
||||
log_level: CLI log level.
|
||||
Can be set via environment variable GITHUB_COPILOT_LOG_LEVEL.
|
||||
"""
|
||||
|
||||
cli_path: str | None
|
||||
model: str | None
|
||||
timeout: float | None
|
||||
log_level: str | None
|
||||
@@ -186,7 +186,6 @@ def test_with_invalid_settings(ollama_unit_test_env: dict[str, str]) -> None:
|
||||
OllamaChatClient(
|
||||
host="http://localhost:12345",
|
||||
model_id=None,
|
||||
env_file_path="test.env",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Agent Framework Python Observability
|
||||
# Agent Framework Observability
|
||||
|
||||
This sample folder shows how a Python application can be configured to send Agent Framework observability data to the Application Performance Management (APM) vendor(s) of your choice based on the OpenTelemetry standard.
|
||||
|
||||
@@ -222,7 +222,15 @@ This folder contains different samples demonstrating how to use telemetry in var
|
||||
1. Open a terminal and navigate to this folder: `python/samples/02-agents/observability/`. This is necessary for the `.env` file to be read correctly.
|
||||
2. Create a `.env` file if one doesn't already exist in this folder. Please refer to the [example file](./.env.example).
|
||||
> **Note**: You can start with just `ENABLE_INSTRUMENTATION=true` and add `OTEL_EXPORTER_OTLP_ENDPOINT` or other configuration as needed. If no exporters are configured, you can set `ENABLE_CONSOLE_EXPORTERS=true` for console output.
|
||||
3. Activate your python virtual environment, and then run `python configure_otel_providers_with_env_var.py` or others.
|
||||
3. Choose one environment-loading approach:
|
||||
- **A. Sample-managed loading (current samples):** run from this folder so the sample's `load_dotenv()` call can find `.env`.
|
||||
- **B. Shell/IDE-managed environment:** set/export environment variables directly, or use an IDE run configuration that injects env vars / `.env`.
|
||||
- **C. Explicit env file in code:** pass `env_file_path` to APIs like `configure_otel_providers(env_file_path=".env")` (or your own settings loader path).
|
||||
- **D. CLI-managed env file:** run with `uv` and pass the file explicitly, for example:
|
||||
`uv run --env-file=.env python configure_otel_providers_with_env_var.py`
|
||||
4. Activate your python virtual environment, then run a sample (for example `python configure_otel_providers_with_env_var.py`).
|
||||
|
||||
> If you do manual provider setup (e.g., Azure Monitor), call `enable_instrumentation()` to turn on Agent Framework telemetry code paths; if you want Agent Framework to configure exporters/providers for you, call `configure_otel_providers(...)`.
|
||||
|
||||
> Each sample will print the Operation/Trace ID, which can be used later for filtering logs and traces in Application Insights or Aspire Dashboard.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import tool
|
||||
from agent_framework import Message, tool
|
||||
from agent_framework.observability import enable_instrumentation
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from opentelemetry._logs import set_logger_provider
|
||||
@@ -66,7 +66,9 @@ def setup_metrics():
|
||||
set_meter_provider(meter_provider)
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
# NOTE: approval_mode="never_require" is for sample brevity.
|
||||
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py
|
||||
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
@tool(approval_mode="never_require")
|
||||
async def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
@@ -107,9 +109,9 @@ async def run_chat_client() -> None:
|
||||
message = "What's the weather in Amsterdam and in Paris?"
|
||||
print(f"User: {message}")
|
||||
print("Assistant: ", end="")
|
||||
async for chunk in client.get_response(message, tools=get_weather, stream=True):
|
||||
if str(chunk):
|
||||
print(str(chunk), end="")
|
||||
async for chunk in client.get_response([Message(role="user", text=message)], tools=get_weather, stream=True):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="")
|
||||
print("")
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
from random import randint
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
from agent_framework import tool
|
||||
from agent_framework import Message, tool
|
||||
from agent_framework.observability import get_tracer
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
from opentelemetry.trace import SpanKind
|
||||
@@ -20,13 +20,14 @@ This sample shows how you can configure observability of an application with zer
|
||||
It relies on the OpenTelemetry auto-instrumentation capabilities, and the observability setup
|
||||
is done via environment variables.
|
||||
|
||||
Follow the install guidance from https://opentelemetry.io/docs/zero-code/python/ to install the OpenTelemetry CLI tool.
|
||||
Follow the install guidance from https://opentelemetry.io/docs/zero-code/python/ to install the OpenTelemetry CLI tool,
|
||||
when using `uv` there are some additional steps, so follow the instructions carefully.
|
||||
|
||||
And setup a local OpenTelemetry Collector instance to receive the traces and metrics (and update the endpoint below).
|
||||
|
||||
Then you can run:
|
||||
```bash
|
||||
opentelemetry-enable_instrumentation \
|
||||
opentelemetry-instrument \
|
||||
--traces_exporter otlp \
|
||||
--metrics_exporter otlp \
|
||||
--service_name agent_framework \
|
||||
@@ -40,7 +41,9 @@ You can also set the environment variables instead of passing them as CLI argume
|
||||
"""
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
# NOTE: approval_mode="never_require" is for sample brevity.
|
||||
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py
|
||||
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
@tool(approval_mode="never_require")
|
||||
async def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
@@ -81,12 +84,12 @@ async def run_chat_client(client: "SupportsChatGetResponse", stream: bool = Fals
|
||||
print(f"User: {message}")
|
||||
if stream:
|
||||
print("Assistant: ", end="")
|
||||
async for chunk in client.get_response(message, tools=get_weather, stream=True):
|
||||
if str(chunk):
|
||||
print(str(chunk), end="")
|
||||
async for chunk in client.get_response([Message(role="user", text=message)], tools=get_weather, stream=True):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="")
|
||||
print("")
|
||||
else:
|
||||
response = await client.get_response(message, tools=get_weather)
|
||||
response = await client.get_response([Message(role="user", text=message)], tools=get_weather)
|
||||
print(f"Assistant: {response}")
|
||||
|
||||
|
||||
|
||||
@@ -53,11 +53,7 @@ async def main():
|
||||
for question in questions:
|
||||
print(f"\nUser: {question}")
|
||||
print(f"{agent.name}: ", end="")
|
||||
async for update in agent.run(
|
||||
question,
|
||||
session=session,
|
||||
stream=True,
|
||||
):
|
||||
async for update in agent.run(question, session=session, stream=True):
|
||||
if update.text:
|
||||
print(update.text, end="")
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@ import os
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
import dotenv
|
||||
from agent_framework import Agent, tool
|
||||
from agent_framework.observability import create_resource, enable_instrumentation, get_tracer
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from azure.monitor.opentelemetry import configure_azure_monitor
|
||||
from dotenv import load_dotenv
|
||||
from opentelemetry.trace import SpanKind
|
||||
from opentelemetry.trace.span import format_trace_id
|
||||
from pydantic import Field
|
||||
@@ -36,12 +36,14 @@ So ensure you have the `azure-monitor-opentelemetry` package installed.
|
||||
"""
|
||||
|
||||
# For loading the `AZURE_AI_PROJECT_ENDPOINT` environment variable
|
||||
dotenv.load_dotenv()
|
||||
load_dotenv()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
# NOTE: approval_mode="never_require" is for sample brevity.
|
||||
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py
|
||||
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
@tool(approval_mode="never_require")
|
||||
async def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
|
||||
@@ -5,12 +5,12 @@ import os
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
import dotenv
|
||||
from agent_framework import Agent, tool
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from agent_framework.observability import get_tracer
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from opentelemetry.trace import SpanKind
|
||||
from opentelemetry.trace.span import format_trace_id
|
||||
from pydantic import Field
|
||||
@@ -26,10 +26,12 @@ for this sample to work.
|
||||
"""
|
||||
|
||||
# For loading the `AZURE_AI_PROJECT_ENDPOINT` environment variable
|
||||
dotenv.load_dotenv()
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
# NOTE: approval_mode="never_require" is for sample brevity.
|
||||
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py
|
||||
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
@tool(approval_mode="never_require")
|
||||
async def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
|
||||
@@ -6,7 +6,7 @@ from contextlib import suppress
|
||||
from random import randint
|
||||
from typing import TYPE_CHECKING, Annotated, Literal
|
||||
|
||||
from agent_framework import tool
|
||||
from agent_framework import Message, tool
|
||||
from agent_framework.observability import configure_otel_providers, get_tracer
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
from opentelemetry import trace
|
||||
@@ -31,7 +31,9 @@ output traces, logs, and metrics to the console.
|
||||
SCENARIOS = ["client", "client_stream", "tool", "all"]
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
# NOTE: approval_mode="never_require" is for sample brevity.
|
||||
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py
|
||||
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
@tool(approval_mode="never_require")
|
||||
async def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
@@ -71,12 +73,14 @@ async def run_chat_client(client: "SupportsChatGetResponse", stream: bool = Fals
|
||||
print(f"User: {message}")
|
||||
if stream:
|
||||
print("Assistant: ", end="")
|
||||
async for chunk in client.get_response(message, tools=get_weather, stream=True):
|
||||
if str(chunk):
|
||||
print(str(chunk), end="")
|
||||
async for chunk in client.get_response(
|
||||
[Message(role="user", text=message)], tools=get_weather, stream=True
|
||||
):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="")
|
||||
print("")
|
||||
else:
|
||||
response = await client.get_response(message, tools=get_weather)
|
||||
response = await client.get_response([Message(role="user", text=message)], tools=get_weather)
|
||||
print(f"Assistant: {response}")
|
||||
|
||||
|
||||
@@ -92,8 +96,7 @@ async def run_tool() -> None:
|
||||
"""
|
||||
with get_tracer().start_as_current_span("Scenario: AI Function", kind=trace.SpanKind.CLIENT):
|
||||
print("Running scenario: AI Function")
|
||||
func = tool(get_weather)
|
||||
weather = await func.invoke(location="Amsterdam")
|
||||
weather = await get_weather.invoke(location="Amsterdam")
|
||||
print(f"Weather in Amsterdam:\n{weather}")
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from contextlib import suppress
|
||||
from random import randint
|
||||
from typing import TYPE_CHECKING, Annotated, Literal
|
||||
|
||||
from agent_framework import tool
|
||||
from agent_framework import Message, tool
|
||||
from agent_framework.observability import configure_otel_providers, get_tracer
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
from opentelemetry import trace
|
||||
@@ -74,12 +74,14 @@ async def run_chat_client(client: "SupportsChatGetResponse", stream: bool = Fals
|
||||
print(f"User: {message}")
|
||||
if stream:
|
||||
print("Assistant: ", end="")
|
||||
async for chunk in client.get_response(message, stream=True, tools=get_weather):
|
||||
if str(chunk):
|
||||
print(str(chunk), end="")
|
||||
async for chunk in client.get_response(
|
||||
[Message(role="user", text=message)], stream=True, tools=get_weather
|
||||
):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="")
|
||||
print("")
|
||||
else:
|
||||
response = await client.get_response(message, tools=get_weather)
|
||||
response = await client.get_response([Message(role="user", text=message)], tools=get_weather)
|
||||
print(f"Assistant: {response}")
|
||||
|
||||
|
||||
@@ -95,8 +97,7 @@ async def run_tool() -> None:
|
||||
"""
|
||||
with get_tracer().start_as_current_span("Scenario: AI Function", kind=trace.SpanKind.CLIENT):
|
||||
print("Running scenario: AI Function")
|
||||
func = tool(get_weather)
|
||||
weather = await func.invoke(location="Amsterdam")
|
||||
weather = await get_weather.invoke(location="Amsterdam")
|
||||
print(f"Weather in Amsterdam:\n{weather}")
|
||||
|
||||
|
||||
|
||||
@@ -7,16 +7,6 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
from pytest import MonkeyPatch, mark, param
|
||||
from samples.getting_started.agents.azure_ai.azure_ai_with_function_tools import (
|
||||
mixed_tools_example as azure_ai_with_function_tools_mixed,
|
||||
)
|
||||
from samples.getting_started.agents.azure_ai.azure_ai_with_function_tools import (
|
||||
tools_on_agent_level as azure_ai_with_function_tools_agent,
|
||||
)
|
||||
from samples.getting_started.agents.azure_ai.azure_ai_with_function_tools import (
|
||||
tools_on_run_level as azure_ai_with_function_tools_run,
|
||||
)
|
||||
|
||||
from samples.getting_started.agents.azure_ai.azure_ai_basic import (
|
||||
main as azure_ai_basic,
|
||||
)
|
||||
@@ -29,6 +19,15 @@ from samples.getting_started.agents.azure_ai.azure_ai_with_existing_agent import
|
||||
from samples.getting_started.agents.azure_ai.azure_ai_with_explicit_settings import (
|
||||
main as azure_ai_with_explicit_settings,
|
||||
)
|
||||
from samples.getting_started.agents.azure_ai.azure_ai_with_function_tools import (
|
||||
mixed_tools_example as azure_ai_with_function_tools_mixed,
|
||||
)
|
||||
from samples.getting_started.agents.azure_ai.azure_ai_with_function_tools import (
|
||||
tools_on_agent_level as azure_ai_with_function_tools_agent,
|
||||
)
|
||||
from samples.getting_started.agents.azure_ai.azure_ai_with_function_tools import (
|
||||
tools_on_run_level as azure_ai_with_function_tools_run,
|
||||
)
|
||||
from samples.getting_started.agents.azure_ai.azure_ai_with_local_mcp import (
|
||||
main as azure_ai_with_local_mcp,
|
||||
)
|
||||
|
||||
@@ -7,7 +7,6 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
from pytest import MonkeyPatch, mark, param
|
||||
|
||||
from samples.getting_started.client.azure_ai_chat_client import (
|
||||
main as azure_ai_chat_client,
|
||||
)
|
||||
|
||||
@@ -7,7 +7,6 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
from pytest import MonkeyPatch, mark, param
|
||||
|
||||
from samples.getting_started.threads.custom_chat_message_store_thread import main as threads_custom_store
|
||||
from samples.getting_started.threads.suspend_resume_thread import main as threads_suspend_resume
|
||||
|
||||
|
||||
Reference in New Issue
Block a user