chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Foundry Local Package (agent-framework-foundry-local)
|
||||
|
||||
Integration with Azure AI Foundry Local for local model inference.
|
||||
|
||||
## Main Classes
|
||||
|
||||
- **`FoundryLocalClient`** - Chat client for Foundry Local models
|
||||
- **`FoundryLocalChatOptions`** - Options TypedDict for Foundry Local parameters
|
||||
- **`FoundryLocalSettings`** - Pydantic settings for configuration
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from agent_framework.foundry import FoundryLocalClient
|
||||
|
||||
client = FoundryLocalClient(model="your-local-model")
|
||||
response = await client.get_response("Hello")
|
||||
```
|
||||
|
||||
## Import Path
|
||||
|
||||
```python
|
||||
from agent_framework.foundry import FoundryLocalClient
|
||||
```
|
||||
@@ -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,13 @@
|
||||
# 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.
|
||||
|
||||
## Foundry Local Sample
|
||||
|
||||
See the [Foundry Local provider sample](../../samples/02-agents/providers/foundry/foundry_local_agent.py) for a runnable example.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
from ._foundry_local_client import FoundryLocalChatOptions, FoundryLocalClient, FoundryLocalSettings
|
||||
|
||||
try:
|
||||
__version__ = importlib.metadata.version(__name__)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
__version__ = "0.0.0" # Fallback for development mode
|
||||
|
||||
__all__ = [
|
||||
"FoundryLocalChatOptions",
|
||||
"FoundryLocalClient",
|
||||
"FoundryLocalSettings",
|
||||
"__version__",
|
||||
]
|
||||
@@ -0,0 +1,348 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
||||
from typing import Any, Generic, Literal, cast, overload
|
||||
|
||||
from agent_framework import (
|
||||
ChatAndFunctionMiddlewareTypes,
|
||||
ChatMiddlewareLayer,
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
CompactionStrategy,
|
||||
FunctionInvocationConfiguration,
|
||||
FunctionInvocationLayer,
|
||||
Message,
|
||||
ResponseStream,
|
||||
TokenizerProtocol,
|
||||
)
|
||||
from agent_framework._settings import load_settings
|
||||
from agent_framework.observability import ChatTelemetryLayer
|
||||
from agent_framework_openai._chat_completion_client import RawOpenAIChatCompletionClient
|
||||
from foundry_local import FoundryLocalManager
|
||||
from foundry_local.models import DeviceType, FoundryModelInfo
|
||||
from openai import AsyncOpenAI
|
||||
from pydantic import BaseModel
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from typing import TypeVar # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import TypeVar # pragma: no cover
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import TypedDict # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import TypedDict # pragma: no cover
|
||||
|
||||
|
||||
__all__ = [
|
||||
"FoundryLocalChatOptions",
|
||||
"FoundryLocalClient",
|
||||
"FoundryLocalSettings",
|
||||
]
|
||||
|
||||
ResponseModelT = TypeVar("ResponseModelT", bound=BaseModel | None, default=None)
|
||||
|
||||
|
||||
# region Foundry Local Chat Options TypedDict
|
||||
|
||||
|
||||
class FoundryLocalChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], total=False):
|
||||
"""Azure Foundry Local (local model deployment) chat options dict.
|
||||
|
||||
Extends base ChatOptions for local model inference via Foundry Local.
|
||||
Foundry Local provides an OpenAI-compatible API, so most standard
|
||||
OpenAI chat completion options are supported.
|
||||
|
||||
See: https://github.com/Azure/azure-ai-foundry-model-inference
|
||||
|
||||
Keys:
|
||||
# Inherited from ChatOptions (supported via OpenAI-compatible API):
|
||||
model: The model identifier or alias (e.g., 'phi-4-mini').
|
||||
temperature: Sampling temperature (0-2).
|
||||
top_p: Nucleus sampling parameter.
|
||||
max_tokens: Maximum tokens to generate.
|
||||
stop: Stop sequences.
|
||||
tools: List of tools available to the model.
|
||||
tool_choice: How the model should use tools.
|
||||
frequency_penalty: Frequency penalty (-2.0 to 2.0).
|
||||
presence_penalty: Presence penalty (-2.0 to 2.0).
|
||||
seed: Random seed for reproducibility.
|
||||
|
||||
# Options with limited support (depends on the model):
|
||||
response_format: Response format specification.
|
||||
Not all local models support JSON mode.
|
||||
logit_bias: Token bias dictionary.
|
||||
May not be supported by all models.
|
||||
|
||||
# Options not supported in Foundry Local:
|
||||
user: Not used locally.
|
||||
store: Not applicable for local inference.
|
||||
metadata: Not applicable for local inference.
|
||||
|
||||
# Foundry Local-specific options:
|
||||
extra_body: Additional request body parameters to pass to the model.
|
||||
Can be used for model-specific options not covered by standard API.
|
||||
|
||||
Note:
|
||||
The actual options supported depend on the specific model being used.
|
||||
Some models (like Phi-4) may not support all OpenAI API features.
|
||||
Options not supported by the model will typically be ignored.
|
||||
"""
|
||||
|
||||
# Foundry Local-specific options
|
||||
extra_body: dict[str, Any]
|
||||
"""Additional request body parameters for model-specific options."""
|
||||
|
||||
# ChatOptions fields not applicable for local inference
|
||||
user: None # type: ignore[misc]
|
||||
"""Not used for local model inference."""
|
||||
|
||||
store: None # type: ignore[misc]
|
||||
"""Not applicable for local inference."""
|
||||
|
||||
|
||||
FoundryLocalChatOptionsT = TypeVar(
|
||||
"FoundryLocalChatOptionsT",
|
||||
bound=TypedDict, # type: ignore[valid-type]
|
||||
default="FoundryLocalChatOptions",
|
||||
covariant=True,
|
||||
)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
class FoundryLocalSettings(TypedDict, total=False):
|
||||
"""Foundry local model settings.
|
||||
|
||||
Settings are resolved in this order: explicit keyword arguments, values from an
|
||||
explicitly provided .env file, then environment variables with the prefix
|
||||
'FOUNDRY_LOCAL_'.
|
||||
|
||||
Keys:
|
||||
model: The name of the model deployment to use.
|
||||
(Env var FOUNDRY_LOCAL_MODEL)
|
||||
"""
|
||||
|
||||
model: str | None
|
||||
|
||||
|
||||
class FoundryLocalClient(
|
||||
FunctionInvocationLayer[FoundryLocalChatOptionsT],
|
||||
ChatMiddlewareLayer[FoundryLocalChatOptionsT],
|
||||
ChatTelemetryLayer[FoundryLocalChatOptionsT],
|
||||
RawOpenAIChatCompletionClient[FoundryLocalChatOptionsT],
|
||||
Generic[FoundryLocalChatOptionsT],
|
||||
):
|
||||
"""Foundry Local Chat completion class with middleware, telemetry, and function invocation support."""
|
||||
|
||||
@overload
|
||||
def get_response(
|
||||
self,
|
||||
messages: Sequence[Message],
|
||||
*,
|
||||
stream: Literal[False] = ...,
|
||||
options: ChatOptions[ResponseModelT],
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
|
||||
) -> Awaitable[ChatResponse[ResponseModelT]]: ...
|
||||
|
||||
@overload
|
||||
def get_response(
|
||||
self,
|
||||
messages: Sequence[Message],
|
||||
*,
|
||||
stream: Literal[False] = ...,
|
||||
options: FoundryLocalChatOptionsT | ChatOptions[None] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
|
||||
) -> Awaitable[ChatResponse[Any]]: ...
|
||||
|
||||
@overload
|
||||
def get_response(
|
||||
self,
|
||||
messages: Sequence[Message],
|
||||
*,
|
||||
stream: Literal[True],
|
||||
options: FoundryLocalChatOptionsT | ChatOptions[Any] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
|
||||
) -> ResponseStream[ChatResponseUpdate, ChatResponse[Any]]: ...
|
||||
|
||||
def get_response(
|
||||
self,
|
||||
messages: Sequence[Message],
|
||||
*,
|
||||
stream: bool = False,
|
||||
options: FoundryLocalChatOptionsT | ChatOptions[Any] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
|
||||
) -> Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]]:
|
||||
"""Get a response from the Foundry Local chat client with all standard layers enabled."""
|
||||
super_get_response = cast(
|
||||
"Callable[..., Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]]]",
|
||||
super().get_response,
|
||||
)
|
||||
effective_client_kwargs = dict(client_kwargs) if client_kwargs is not None else {}
|
||||
if middleware is not None:
|
||||
effective_client_kwargs["middleware"] = middleware
|
||||
return super_get_response(
|
||||
messages=messages,
|
||||
stream=stream,
|
||||
options=options,
|
||||
compaction_strategy=compaction_strategy,
|
||||
tokenizer=tokenizer,
|
||||
function_invocation_kwargs=function_invocation_kwargs,
|
||||
client_kwargs=effective_client_kwargs,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str | None = None,
|
||||
*,
|
||||
bootstrap: bool = True,
|
||||
timeout: float | None = None,
|
||||
prepare_model: bool = True,
|
||||
device: DeviceType | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
|
||||
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str = "utf-8",
|
||||
) -> None:
|
||||
"""Initialize a FoundryLocalClient.
|
||||
|
||||
Keyword Args:
|
||||
model: 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.
|
||||
additional_properties: Additional properties stored on the client instance.
|
||||
middleware: Optional sequence of ChatAndFunctionMiddlewareTypes to apply to requests.
|
||||
function_invocation_configuration: Optional configuration for function invocation support.
|
||||
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'.
|
||||
|
||||
Examples:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Create a FoundryLocalClient with a specific model ID:
|
||||
from agent_framework.foundry import FoundryLocalClient
|
||||
|
||||
client = FoundryLocalClient(model="phi-4-mini")
|
||||
|
||||
agent = client.as_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"] = "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="phi-4-mini",
|
||||
device=DeviceType.GPU,
|
||||
)
|
||||
# and choosing if the model should be prepared on initialization:
|
||||
client = FoundryLocalClient(
|
||||
model="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("phi-4-mini", device=DeviceType.CPU)
|
||||
client.manager.load_model("phi-4-mini", device=DeviceType.CPU)
|
||||
|
||||
# You can also use the CLI:
|
||||
`foundry model load phi-4-mini --device Auto`
|
||||
|
||||
# Using custom ChatOptions with type safety:
|
||||
from typing import TypedDict
|
||||
from agent_framework.foundry import FoundryLocalChatOptions
|
||||
|
||||
class MyOptions(FoundryLocalChatOptions, total=False):
|
||||
my_custom_option: str
|
||||
|
||||
client: FoundryLocalClient[MyOptions] = FoundryLocalClient(model="phi-4-mini")
|
||||
response = await client.get_response("Hello", options={"my_custom_option": "value"})
|
||||
|
||||
Raises:
|
||||
ValueError: 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 = load_settings(
|
||||
FoundryLocalSettings,
|
||||
env_prefix="FOUNDRY_LOCAL_",
|
||||
required_fields=["model"],
|
||||
model=model,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
model_setting: str = settings["model"] # type: ignore[assignment]
|
||||
|
||||
manager = FoundryLocalManager(bootstrap=bootstrap, timeout=timeout)
|
||||
model_info: FoundryModelInfo | None = manager.get_model_info(
|
||||
model_setting,
|
||||
device=device,
|
||||
)
|
||||
if model_info is None:
|
||||
message = (
|
||||
f"Model with ID or alias '{model_setting}:{device.value}' not found in Foundry Local."
|
||||
if device
|
||||
else (f"Model with ID or alias '{model_setting}' for your current device not found in Foundry Local.")
|
||||
)
|
||||
raise ValueError(message)
|
||||
if prepare_model:
|
||||
manager.download_model(model_info.id, device=device)
|
||||
manager.load_model(model_info.id, device=device)
|
||||
|
||||
super().__init__(
|
||||
model=model_info.id,
|
||||
async_client=AsyncOpenAI(base_url=manager.endpoint, api_key=manager.api_key),
|
||||
additional_properties=additional_properties,
|
||||
middleware=middleware,
|
||||
function_invocation_configuration=function_invocation_configuration,
|
||||
)
|
||||
self.manager = manager
|
||||
@@ -0,0 +1,98 @@
|
||||
[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.0b260521"
|
||||
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>=1.6.0,<2",
|
||||
"agent-framework-openai>=1.6.0,<2",
|
||||
"foundry-local-sdk>=0.5.1,<0.5.2",
|
||||
]
|
||||
|
||||
[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
|
||||
markers = [
|
||||
"integration: marks tests as integration tests that require external services",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
|
||||
[tool.coverage.run]
|
||||
omit = [
|
||||
"**/__init__.py"
|
||||
]
|
||||
|
||||
[tool.pyright]
|
||||
extends = "../../pyproject.toml"
|
||||
include = ["agent_framework_foundry_local"]
|
||||
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]
|
||||
help = "Run MyPy for this package."
|
||||
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_foundry_local"
|
||||
|
||||
[tool.poe.tasks.test]
|
||||
help = "Run the default unit test suite for this package."
|
||||
cmd = 'pytest -m "not integration" --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,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": "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,227 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import inspect
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from agent_framework import Agent, SupportsChatGetResponse
|
||||
from agent_framework._settings import load_settings
|
||||
from agent_framework.exceptions import SettingNotFoundError
|
||||
from agent_framework.foundry 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 = load_settings(FoundryLocalSettings, env_prefix="FOUNDRY_LOCAL_")
|
||||
|
||||
assert settings["model"] == foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL"]
|
||||
|
||||
|
||||
def test_foundry_local_settings_init_with_explicit_values() -> None:
|
||||
"""Test FoundryLocalSettings initialization with explicit values."""
|
||||
settings = load_settings(
|
||||
FoundryLocalSettings,
|
||||
env_prefix="FOUNDRY_LOCAL_",
|
||||
model="custom-model-id",
|
||||
)
|
||||
|
||||
assert settings["model"] == "custom-model-id"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exclude_list", [["FOUNDRY_LOCAL_MODEL"]], indirect=True)
|
||||
def test_foundry_local_settings_missing_model(foundry_local_unit_test_env: dict[str, str]) -> None:
|
||||
"""Test FoundryLocalSettings when model is missing raises error."""
|
||||
with pytest.raises(SettingNotFoundError, match="Required setting 'model'"):
|
||||
load_settings(
|
||||
FoundryLocalSettings,
|
||||
env_prefix="FOUNDRY_LOCAL_",
|
||||
required_fields=["model"],
|
||||
)
|
||||
|
||||
|
||||
def test_foundry_local_settings_explicit_overrides_env(foundry_local_unit_test_env: dict[str, str]) -> None:
|
||||
"""Test that explicit values override environment variables."""
|
||||
settings = load_settings(FoundryLocalSettings, env_prefix="FOUNDRY_LOCAL_", model="override-model-id")
|
||||
|
||||
assert settings["model"] == "override-model-id"
|
||||
assert settings["model"] != foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL"]
|
||||
|
||||
|
||||
# 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="test-model-id")
|
||||
|
||||
assert client.model == "test-model-id"
|
||||
assert client.manager is mock_foundry_local_manager
|
||||
assert isinstance(client, SupportsChatGetResponse)
|
||||
|
||||
|
||||
def test_agent_accepts_foundry_local_client(mock_foundry_local_manager: MagicMock) -> None:
|
||||
with patch(
|
||||
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
|
||||
return_value=mock_foundry_local_manager,
|
||||
):
|
||||
client = FoundryLocalClient(model="test-model-id")
|
||||
|
||||
agent = Agent(client=client, instructions="test agent")
|
||||
assert agent.client is client
|
||||
|
||||
|
||||
def test_foundry_local_client_get_response_uses_explicit_runtime_buckets() -> None:
|
||||
"""Foundry Local should expose explicit runtime buckets instead of raw kwargs."""
|
||||
signature = inspect.signature(FoundryLocalClient.get_response)
|
||||
|
||||
assert "client_kwargs" in signature.parameters
|
||||
assert "function_invocation_kwargs" in signature.parameters
|
||||
assert all(parameter.kind != inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values())
|
||||
|
||||
|
||||
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="test-model-id", bootstrap=False)
|
||||
|
||||
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="test-model-id", timeout=60.0)
|
||||
|
||||
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(ValueError, match="not found in Foundry Local"),
|
||||
):
|
||||
FoundryLocalClient(model="unknown-model")
|
||||
|
||||
|
||||
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="model-alias")
|
||||
|
||||
assert client.model == "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()
|
||||
|
||||
assert client.model == foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL"]
|
||||
|
||||
|
||||
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="test-model-id", device=DeviceType.CPU)
|
||||
|
||||
mock_foundry_local_manager.get_model_info.assert_called_once_with(
|
||||
"test-model-id",
|
||||
device=DeviceType.CPU,
|
||||
)
|
||||
mock_foundry_local_manager.download_model.assert_called_once_with(
|
||||
"test-model-id",
|
||||
device=DeviceType.CPU,
|
||||
)
|
||||
mock_foundry_local_manager.load_model.assert_called_once_with(
|
||||
"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(ValueError, match="unknown-model:GPU.*not found"),
|
||||
):
|
||||
FoundryLocalClient(model="unknown-model", device=DeviceType.GPU)
|
||||
|
||||
|
||||
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="test-model-id", prepare_model=False)
|
||||
|
||||
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="test-model-id")
|
||||
|
||||
mock_foundry_local_manager.download_model.assert_called_once_with(
|
||||
"test-model-id",
|
||||
device=None,
|
||||
)
|
||||
mock_foundry_local_manager.load_model.assert_called_once_with(
|
||||
"test-model-id",
|
||||
device=None,
|
||||
)
|
||||
Reference in New Issue
Block a user