mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Introducing Foundry Local Chat Clients (#2915)
* redo foundry local chat client * fix mypy and spelling * better docstring, updated sample * fixed tests and added tests * small sample update
This commit is contained in:
committed by
GitHub
Unverified
parent
e15eab7da6
commit
8783ac58f1
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
# Get Started with Microsoft Agent Framework Foundry Local
|
||||
|
||||
Please install this package as the extra for `agent-framework`:
|
||||
|
||||
```bash
|
||||
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.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
from ._foundry_local_client import FoundryLocalClient
|
||||
|
||||
try:
|
||||
__version__ = importlib.metadata.version(__name__)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
__version__ = "0.0.0" # Fallback for development mode
|
||||
|
||||
__all__ = [
|
||||
"FoundryLocalClient",
|
||||
"__version__",
|
||||
]
|
||||
@@ -0,0 +1,160 @@
|
||||
# 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 foundry_local.models import DeviceType
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
__all__ = [
|
||||
"FoundryLocalClient",
|
||||
]
|
||||
|
||||
|
||||
class FoundryLocalSettings(AFBaseSettings):
|
||||
"""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'. 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 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 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(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.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."
|
||||
)
|
||||
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
|
||||
@@ -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.0b251218"
|
||||
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_local"
|
||||
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"
|
||||
@@ -0,0 +1,78 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
# ruff: noqa
|
||||
|
||||
import asyncio
|
||||
from random import randint
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
from agent_framework_foundry_local import FoundryLocalClient
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agent_framework import ChatAgent
|
||||
|
||||
"""
|
||||
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
|
||||
downloaded and initialized.
|
||||
|
||||
Also, not every model supports function calling, so be sure to check the
|
||||
model capabilities in the Foundry catalog, or pick one from the list printed
|
||||
when running this sample.
|
||||
"""
|
||||
|
||||
|
||||
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(agent: "ChatAgent") -> None:
|
||||
"""Example of non-streaming response (get the complete result at once)."""
|
||||
print("=== Non-streaming Response Example ===")
|
||||
|
||||
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(agent: "ChatAgent") -> None:
|
||||
"""Example of streaming response (get results as they are generated)."""
|
||||
print("=== Streaming Response Example ===")
|
||||
|
||||
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):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="", flush=True)
|
||||
print("\n")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
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 (tool calling supported only):")
|
||||
for model in client.manager.list_catalog_models():
|
||||
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.",
|
||||
tools=get_weather,
|
||||
)
|
||||
await non_streaming_example(agent)
|
||||
await streaming_example(agent)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -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
|
||||
@@ -0,0 +1,198 @@
|
||||
# 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 FoundryLocalClient
|
||||
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 FoundryLocalClient initialization with mocked manager."""
|
||||
with patch(
|
||||
"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")
|
||||
|
||||
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 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:
|
||||
FoundryLocalClient(model_id="test-model-id", bootstrap=False, env_file_path="test.env")
|
||||
|
||||
mock_manager_class.assert_called_once_with(
|
||||
bootstrap=False,
|
||||
timeout=None,
|
||||
)
|
||||
|
||||
|
||||
def test_foundry_local_client_init_with_timeout(mock_foundry_local_manager: MagicMock) -> None:
|
||||
"""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:
|
||||
FoundryLocalClient(model_id="test-model-id", timeout=60.0, env_file_path="test.env")
|
||||
|
||||
mock_manager_class.assert_called_once_with(
|
||||
bootstrap=True,
|
||||
timeout=60.0,
|
||||
)
|
||||
|
||||
|
||||
def test_foundry_local_client_init_model_not_found(mock_foundry_local_manager: MagicMock) -> None:
|
||||
"""Test FoundryLocalClient 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"),
|
||||
):
|
||||
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:
|
||||
"""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 = FoundryLocalClient(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 FoundryLocalClient initialization using environment variables."""
|
||||
with patch(
|
||||
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
|
||||
return_value=mock_foundry_local_manager,
|
||||
):
|
||||
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,
|
||||
)
|
||||
@@ -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 }
|
||||
|
||||
Generated
+34
-5
@@ -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.0b251218"
|
||||
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"
|
||||
@@ -1343,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 = [
|
||||
@@ -1822,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 = [
|
||||
@@ -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"
|
||||
@@ -4475,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 = [
|
||||
@@ -5145,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 = [
|
||||
|
||||
Reference in New Issue
Block a user