Python: Replace Pydantic Settings with TypedDict + load_settings() (#3843)

* Replace Pydantic Settings with TypedDict + load_settings()

- Remove pydantic-settings dependency, add python-dotenv
- Delete _pydantic.py (AFBaseSettings, HTTPsUrl)
- Add _settings.py with generic load_settings() function, SecretString,
  type coercion, and Required field validation (SettingNotFoundError)
- Convert all 13 settings classes from AFBaseSettings subclasses to
  TypedDict definitions with load_settings() calls
- Update all consumers from attribute access to dict access
- Add 20 unit tests for load_settings() covering basic loading, dotenv,
  SecretString, type coercion, and required field validation
- Update all existing tests for new settings patterns

* Fix mypy type errors from settings conversion

- Fix str | None attribute access in responses_client (walrus operator)
- Fix SecretString | None narrowing in bedrock (type: ignore after guard)
- Convert _context_provider.py attribute access to dict access (missed file)
- Fix endpoint type narrowing in search_provider and context_provider
- Fix purview: str | None .rstrip(), int | None defaults, urlparse bytes

* Address PR review: required_fields param, type validation, fixes

- Move required field validation from TypedDict annotations (Required)
  to a required_fields parameter on load_settings(), enabling runtime
  decisions about which fields are required
- Remove Required imports and restore from __future__ import annotations
  in ollama and foundry_local
- Add _check_override_type() for deterministic ServiceInitializationError
  on invalid override types (e.g. dict passed for str field)
- Fix all multi-exception test catches back to single exception type
- Fix Ollama host=None: use .get() so None is passed through to SDK default
- Fix Purview processor: use explicit is-None checks instead of or operator
- Remove unused BaseModel import from openai/_shared.py
- Add 4 new tests (24 total): required_fields param, type validation

* Fix type validation: allow int for float fields

_check_override_type now permits int values for float-typed fields,
matching Python's standard numeric promotion behavior.

* fix: wrap urlparse arg with str() to fix mypy bytes endswith error
This commit is contained in:
Eduard van Valkenburg
2026-02-12 09:51:20 +01:00
committed by GitHub
Unverified
parent b488158abe
commit 8457533c69
58 changed files with 1526 additions and 1113 deletions
@@ -30,13 +30,13 @@ from agent_framework import (
prepare_function_call_results,
validate_tool_mode,
)
from agent_framework._pydantic import AFBaseSettings
from agent_framework._settings import SecretString, load_settings
from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidResponseError
from agent_framework.observability import ChatTelemetryLayer
from boto3.session import Session as Boto3Session
from botocore.client import BaseClient
from botocore.config import Config as BotoConfig
from pydantic import BaseModel, SecretStr, ValidationError
from pydantic import BaseModel
if sys.version_info >= (3, 13):
from typing import TypeVar # type: ignore # pragma: no cover
@@ -205,16 +205,14 @@ FINISH_REASON_MAP: dict[str, FinishReasonLiteral] = {
}
class BedrockSettings(AFBaseSettings):
class BedrockSettings(TypedDict, total=False):
"""Bedrock configuration settings pulled from environment variables or .env files."""
env_prefix: ClassVar[str] = "BEDROCK_"
region: str = DEFAULT_REGION
chat_model_id: str | None = None
access_key: SecretStr | None = None
secret_key: SecretStr | None = None
session_token: SecretStr | None = None
region: str | None
chat_model_id: str | None
access_key: SecretString | None
secret_key: SecretString | None
session_token: SecretString | None
class BedrockChatClient(
@@ -280,24 +278,25 @@ class BedrockChatClient(
client = BedrockChatClient[MyOptions](model_id="<model name>")
response = await client.get_response("Hello", options={"my_custom_option": "value"})
"""
try:
settings = BedrockSettings(
region=region,
chat_model_id=model_id,
access_key=access_key, # type: ignore[arg-type]
secret_key=secret_key, # type: ignore[arg-type]
session_token=session_token, # type: ignore[arg-type]
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to initialize Bedrock settings.", ex) from ex
settings = load_settings(
BedrockSettings,
env_prefix="BEDROCK_",
region=region,
chat_model_id=model_id,
access_key=access_key,
secret_key=secret_key,
session_token=session_token,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
if not settings.get("region"):
settings["region"] = DEFAULT_REGION
if client is None:
session = boto3_session or self._create_session(settings)
client = session.client(
"bedrock-runtime",
region_name=settings.region,
region_name=settings["region"],
config=BotoConfig(user_agent_extra=AGENT_FRAMEWORK_USER_AGENT),
)
@@ -307,17 +306,17 @@ class BedrockChatClient(
**kwargs,
)
self._bedrock_client = client
self.model_id = settings.chat_model_id
self.region = settings.region
self.model_id = settings["chat_model_id"]
self.region = settings["region"]
@staticmethod
def _create_session(settings: BedrockSettings) -> Boto3Session:
session_kwargs: dict[str, Any] = {"region_name": settings.region or DEFAULT_REGION}
if settings.access_key and settings.secret_key:
session_kwargs["aws_access_key_id"] = settings.access_key.get_secret_value()
session_kwargs["aws_secret_access_key"] = settings.secret_key.get_secret_value()
if settings.session_token:
session_kwargs["aws_session_token"] = settings.session_token.get_secret_value()
session_kwargs: dict[str, Any] = {"region_name": settings.get("region") or DEFAULT_REGION}
if settings.get("access_key") and settings.get("secret_key"):
session_kwargs["aws_access_key_id"] = settings["access_key"].get_secret_value() # type: ignore[union-attr]
session_kwargs["aws_secret_access_key"] = settings["secret_key"].get_secret_value() # type: ignore[union-attr]
if settings.get("session_token"):
session_kwargs["aws_session_token"] = settings["session_token"].get_secret_value() # type: ignore[union-attr]
return Boto3Session(**session_kwargs)
@override
@@ -11,6 +11,7 @@ from agent_framework import (
FunctionTool,
Message,
)
from agent_framework._settings import load_settings
from pydantic import BaseModel
from agent_framework_bedrock._chat_client import BedrockChatClient, BedrockSettings
@@ -33,9 +34,9 @@ def _dummy_weather(location: str) -> str: # pragma: no cover - helper
def test_settings_load_from_environment(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("BEDROCK_REGION", "us-west-2")
monkeypatch.setenv("BEDROCK_CHAT_MODEL_ID", "anthropic.claude-v2")
settings = BedrockSettings()
assert settings.region == "us-west-2"
assert settings.chat_model_id == "anthropic.claude-v2"
settings = load_settings(BedrockSettings, env_prefix="BEDROCK_")
assert settings["region"] == "us-west-2"
assert settings["chat_model_id"] == "anthropic.claude-v2"
def test_build_request_includes_tool_config() -> None: