From 30ae3bf6dc5f2f10909b3a8051d868bdbcb9a3b1 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Tue, 16 Dec 2025 20:57:00 +0100 Subject: [PATCH 1/5] redo foundry local chat client --- python/packages/foundry_local/LICENSE | 21 +++ python/packages/foundry_local/README.md | 9 ++ .../agent_framework_foundry_local/__init__.py | 15 ++ .../_foundry_local_client.py | 73 ++++++++++ .../agent_framework_foundry_local/py.typed | 0 python/packages/foundry_local/pyproject.toml | 87 ++++++++++++ .../packages/foundry_local/tests/conftest.py | 55 ++++++++ .../tests/test_foundry_local_client.py | 130 ++++++++++++++++++ python/pyproject.toml | 1 + .../agents/foundry_local/foundry_basic.py | 83 +++++++++++ python/uv.lock | 29 ++++ 11 files changed, 503 insertions(+) create mode 100644 python/packages/foundry_local/LICENSE create mode 100644 python/packages/foundry_local/README.md create mode 100644 python/packages/foundry_local/agent_framework_foundry_local/__init__.py create mode 100644 python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py create mode 100644 python/packages/foundry_local/agent_framework_foundry_local/py.typed create mode 100644 python/packages/foundry_local/pyproject.toml create mode 100644 python/packages/foundry_local/tests/conftest.py create mode 100644 python/packages/foundry_local/tests/test_foundry_local_client.py create mode 100644 python/samples/getting_started/agents/foundry_local/foundry_basic.py diff --git a/python/packages/foundry_local/LICENSE b/python/packages/foundry_local/LICENSE new file mode 100644 index 0000000000..9e841e7a26 --- /dev/null +++ b/python/packages/foundry_local/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/python/packages/foundry_local/README.md b/python/packages/foundry_local/README.md new file mode 100644 index 0000000000..021a83b574 --- /dev/null +++ b/python/packages/foundry_local/README.md @@ -0,0 +1,9 @@ +# Get Started with Microsoft Agent Framework Foundry + +Please install this package as the extra for `agent-framework`: + +```bash +pip install agent-framework[foundry] +``` + +and see the [README](https://github.com/microsoft/agent-framework/tree/main/python/README.md) for more information. diff --git a/python/packages/foundry_local/agent_framework_foundry_local/__init__.py b/python/packages/foundry_local/agent_framework_foundry_local/__init__.py new file mode 100644 index 0000000000..0d59ce118a --- /dev/null +++ b/python/packages/foundry_local/agent_framework_foundry_local/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft. All rights reserved. + +import importlib.metadata + +from ._foundry_local_client import FoundryLocalChatClient + +try: + __version__ = importlib.metadata.version(__name__) +except importlib.metadata.PackageNotFoundError: + __version__ = "0.0.0" # Fallback for development mode + +__all__ = [ + "FoundryLocalChatClient", + "__version__", +] diff --git a/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py b/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py new file mode 100644 index 0000000000..adc4cdbf77 --- /dev/null +++ b/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py @@ -0,0 +1,73 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import Any, ClassVar + +from agent_framework import use_chat_middleware, use_function_invocation +from agent_framework._pydantic import AFBaseSettings +from agent_framework.exceptions import ServiceInitializationError +from agent_framework.observability import use_instrumentation +from agent_framework.openai._chat_client import OpenAIBaseChatClient +from foundry_local import FoundryLocalManager +from openai import AsyncOpenAI + +__all__ = [ + "FoundryLocalChatClient", +] + + +class FoundryLocalSettings(AFBaseSettings): + """Foundry local model settings. + + The settings are first loaded from environment variables with the prefix 'FOUNDRY_'. + 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. + + Attributes: + model_id: The name of the model deployment to use. + (Env var FOUNDRY_LOCAL_MODEL_ID) + Parameters: + 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'. + """ + + env_prefix: ClassVar[str] = "FOUNDRY_LOCAL_" + + model_id: str + + +@use_function_invocation +@use_instrumentation +@use_chat_middleware +class FoundryLocalChatClient(OpenAIBaseChatClient): + """Foundry Local Chat completion class.""" + + def __init__( + self, + *, + model_id: str | None = None, + bootstrap: bool = True, + timeout: float | None = None, + env_file_path: str | None = None, + env_file_encoding: str = "utf-8", + **kwargs: Any, + ) -> None: + """Initialize a FoundryLocal ChatClient.""" + settings = FoundryLocalSettings( + model_id=model_id, # type: ignore + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + manager = FoundryLocalManager(alias_or_model_id=settings.model_id, bootstrap=bootstrap, timeout=timeout) + model_info = manager.get_model_info(settings.model_id) + if not model_info: + raise ServiceInitializationError( + f"Model with ID or alias '{settings.model_id}' not found in Foundry Local." + ) + async_client = AsyncOpenAI(base_url=manager.endpoint, api_key=manager.api_key) + args: dict[str, Any] = { + "model_id": model_info.id, + "client": async_client, + } + super().__init__(**args) + self.manager = manager diff --git a/python/packages/foundry_local/agent_framework_foundry_local/py.typed b/python/packages/foundry_local/agent_framework_foundry_local/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/packages/foundry_local/pyproject.toml b/python/packages/foundry_local/pyproject.toml new file mode 100644 index 0000000000..380ae0a3e6 --- /dev/null +++ b/python/packages/foundry_local/pyproject.toml @@ -0,0 +1,87 @@ +[project] +name = "agent-framework-foundry-local" +description = "Foundry Local integration for Microsoft Agent Framework." +authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] +readme = "README.md" +requires-python = ">=3.10" +version = "1.0.0b20251216" +license-files = ["LICENSE"] +urls.homepage = "https://aka.ms/agent-framework" +urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" +urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true" +urls.issues = "https://github.com/microsoft/agent-framework/issues" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Typing :: Typed", +] +dependencies = [ + "agent-framework-core", + "foundry-local-sdk>=0.5.1,<1", +] + +[tool.uv] +prerelease = "if-necessary-or-explicit" +environments = [ + "sys_platform == 'darwin'", + "sys_platform == 'linux'", + "sys_platform == 'win32'" +] + +[tool.uv-dynamic-versioning] +fallback-version = "0.0.0" +[tool.pytest.ini_options] +testpaths = 'tests' +addopts = "-ra -q -r fEX" +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +filterwarnings = [] +timeout = 120 + +[tool.ruff] +extend = "../../pyproject.toml" + +[tool.coverage.run] +omit = [ + "**/__init__.py" +] + +[tool.pyright] +extends = "../../pyproject.toml" +exclude = ['tests'] + +[tool.mypy] +plugins = ['pydantic.mypy'] +strict = true +python_version = "3.10" +ignore_missing_imports = true +disallow_untyped_defs = true +no_implicit_optional = true +check_untyped_defs = true +warn_return_any = true +show_error_codes = true +warn_unused_ignores = false +disallow_incomplete_defs = true +disallow_untyped_decorators = true + +[tool.bandit] +targets = ["agent_framework_foundry_local"] +exclude_dirs = ["tests"] + +[tool.poe] +executor.type = "uv" +include = "../../shared_tasks.toml" +[tool.poe.tasks] +mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_foundry" +test = "pytest --cov=agent_framework_foundry_local --cov-report=term-missing:skip-covered tests" + +[build-system] +requires = ["flit-core >= 3.11,<4.0"] +build-backend = "flit_core.buildapi" diff --git a/python/packages/foundry_local/tests/conftest.py b/python/packages/foundry_local/tests/conftest.py new file mode 100644 index 0000000000..0afc223356 --- /dev/null +++ b/python/packages/foundry_local/tests/conftest.py @@ -0,0 +1,55 @@ +# Copyright (c) Microsoft. All rights reserved. +from typing import Any +from unittest.mock import MagicMock + +from pytest import fixture + + +@fixture +def exclude_list(request: Any) -> list[str]: + """Fixture that returns a list of environment variables to exclude.""" + return request.param if hasattr(request, "param") else [] + + +@fixture +def override_env_param_dict(request: Any) -> dict[str, str]: + """Fixture that returns a dict of environment variables to override.""" + return request.param if hasattr(request, "param") else {} + + +@fixture() +def foundry_local_unit_test_env(monkeypatch: Any, exclude_list: list[str], override_env_param_dict: dict[str, str]): + """Fixture to set environment variables for FoundryLocalSettings.""" + if exclude_list is None: + exclude_list = [] + + if override_env_param_dict is None: + override_env_param_dict = {} + + env_vars = { + "FOUNDRY_LOCAL_MODEL_ID": "test-model-id", + } + + env_vars.update(override_env_param_dict) + + for key, value in env_vars.items(): + if key in exclude_list: + monkeypatch.delenv(key, raising=False) + continue + monkeypatch.setenv(key, value) + + return env_vars + + +@fixture +def mock_foundry_local_manager() -> MagicMock: + """Fixture that provides a mock FoundryLocalManager.""" + mock_manager = MagicMock() + mock_manager.endpoint = "http://localhost:5272/v1" + mock_manager.api_key = "test-api-key" + + mock_model_info = MagicMock() + mock_model_info.id = "test-model-id" + mock_manager.get_model_info.return_value = mock_model_info + + return mock_manager diff --git a/python/packages/foundry_local/tests/test_foundry_local_client.py b/python/packages/foundry_local/tests/test_foundry_local_client.py new file mode 100644 index 0000000000..a839fcca4c --- /dev/null +++ b/python/packages/foundry_local/tests/test_foundry_local_client.py @@ -0,0 +1,130 @@ +# Copyright (c) Microsoft. All rights reserved. + +from unittest.mock import MagicMock, patch + +import pytest +from agent_framework import ChatClientProtocol +from agent_framework.exceptions import ServiceInitializationError +from pydantic import ValidationError + +from agent_framework_foundry_local import FoundryLocalChatClient +from agent_framework_foundry_local._foundry_local_client import FoundryLocalSettings + +# Settings Tests + + +def test_foundry_local_settings_init_from_env(foundry_local_unit_test_env: dict[str, str]) -> None: + """Test FoundryLocalSettings initialization from environment variables.""" + settings = FoundryLocalSettings(env_file_path="test.env") + + assert settings.model_id == foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL_ID"] + + +def test_foundry_local_settings_init_with_explicit_values() -> None: + """Test FoundryLocalSettings initialization with explicit values.""" + settings = FoundryLocalSettings(model_id="custom-model-id", env_file_path="test.env") + + assert settings.model_id == "custom-model-id" + + +@pytest.mark.parametrize("exclude_list", [["FOUNDRY_LOCAL_MODEL_ID"]], indirect=True) +def test_foundry_local_settings_missing_model_id(foundry_local_unit_test_env: dict[str, str]) -> None: + """Test FoundryLocalSettings when model_id is missing raises ValidationError.""" + with pytest.raises(ValidationError): + FoundryLocalSettings(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 = FoundryLocalSettings(model_id="override-model-id", env_file_path="test.env") + + assert settings.model_id == "override-model-id" + assert settings.model_id != foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL_ID"] + + +# Client Initialization Tests + + +def test_foundry_local_client_init(mock_foundry_local_manager: MagicMock) -> None: + """Test FoundryLocalChatClient initialization with mocked manager.""" + with patch( + "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", + return_value=mock_foundry_local_manager, + ): + client = FoundryLocalChatClient(model_id="test-model-id", env_file_path="test.env") + + assert client.model_id == "test-model-id" + assert client.manager is mock_foundry_local_manager + assert isinstance(client, ChatClientProtocol) + + +def test_foundry_local_client_init_with_bootstrap_false(mock_foundry_local_manager: MagicMock) -> None: + """Test FoundryLocalChatClient initialization with bootstrap=False.""" + with patch( + "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", + return_value=mock_foundry_local_manager, + ) as mock_manager_class: + FoundryLocalChatClient(model_id="test-model-id", bootstrap=False, env_file_path="test.env") + + mock_manager_class.assert_called_once_with( + alias_or_model_id="test-model-id", + bootstrap=False, + timeout=None, + ) + + +def test_foundry_local_client_init_with_timeout(mock_foundry_local_manager: MagicMock) -> None: + """Test FoundryLocalChatClient initialization with custom timeout.""" + with patch( + "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", + return_value=mock_foundry_local_manager, + ) as mock_manager_class: + FoundryLocalChatClient(model_id="test-model-id", timeout=60.0, env_file_path="test.env") + + mock_manager_class.assert_called_once_with( + alias_or_model_id="test-model-id", + bootstrap=True, + timeout=60.0, + ) + + +def test_foundry_local_client_init_model_not_found(mock_foundry_local_manager: MagicMock) -> None: + """Test FoundryLocalChatClient initialization when model is not found.""" + mock_foundry_local_manager.get_model_info.return_value = None + + with ( + patch( + "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", + return_value=mock_foundry_local_manager, + ), + pytest.raises(ServiceInitializationError, match="not found in Foundry Local"), + ): + FoundryLocalChatClient(model_id="unknown-model", env_file_path="test.env") + + +def test_foundry_local_client_uses_model_info_id(mock_foundry_local_manager: MagicMock) -> None: + """Test that client uses the model ID from model_info, not the alias.""" + mock_model_info = MagicMock() + mock_model_info.id = "resolved-model-id" + mock_foundry_local_manager.get_model_info.return_value = mock_model_info + + with patch( + "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", + return_value=mock_foundry_local_manager, + ): + client = FoundryLocalChatClient(model_id="model-alias", env_file_path="test.env") + + assert client.model_id == "resolved-model-id" + + +def test_foundry_local_client_init_from_env( + foundry_local_unit_test_env: dict[str, str], mock_foundry_local_manager: MagicMock +) -> None: + """Test FoundryLocalChatClient initialization using environment variables.""" + with patch( + "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", + return_value=mock_foundry_local_manager, + ): + client = FoundryLocalChatClient(env_file_path="test.env") + + assert client.model_id == foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL_ID"] diff --git a/python/pyproject.toml b/python/pyproject.toml index ecee05161e..48dd976ad5 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -94,6 +94,7 @@ agent-framework-chatkit = { workspace = true } agent-framework-copilotstudio = { workspace = true } agent-framework-declarative = { workspace = true } agent-framework-devui = { workspace = true } +agent-framework-foundry-local = { workspace = true } agent-framework-lab = { workspace = true } agent-framework-mem0 = { workspace = true } agent-framework-ollama = { workspace = true } diff --git a/python/samples/getting_started/agents/foundry_local/foundry_basic.py b/python/samples/getting_started/agents/foundry_local/foundry_basic.py new file mode 100644 index 0000000000..98b3da0d50 --- /dev/null +++ b/python/samples/getting_started/agents/foundry_local/foundry_basic.py @@ -0,0 +1,83 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +from random import randint +from typing import Annotated + +from agent_framework_foundry_local import FoundryLocalChatClient + +""" +This samples demonstrates basic usage of FoundryLocalChatClient. +Shows both streaming and non-streaming responses with function tools. + +Running this sample the first time will be slow, as the model needs to be +downloaded and initialized. + +Also, not every model supports function calling, so be sure to check the +model capabilities in the Foundry catalog. +""" + + +def get_weather( + location: Annotated[str, "The location to get the weather for."], +) -> str: + """Get the weather for a given location.""" + conditions = ["sunny", "cloudy", "rainy", "stormy"] + return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." + + +async def non_streaming_example(client: FoundryLocalChatClient) -> None: + """Example of non-streaming response (get the complete result at once).""" + print("=== Non-streaming Response Example ===") + + # Since no Agent ID is provided, the agent will be automatically created + # and deleted after getting a response + # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred + # authentication option. + + agent = client.create_agent( + name="LocalAgent", + instructions="You are a helpful agent.", + tools=get_weather, + ) + query = "Whats the weather like in Seattle?" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result}\n") + + +async def streaming_example(client: FoundryLocalChatClient) -> None: + """Example of streaming response (get results as they are generated).""" + print("=== Streaming Response Example ===") + + agent = client.create_agent( + name="LocalAgent", + instructions="You are a helpful agent.", + tools=get_weather, + ) + query = "Whats the weather like in Amsterdam?" + print(f"User: {query}") + print("Agent: ", end="", flush=True) + async for chunk in agent.run_stream(query): + if chunk.text: + print(chunk.text, end="", flush=True) + print("\n") + + +async def main() -> None: + print("=== Basic Foundry Chat Client Agent Example ===") + + client = FoundryLocalChatClient(model_id="phi-4-mini") + print(f"Client Model ID: {client.model_id}\n") + print("Other available models:") + for model in client.manager.list_catalog_models(): + print( + f"- {model.alias} for {model.task} - id={model.id} - {(model.file_size_mb / 1000):.2f} GB - {model.license}" + ) + + await non_streaming_example(client) + await streaming_example(client) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/uv.lock b/python/uv.lock index 6a5252e49d..2096622f66 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -38,6 +38,7 @@ members = [ "agent-framework-core", "agent-framework-declarative", "agent-framework-devui", + "agent-framework-foundry-local", "agent-framework-lab", "agent-framework-mem0", "agent-framework-ollama", @@ -429,6 +430,21 @@ requires-dist = [ ] provides-extras = ["dev", "all"] +[[package]] +name = "agent-framework-foundry-local" +version = "1.0.0b20251216" +source = { editable = "packages/foundry_local" } +dependencies = [ + { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "foundry-local-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[package.metadata] +requires-dist = [ + { name = "agent-framework-core", editable = "packages/core" }, + { name = "foundry-local-sdk", specifier = ">=0.5.1,<1" }, +] + [[package]] name = "agent-framework-lab" version = "1.0.0b251218" @@ -2039,6 +2055,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, ] +[[package]] +name = "foundry-local-sdk" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/6b/76a7fe8f9f4c52cc84eaa1cd1b66acddf993496d55d6ea587bf0d0854d1c/foundry_local_sdk-0.5.1-py3-none-any.whl", hash = "sha256:f3639a3666bc3a94410004a91671338910ac2e1b8094b1587cc4db0f4a7df07e", size = 14003, upload-time = "2025-11-21T05:39:58.099Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" From 6a6bb8d41550bacb11510b161b5b440a8989a70c Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Tue, 16 Dec 2025 22:01:36 +0100 Subject: [PATCH 2/5] fix mypy and spelling --- python/packages/foundry_local/README.md | 4 ++-- .../agent_framework_foundry_local/_foundry_local_client.py | 2 +- python/packages/foundry_local/pyproject.toml | 2 +- .../getting_started/agents/foundry_local/foundry_basic.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/python/packages/foundry_local/README.md b/python/packages/foundry_local/README.md index 021a83b574..c65e5a0386 100644 --- a/python/packages/foundry_local/README.md +++ b/python/packages/foundry_local/README.md @@ -1,9 +1,9 @@ -# Get Started with Microsoft Agent Framework Foundry +# Get Started with Microsoft Agent Framework Foundry Local Please install this package as the extra for `agent-framework`: ```bash -pip install agent-framework[foundry] +pip install agent-framework-foundry-local --pre ``` and see the [README](https://github.com/microsoft/agent-framework/tree/main/python/README.md) for more information. diff --git a/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py b/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py index adc4cdbf77..5759d8d160 100644 --- a/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py +++ b/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py @@ -18,7 +18,7 @@ class FoundryLocalSettings(AFBaseSettings): """Foundry local model settings. - The settings are first loaded from environment variables with the prefix 'FOUNDRY_'. + 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'. If the settings are not found in the .env file, the settings are ignored; however, validation will fail alerting that the settings are missing. diff --git a/python/packages/foundry_local/pyproject.toml b/python/packages/foundry_local/pyproject.toml index 380ae0a3e6..4d208f9552 100644 --- a/python/packages/foundry_local/pyproject.toml +++ b/python/packages/foundry_local/pyproject.toml @@ -79,7 +79,7 @@ exclude_dirs = ["tests"] executor.type = "uv" include = "../../shared_tasks.toml" [tool.poe.tasks] -mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_foundry" +mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_foundry_local" test = "pytest --cov=agent_framework_foundry_local --cov-report=term-missing:skip-covered tests" [build-system] diff --git a/python/samples/getting_started/agents/foundry_local/foundry_basic.py b/python/samples/getting_started/agents/foundry_local/foundry_basic.py index 98b3da0d50..e9aaed0298 100644 --- a/python/samples/getting_started/agents/foundry_local/foundry_basic.py +++ b/python/samples/getting_started/agents/foundry_local/foundry_basic.py @@ -7,7 +7,7 @@ from agent_framework_foundry_local import FoundryLocalChatClient """ -This samples demonstrates basic usage of FoundryLocalChatClient. +This sample demonstrates basic usage of the FoundryLocalChatClient. Shows both streaming and non-streaming responses with function tools. Running this sample the first time will be slow, as the model needs to be @@ -40,7 +40,7 @@ async def non_streaming_example(client: FoundryLocalChatClient) -> None: instructions="You are a helpful agent.", tools=get_weather, ) - query = "Whats the weather like in Seattle?" + query = "What's the weather like in Seattle?" print(f"User: {query}") result = await agent.run(query) print(f"Agent: {result}\n") @@ -55,7 +55,7 @@ async def streaming_example(client: FoundryLocalChatClient) -> None: instructions="You are a helpful agent.", tools=get_weather, ) - query = "Whats the weather like in Amsterdam?" + query = "What's the weather like in Amsterdam?" print(f"User: {query}") print("Agent: ", end="", flush=True) async for chunk in agent.run_stream(query): From 9b7c4beccd1f8915e82cf7157e3e368c420419be Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Thu, 18 Dec 2025 14:18:18 +0100 Subject: [PATCH 3/5] better docstring, updated sample --- .../agent_framework_foundry_local/__init__.py | 4 +- .../_foundry_local_client.py | 117 +++++++++++++++--- python/packages/foundry_local/pyproject.toml | 2 +- .../samples/foundry_local_agent.py} | 41 +++--- .../tests/test_foundry_local_client.py | 24 ++-- python/uv.lock | 12 +- 6 files changed, 140 insertions(+), 60 deletions(-) rename python/{samples/getting_started/agents/foundry_local/foundry_basic.py => packages/foundry_local/samples/foundry_local_agent.py} (69%) diff --git a/python/packages/foundry_local/agent_framework_foundry_local/__init__.py b/python/packages/foundry_local/agent_framework_foundry_local/__init__.py index 0d59ce118a..dbea932348 100644 --- a/python/packages/foundry_local/agent_framework_foundry_local/__init__.py +++ b/python/packages/foundry_local/agent_framework_foundry_local/__init__.py @@ -2,7 +2,7 @@ import importlib.metadata -from ._foundry_local_client import FoundryLocalChatClient +from ._foundry_local_client import FoundryLocalClient try: __version__ = importlib.metadata.version(__name__) @@ -10,6 +10,6 @@ __version__ = "0.0.0" # Fallback for development mode __all__ = [ - "FoundryLocalChatClient", + "FoundryLocalClient", "__version__", ] diff --git a/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py b/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py index 5759d8d160..eb1643c4a8 100644 --- a/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py +++ b/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py @@ -8,10 +8,11 @@ from agent_framework.observability import use_instrumentation from agent_framework.openai._chat_client import OpenAIBaseChatClient from foundry_local import FoundryLocalManager +from foundry_local.models import DeviceType from openai import AsyncOpenAI __all__ = [ - "FoundryLocalChatClient", + "FoundryLocalClient", ] @@ -39,35 +40,121 @@ class FoundryLocalSettings(AFBaseSettings): @use_function_invocation @use_instrumentation @use_chat_middleware -class FoundryLocalChatClient(OpenAIBaseChatClient): +class FoundryLocalClient(OpenAIBaseChatClient): """Foundry Local Chat completion class.""" def __init__( self, - *, model_id: str | None = None, + *, bootstrap: bool = True, timeout: float | None = None, + prepare_model: bool = True, + device: DeviceType | None = None, env_file_path: str | None = None, env_file_encoding: str = "utf-8", **kwargs: Any, ) -> None: - """Initialize a FoundryLocal ChatClient.""" + """Initialize a FoundryLocalClient. + + Keyword Args: + model_id: The Foundry Local model ID or alias to use. If not provided, + it will be loaded from the FoundryLocalSettings. + bootstrap: Whether to start the Foundry Local service if not already running. + Default is True. + timeout: Optional timeout for requests to Foundry Local. + This timeout is applied to any call to the Foundry Local service. + prepare_model: Whether to download the model into the cache, and load the model into + the inferencing service upon initialization. Default is True. + If false, the first call to generate a completion will load the model, + and might take a long time. + device: The device type to use for model inference. + The device is used to select the appropriate model variant. + If not provided, the default device for your system will be used. + The values are in the foundry_local.models.DeviceType enum. + 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'. + kwargs: Additional keyword arguments, are passed to the OpenAIBaseChatClient. + This can include middleware and additional properties. + + Examples: + + .. code-block:: python + + # Create a FoundryLocalClient with a specific model ID: + from agent_framework_foundry_local import FoundryLocalClient + + client = FoundryLocalClient(model_id="phi-4-mini") + + agent = client.create_agent( + name="LocalAgent", + instructions="You are a helpful agent.", + tools=get_weather, + ) + response = await agent.run("What's the weather like in Seattle?") + + # Or you can set the model id in the environment: + os.environ["FOUNDRY_LOCAL_MODEL_ID"] = "phi-4-mini" + client = FoundryLocalClient() + + # A FoundryLocalManager is created and if set, the service is started. + # The FoundryLocalManager is available via the `manager` property. + # For instance to find out which models are available: + for model in client.manager.list_catalog_models(): + print(f"- {model.alias} for {model.task} - id={model.id}") + + # Other options include specifying the device type: + from foundry_local.models import DeviceType + + client = FoundryLocalClient( + model_id="phi-4-mini", + device=DeviceType.GPU, + ) + # and choosing if the model should be prepared on initialization: + client = FoundryLocalClient( + model_id="phi-4-mini", + prepare_model=False, + ) + # Beware, in this case the first request to generate a completion + # will take a long time as the model is loaded then. + # Alternatively, you could call the `download_model` and `load_model` methods + # on the `manager` property manually. + client.manager.download_model(alias_or_model_id="phi-4-mini", device=DeviceType.CPU) + client.manager.load_model(alias_or_model_id="phi-4-mini", device=DeviceType.CPU) + + # You can also use the CLI: + `foundry model load phi-4-mini --device Auto` + + Raises: + ServiceInitializationError: If the specified model ID or alias is not found. + Sometimes a model might be available but if you have specified a device + type that is not supported by the model, it will not be found. + + """ settings = FoundryLocalSettings( model_id=model_id, # type: ignore env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) - manager = FoundryLocalManager(alias_or_model_id=settings.model_id, bootstrap=bootstrap, timeout=timeout) - model_info = manager.get_model_info(settings.model_id) - if not model_info: - raise ServiceInitializationError( - f"Model with ID or alias '{settings.model_id}' not found in Foundry Local." + manager = FoundryLocalManager(bootstrap=bootstrap, timeout=timeout) + model_info = manager.get_model_info( + alias_or_model_id=settings.model_id, + device=device, + ) + if model_info is None: + message = ( + f"Model with ID or alias '{settings.model_id}:{device}' not found in Foundry Local." + if device + else f"Model with ID or alias '{settings.model_id}' for your current device not found in Foundry Local." ) - async_client = AsyncOpenAI(base_url=manager.endpoint, api_key=manager.api_key) - args: dict[str, Any] = { - "model_id": model_info.id, - "client": async_client, - } - super().__init__(**args) + raise ServiceInitializationError(message) + if prepare_model: + manager.download_model(alias_or_model_id=model_info.id, device=device) + manager.load_model(alias_or_model_id=model_info.id, device=device) + + super().__init__( + model_id=model_info.id, + client=AsyncOpenAI(base_url=manager.endpoint, api_key=manager.api_key), + **kwargs, + ) self.manager = manager diff --git a/python/packages/foundry_local/pyproject.toml b/python/packages/foundry_local/pyproject.toml index 4d208f9552..bca5cfc8c1 100644 --- a/python/packages/foundry_local/pyproject.toml +++ b/python/packages/foundry_local/pyproject.toml @@ -4,7 +4,7 @@ description = "Foundry Local integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b20251216" +version = "1.0.0b251218" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/samples/getting_started/agents/foundry_local/foundry_basic.py b/python/packages/foundry_local/samples/foundry_local_agent.py similarity index 69% rename from python/samples/getting_started/agents/foundry_local/foundry_basic.py rename to python/packages/foundry_local/samples/foundry_local_agent.py index e9aaed0298..657348f86a 100644 --- a/python/samples/getting_started/agents/foundry_local/foundry_basic.py +++ b/python/packages/foundry_local/samples/foundry_local_agent.py @@ -1,13 +1,17 @@ # Copyright (c) Microsoft. All rights reserved. +# ruff: noqa import asyncio from random import randint -from typing import Annotated +from typing import TYPE_CHECKING, Annotated -from agent_framework_foundry_local import FoundryLocalChatClient +from agent_framework_foundry_local import FoundryLocalClient + +if TYPE_CHECKING: + from agent_framework import ChatAgent """ -This sample demonstrates basic usage of the FoundryLocalChatClient. +This sample demonstrates basic usage of the FoundryLocalClient. Shows both streaming and non-streaming responses with function tools. Running this sample the first time will be slow, as the model needs to be @@ -26,35 +30,20 @@ def get_weather( return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." -async def non_streaming_example(client: FoundryLocalChatClient) -> None: +async def non_streaming_example(agent: "ChatAgent") -> None: """Example of non-streaming response (get the complete result at once).""" print("=== Non-streaming Response Example ===") - # Since no Agent ID is provided, the agent will be automatically created - # and deleted after getting a response - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - - agent = client.create_agent( - name="LocalAgent", - instructions="You are a helpful agent.", - tools=get_weather, - ) query = "What's the weather like in Seattle?" print(f"User: {query}") result = await agent.run(query) print(f"Agent: {result}\n") -async def streaming_example(client: FoundryLocalChatClient) -> None: +async def streaming_example(agent: "ChatAgent") -> None: """Example of streaming response (get results as they are generated).""" print("=== Streaming Response Example ===") - agent = client.create_agent( - name="LocalAgent", - instructions="You are a helpful agent.", - tools=get_weather, - ) query = "What's the weather like in Amsterdam?" print(f"User: {query}") print("Agent: ", end="", flush=True) @@ -67,16 +56,20 @@ async def streaming_example(client: FoundryLocalChatClient) -> None: async def main() -> None: print("=== Basic Foundry Chat Client Agent Example ===") - client = FoundryLocalChatClient(model_id="phi-4-mini") + client = FoundryLocalClient(model_id="phi-4-mini") print(f"Client Model ID: {client.model_id}\n") print("Other available models:") for model in client.manager.list_catalog_models(): print( f"- {model.alias} for {model.task} - id={model.id} - {(model.file_size_mb / 1000):.2f} GB - {model.license}" ) - - await non_streaming_example(client) - await streaming_example(client) + agent = client.create_agent( + name="LocalAgent", + instructions="You are a helpful agent.", + tools=get_weather, + ) + await non_streaming_example(agent) + await streaming_example(agent) if __name__ == "__main__": diff --git a/python/packages/foundry_local/tests/test_foundry_local_client.py b/python/packages/foundry_local/tests/test_foundry_local_client.py index a839fcca4c..a6cf7256ab 100644 --- a/python/packages/foundry_local/tests/test_foundry_local_client.py +++ b/python/packages/foundry_local/tests/test_foundry_local_client.py @@ -7,7 +7,7 @@ from agent_framework.exceptions import ServiceInitializationError from pydantic import ValidationError -from agent_framework_foundry_local import FoundryLocalChatClient +from agent_framework_foundry_local import FoundryLocalClient from agent_framework_foundry_local._foundry_local_client import FoundryLocalSettings # Settings Tests @@ -46,12 +46,12 @@ def test_foundry_local_settings_explicit_overrides_env(foundry_local_unit_test_e def test_foundry_local_client_init(mock_foundry_local_manager: MagicMock) -> None: - """Test FoundryLocalChatClient initialization with mocked manager.""" + """Test FoundryLocalClient initialization with mocked manager.""" with patch( "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", return_value=mock_foundry_local_manager, ): - client = FoundryLocalChatClient(model_id="test-model-id", env_file_path="test.env") + client = FoundryLocalClient(model_id="test-model-id", env_file_path="test.env") assert client.model_id == "test-model-id" assert client.manager is mock_foundry_local_manager @@ -59,12 +59,12 @@ def test_foundry_local_client_init(mock_foundry_local_manager: MagicMock) -> Non def test_foundry_local_client_init_with_bootstrap_false(mock_foundry_local_manager: MagicMock) -> None: - """Test FoundryLocalChatClient initialization with bootstrap=False.""" + """Test FoundryLocalClient initialization with bootstrap=False.""" with patch( "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", return_value=mock_foundry_local_manager, ) as mock_manager_class: - FoundryLocalChatClient(model_id="test-model-id", bootstrap=False, env_file_path="test.env") + FoundryLocalClient(model_id="test-model-id", bootstrap=False, env_file_path="test.env") mock_manager_class.assert_called_once_with( alias_or_model_id="test-model-id", @@ -74,12 +74,12 @@ def test_foundry_local_client_init_with_bootstrap_false(mock_foundry_local_manag def test_foundry_local_client_init_with_timeout(mock_foundry_local_manager: MagicMock) -> None: - """Test FoundryLocalChatClient initialization with custom timeout.""" + """Test FoundryLocalClient initialization with custom timeout.""" with patch( "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", return_value=mock_foundry_local_manager, ) as mock_manager_class: - FoundryLocalChatClient(model_id="test-model-id", timeout=60.0, env_file_path="test.env") + FoundryLocalClient(model_id="test-model-id", timeout=60.0, env_file_path="test.env") mock_manager_class.assert_called_once_with( alias_or_model_id="test-model-id", @@ -89,7 +89,7 @@ def test_foundry_local_client_init_with_timeout(mock_foundry_local_manager: Magi def test_foundry_local_client_init_model_not_found(mock_foundry_local_manager: MagicMock) -> None: - """Test FoundryLocalChatClient initialization when model is not found.""" + """Test FoundryLocalClient initialization when model is not found.""" mock_foundry_local_manager.get_model_info.return_value = None with ( @@ -99,7 +99,7 @@ def test_foundry_local_client_init_model_not_found(mock_foundry_local_manager: M ), pytest.raises(ServiceInitializationError, match="not found in Foundry Local"), ): - FoundryLocalChatClient(model_id="unknown-model", env_file_path="test.env") + FoundryLocalClient(model_id="unknown-model", env_file_path="test.env") def test_foundry_local_client_uses_model_info_id(mock_foundry_local_manager: MagicMock) -> None: @@ -112,7 +112,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 = FoundryLocalChatClient(model_id="model-alias", env_file_path="test.env") + client = FoundryLocalClient(model_id="model-alias", env_file_path="test.env") assert client.model_id == "resolved-model-id" @@ -120,11 +120,11 @@ def test_foundry_local_client_uses_model_info_id(mock_foundry_local_manager: Mag def test_foundry_local_client_init_from_env( foundry_local_unit_test_env: dict[str, str], mock_foundry_local_manager: MagicMock ) -> None: - """Test FoundryLocalChatClient initialization using environment variables.""" + """Test FoundryLocalClient initialization using environment variables.""" with patch( "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", return_value=mock_foundry_local_manager, ): - client = FoundryLocalChatClient(env_file_path="test.env") + client = FoundryLocalClient(env_file_path="test.env") assert client.model_id == foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL_ID"] diff --git a/python/uv.lock b/python/uv.lock index 2096622f66..346d760b03 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -432,7 +432,7 @@ provides-extras = ["dev", "all"] [[package]] name = "agent-framework-foundry-local" -version = "1.0.0b20251216" +version = "1.0.0b251218" source = { editable = "packages/foundry_local" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1359,7 +1359,7 @@ name = "clr-loader" version = "0.2.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/54/c2/da52aaf19424e3f0abec003d08dd1ccae52c88a3b41e31151a03bed18488/clr_loader-0.2.9.tar.gz", hash = "sha256:6af3d582c3de55ce9e9e676d2b3dbf6bc680c4ea8f76c58786739a5bdcf6b52d", size = 84829, upload-time = "2025-12-05T16:57:12.466Z" } wheels = [ @@ -1838,7 +1838,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -4504,8 +4504,8 @@ name = "powerfx" version = "0.0.33" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "pythonnet", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, + { name = "pythonnet", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/41/8f95f72f4f3b7ea54357c449bf5bd94813b6321dec31db9ffcbf578e2fa3/powerfx-0.0.33.tar.gz", hash = "sha256:85e8330bef8a7a207c3e010aa232df0ae38825e94d590c73daf3a3f44115cb09", size = 3236647, upload-time = "2025-11-20T19:31:09.414Z" } wheels = [ @@ -5174,7 +5174,7 @@ name = "pythonnet" version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "clr-loader", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "clr-loader", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" } wheels = [ From 8da11b1bc5262f2395403f7f579c9c6b3f268b58 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Thu, 18 Dec 2025 15:00:54 +0100 Subject: [PATCH 4/5] fixed tests and added tests --- .../_foundry_local_client.py | 2 +- .../tests/test_foundry_local_client.py | 72 ++++++++++++++++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py b/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py index eb1643c4a8..c2b7bd34ab 100644 --- a/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py +++ b/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py @@ -143,7 +143,7 @@ def __init__( ) if model_info is None: message = ( - f"Model with ID or alias '{settings.model_id}:{device}' not found in Foundry Local." + f"Model with ID or alias '{settings.model_id}:{device.value}' not found in Foundry Local." if device else f"Model with ID or alias '{settings.model_id}' for your current device not found in Foundry Local." ) diff --git a/python/packages/foundry_local/tests/test_foundry_local_client.py b/python/packages/foundry_local/tests/test_foundry_local_client.py index a6cf7256ab..324c94630e 100644 --- a/python/packages/foundry_local/tests/test_foundry_local_client.py +++ b/python/packages/foundry_local/tests/test_foundry_local_client.py @@ -67,7 +67,6 @@ def test_foundry_local_client_init_with_bootstrap_false(mock_foundry_local_manag FoundryLocalClient(model_id="test-model-id", bootstrap=False, env_file_path="test.env") mock_manager_class.assert_called_once_with( - alias_or_model_id="test-model-id", bootstrap=False, timeout=None, ) @@ -82,7 +81,6 @@ def test_foundry_local_client_init_with_timeout(mock_foundry_local_manager: Magi FoundryLocalClient(model_id="test-model-id", timeout=60.0, env_file_path="test.env") mock_manager_class.assert_called_once_with( - alias_or_model_id="test-model-id", bootstrap=True, timeout=60.0, ) @@ -128,3 +126,73 @@ def test_foundry_local_client_init_from_env( client = FoundryLocalClient(env_file_path="test.env") assert client.model_id == foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL_ID"] + + +def test_foundry_local_client_init_with_device(mock_foundry_local_manager: MagicMock) -> None: + """Test FoundryLocalClient initialization with device parameter.""" + from foundry_local.models import DeviceType + + with patch( + "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") + + mock_foundry_local_manager.get_model_info.assert_called_once_with( + alias_or_model_id="test-model-id", + device=DeviceType.CPU, + ) + mock_foundry_local_manager.download_model.assert_called_once_with( + alias_or_model_id="test-model-id", + device=DeviceType.CPU, + ) + mock_foundry_local_manager.load_model.assert_called_once_with( + alias_or_model_id="test-model-id", + device=DeviceType.CPU, + ) + + +def test_foundry_local_client_init_model_not_found_with_device(mock_foundry_local_manager: MagicMock) -> None: + """Test FoundryLocalClient error message includes device when model not found with device specified.""" + from foundry_local.models import DeviceType + + mock_foundry_local_manager.get_model_info.return_value = None + + with ( + patch( + "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", + return_value=mock_foundry_local_manager, + ), + pytest.raises(ServiceInitializationError, match="unknown-model:GPU.*not found"), + ): + FoundryLocalClient(model_id="unknown-model", device=DeviceType.GPU, env_file_path="test.env") + + +def test_foundry_local_client_init_with_prepare_model_false(mock_foundry_local_manager: MagicMock) -> None: + """Test FoundryLocalClient initialization with prepare_model=False skips download and load.""" + with patch( + "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") + + mock_foundry_local_manager.download_model.assert_not_called() + mock_foundry_local_manager.load_model.assert_not_called() + + +def test_foundry_local_client_init_calls_download_and_load(mock_foundry_local_manager: MagicMock) -> None: + """Test FoundryLocalClient initialization calls download_model and load_model by default.""" + with patch( + "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") + + mock_foundry_local_manager.download_model.assert_called_once_with( + alias_or_model_id="test-model-id", + device=None, + ) + mock_foundry_local_manager.load_model.assert_called_once_with( + alias_or_model_id="test-model-id", + device=None, + ) From 31fe20171d61499da5f9b3c14498667414e59a22 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 19 Dec 2025 09:41:58 +0100 Subject: [PATCH 5/5] small sample update --- .../foundry_local/samples/foundry_local_agent.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/python/packages/foundry_local/samples/foundry_local_agent.py b/python/packages/foundry_local/samples/foundry_local_agent.py index 657348f86a..e74f3de073 100644 --- a/python/packages/foundry_local/samples/foundry_local_agent.py +++ b/python/packages/foundry_local/samples/foundry_local_agent.py @@ -18,7 +18,8 @@ downloaded and initialized. Also, not every model supports function calling, so be sure to check the -model capabilities in the Foundry catalog. +model capabilities in the Foundry catalog, or pick one from the list printed +when running this sample. """ @@ -54,15 +55,16 @@ async def streaming_example(agent: "ChatAgent") -> None: async def main() -> None: - print("=== Basic Foundry Chat Client Agent Example ===") + print("=== Basic Foundry Local Client Agent Example ===") client = FoundryLocalClient(model_id="phi-4-mini") print(f"Client Model ID: {client.model_id}\n") - print("Other available models:") + print("Other available models (tool calling supported only):") for model in client.manager.list_catalog_models(): - print( - f"- {model.alias} for {model.task} - id={model.id} - {(model.file_size_mb / 1000):.2f} GB - {model.license}" - ) + if model.supports_tool_calling: + print( + f"- {model.alias} for {model.task} - id={model.id} - {(model.file_size_mb / 1000):.2f} GB - {model.license}" + ) agent = client.create_agent( name="LocalAgent", instructions="You are a helpful agent.",