agent-framework-foundry 1.16.0__tar.gz → 1.16.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/PKG-INFO +14 -6
- {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/README.md +11 -3
- {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_agent.py +1 -5
- {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_chat_client.py +43 -9
- {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_embedding_client.py +206 -91
- {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_foundry_evals.py +7 -3
- {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_memory_provider.py +1 -7
- {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_tools.py +18 -2
- {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/pyproject.toml +4 -4
- {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/LICENSE +0 -0
- {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/__init__.py +0 -0
- {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_constants.py +0 -0
- {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_feature_usage.py +0 -0
- {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_oauth_helpers.py +0 -0
- {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_to_prompt_agent.py +0 -0
- {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/py.typed +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agent-framework-foundry
|
|
3
|
-
Version: 1.16.
|
|
3
|
+
Version: 1.16.2
|
|
4
4
|
Summary: Microsoft Foundry integrations for Microsoft Agent Framework.
|
|
5
5
|
Author-email: Microsoft <af-support@microsoft.com>
|
|
6
|
-
Requires-Python: >=3.
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Classifier: License :: OSI Approved :: MIT License
|
|
9
9
|
Classifier: Development Status :: 5 - Production/Stable
|
|
@@ -20,7 +20,7 @@ Requires-Dist: agent-framework-core>=1.24.0,<2
|
|
|
20
20
|
Requires-Dist: agent-framework-openai>=1.18.0,<2
|
|
21
21
|
Requires-Dist: aiohttp>=3.9,<4
|
|
22
22
|
Requires-Dist: azure-ai-inference>=1.0.0b9,<1.0.0b10
|
|
23
|
-
Requires-Dist: azure-ai-projects>=2.2.0,<
|
|
23
|
+
Requires-Dist: azure-ai-projects>=2.2.0,<3
|
|
24
24
|
Requires-Dist: httpx>=0.28,<1
|
|
25
25
|
Project-URL: homepage, https://aka.ms/agent-framework
|
|
26
26
|
Project-URL: issues, https://github.com/microsoft/agent-framework/issues
|
|
@@ -101,9 +101,17 @@ The client uses the project for authentication and converts a
|
|
|
101
101
|
`https://<resource>.services.ai.azure.com/api/projects/<project>` endpoint to the documented resource-scoped
|
|
102
102
|
`https://<resource>.openai.azure.com/openai/v1/` model route. The existing `FOUNDRY_MODELS_ENDPOINT` and
|
|
103
103
|
`FOUNDRY_MODELS_API_KEY` configuration remains available for Foundry Models inference endpoints. A Models endpoint is
|
|
104
|
-
required for image embedding models. If both project and Models
|
|
105
|
-
variables, the Models endpoint is retained for backward
|
|
106
|
-
the project OpenAI deployment.
|
|
104
|
+
required for image embedding models unless an `ImageEmbeddingsClient` is supplied. If both project and Models
|
|
105
|
+
endpoints are configured only through environment variables, the Models endpoint is retained for backward
|
|
106
|
+
compatibility; pass `project_endpoint` explicitly to select the project OpenAI deployment.
|
|
107
|
+
|
|
108
|
+
A preconfigured `EmbeddingsClient` or `ImageEmbeddingsClient` can be passed as `text_client` or `image_client`.
|
|
109
|
+
The injected client is the transport for that modality and takes precedence over constructor connection settings,
|
|
110
|
+
environment settings, and framework defaults, so no endpoint, API key, credential, or Foundry environment is
|
|
111
|
+
required for it. `model` and `image_model` keep their usual explicit-argument, environment, then default
|
|
112
|
+
resolution. The other modality is created only when a request needs it and enough settings are available.
|
|
113
|
+
For example, an injected `image_client` can be combined with project configuration for text embeddings;
|
|
114
|
+
the project text transport is initialized only when a request includes text.
|
|
107
115
|
|
|
108
116
|
## Evaluations
|
|
109
117
|
|
|
@@ -71,9 +71,17 @@ The client uses the project for authentication and converts a
|
|
|
71
71
|
`https://<resource>.services.ai.azure.com/api/projects/<project>` endpoint to the documented resource-scoped
|
|
72
72
|
`https://<resource>.openai.azure.com/openai/v1/` model route. The existing `FOUNDRY_MODELS_ENDPOINT` and
|
|
73
73
|
`FOUNDRY_MODELS_API_KEY` configuration remains available for Foundry Models inference endpoints. A Models endpoint is
|
|
74
|
-
required for image embedding models. If both project and Models
|
|
75
|
-
variables, the Models endpoint is retained for backward
|
|
76
|
-
the project OpenAI deployment.
|
|
74
|
+
required for image embedding models unless an `ImageEmbeddingsClient` is supplied. If both project and Models
|
|
75
|
+
endpoints are configured only through environment variables, the Models endpoint is retained for backward
|
|
76
|
+
compatibility; pass `project_endpoint` explicitly to select the project OpenAI deployment.
|
|
77
|
+
|
|
78
|
+
A preconfigured `EmbeddingsClient` or `ImageEmbeddingsClient` can be passed as `text_client` or `image_client`.
|
|
79
|
+
The injected client is the transport for that modality and takes precedence over constructor connection settings,
|
|
80
|
+
environment settings, and framework defaults, so no endpoint, API key, credential, or Foundry environment is
|
|
81
|
+
required for it. `model` and `image_model` keep their usual explicit-argument, environment, then default
|
|
82
|
+
resolution. The other modality is created only when a request needs it and enough settings are available.
|
|
83
|
+
For example, an injected `image_client` can be combined with project configuration for text embeddings;
|
|
84
|
+
the project text transport is initialized only when a request includes text.
|
|
77
85
|
|
|
78
86
|
## Evaluations
|
|
79
87
|
|
{agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_agent.py
RENAMED
|
@@ -13,7 +13,7 @@ import logging
|
|
|
13
13
|
import sys
|
|
14
14
|
import warnings
|
|
15
15
|
from collections.abc import Awaitable, Callable, Mapping, MutableMapping, Sequence
|
|
16
|
-
from typing import TYPE_CHECKING, Any, ClassVar, Generic, cast
|
|
16
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypedDict, cast
|
|
17
17
|
|
|
18
18
|
from agent_framework import (
|
|
19
19
|
AgentMiddlewareLayer,
|
|
@@ -57,10 +57,6 @@ if sys.version_info >= (3, 12):
|
|
|
57
57
|
from typing import override # pragma: no cover
|
|
58
58
|
else:
|
|
59
59
|
from typing_extensions import override # pragma: no cover
|
|
60
|
-
if sys.version_info >= (3, 11):
|
|
61
|
-
from typing import TypedDict # pragma: no cover
|
|
62
|
-
else:
|
|
63
|
-
from typing_extensions import TypedDict # pragma: no cover
|
|
64
60
|
|
|
65
61
|
if TYPE_CHECKING:
|
|
66
62
|
from agent_framework import (
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import logging
|
|
6
6
|
import sys
|
|
7
7
|
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
8
|
-
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, cast
|
|
8
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, TypedDict, cast
|
|
9
9
|
|
|
10
10
|
from agent_framework import (
|
|
11
11
|
ChatMiddlewareLayer,
|
|
@@ -75,10 +75,6 @@ if sys.version_info >= (3, 12):
|
|
|
75
75
|
from typing import override # pragma: no cover
|
|
76
76
|
else:
|
|
77
77
|
from typing_extensions import override # pragma: no cover
|
|
78
|
-
if sys.version_info >= (3, 11):
|
|
79
|
-
from typing import TypedDict # pragma: no cover
|
|
80
|
-
else:
|
|
81
|
-
from typing_extensions import TypedDict # pragma: no cover
|
|
82
78
|
|
|
83
79
|
if TYPE_CHECKING:
|
|
84
80
|
from agent_framework import ChatAndFunctionMiddlewareTypes, ToolTypes
|
|
@@ -380,15 +376,53 @@ class RawFoundryChatClient(
|
|
|
380
376
|
) -> ChatResponse:
|
|
381
377
|
chat_response = super()._parse_response_from_openai(response, options)
|
|
382
378
|
if chat_response.messages:
|
|
379
|
+
messages = chat_response.messages
|
|
383
380
|
for item in getattr(response, "output", ()):
|
|
384
381
|
if getattr(item, "type", None) != "reasoning":
|
|
385
382
|
continue
|
|
386
|
-
self.
|
|
387
|
-
|
|
388
|
-
parse_oauth_consent_output_items(getattr(response, "output", None))
|
|
389
|
-
)
|
|
383
|
+
self._attach_foundry_reasoning_replay_item_to_messages(messages, item)
|
|
384
|
+
messages[0].contents.extend(parse_oauth_consent_output_items(getattr(response, "output", None)))
|
|
390
385
|
return chat_response
|
|
391
386
|
|
|
387
|
+
@classmethod
|
|
388
|
+
def _attach_foundry_reasoning_replay_item_to_messages(cls, messages: list[Message], item: Any) -> None:
|
|
389
|
+
"""Attach replay data to the message that owns the reasoning content.
|
|
390
|
+
|
|
391
|
+
Multi-message responses keep one message per provider output message, and a
|
|
392
|
+
trailing reasoning item is parsed into the last message, so the owning
|
|
393
|
+
content can live in any message. Search every message (most recent first)
|
|
394
|
+
and only append to the last message when no parsed content matches, instead
|
|
395
|
+
of always targeting the first message, which duplicated the reasoning
|
|
396
|
+
content there while the real one kept no replay data.
|
|
397
|
+
"""
|
|
398
|
+
payload = cls._prepare_foundry_reasoning_replay_item(item)
|
|
399
|
+
if payload is None:
|
|
400
|
+
return
|
|
401
|
+
reasoning_id = cast(str, payload["id"])
|
|
402
|
+
for message in reversed(messages):
|
|
403
|
+
for content in reversed(message.contents):
|
|
404
|
+
if content.type == "text_reasoning" and content.id == reasoning_id:
|
|
405
|
+
updated = with_namespaced_value(
|
|
406
|
+
content.additional_properties,
|
|
407
|
+
"foundry",
|
|
408
|
+
"reasoning_item",
|
|
409
|
+
payload,
|
|
410
|
+
)
|
|
411
|
+
updated[_FOUNDRY_REASONING_REPLAY_ITEM_KEY] = payload
|
|
412
|
+
content.additional_properties = updated
|
|
413
|
+
return
|
|
414
|
+
messages[-1].contents.append(
|
|
415
|
+
Content.from_text_reasoning(
|
|
416
|
+
id=reasoning_id,
|
|
417
|
+
text="",
|
|
418
|
+
raw_representation=item,
|
|
419
|
+
additional_properties={
|
|
420
|
+
"foundry": {"reasoning_item": payload},
|
|
421
|
+
_FOUNDRY_REASONING_REPLAY_ITEM_KEY: payload,
|
|
422
|
+
},
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
|
|
392
426
|
@override
|
|
393
427
|
def _check_model_presence(self, options: dict[str, Any]) -> None:
|
|
394
428
|
if not options.get("model"):
|
|
@@ -8,7 +8,7 @@ import struct
|
|
|
8
8
|
import sys
|
|
9
9
|
from collections.abc import Mapping, Sequence
|
|
10
10
|
from contextlib import suppress
|
|
11
|
-
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypedDict
|
|
11
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypedDict
|
|
12
12
|
from urllib.parse import urlsplit, urlunsplit
|
|
13
13
|
|
|
14
14
|
from agent_framework import (
|
|
@@ -59,6 +59,50 @@ def _get_openai_model_base_url(endpoint: str) -> str:
|
|
|
59
59
|
return urlunsplit((parts.scheme, openai_netloc, "/openai/v1/", "", ""))
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
def _injected_client_endpoint(client: Any) -> str | None:
|
|
63
|
+
"""Return the endpoint configured on an injected Azure AI Inference client.
|
|
64
|
+
|
|
65
|
+
``EmbeddingsClient`` and ``ImageEmbeddingsClient`` store the endpoint passed to
|
|
66
|
+
their constructor on ``_config.endpoint``. The pipeline base URL is an unresolved
|
|
67
|
+
``{endpoint}`` template, so it is not a service URL. Non-string values, including
|
|
68
|
+
those produced by test doubles, are ignored.
|
|
69
|
+
"""
|
|
70
|
+
config = getattr(client, "_config", None)
|
|
71
|
+
endpoint = getattr(config, "endpoint", None) if config is not None else None
|
|
72
|
+
if not isinstance(endpoint, str):
|
|
73
|
+
return None
|
|
74
|
+
endpoint = endpoint.strip()
|
|
75
|
+
return endpoint or None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _service_url_for_inference_clients(
|
|
79
|
+
*,
|
|
80
|
+
text_client: Any,
|
|
81
|
+
image_client: Any,
|
|
82
|
+
explicit_endpoint: str | None,
|
|
83
|
+
resolved_endpoint: str | None,
|
|
84
|
+
) -> str:
|
|
85
|
+
"""Choose a service URL without letting ambient settings replace an injected transport."""
|
|
86
|
+
if text_client is not None or image_client is not None:
|
|
87
|
+
for client in (text_client, image_client):
|
|
88
|
+
if client is None:
|
|
89
|
+
continue
|
|
90
|
+
if endpoint := _injected_client_endpoint(client):
|
|
91
|
+
return endpoint
|
|
92
|
+
return explicit_endpoint or ""
|
|
93
|
+
return resolved_endpoint or ""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
_MISSING_TEXT_EMBEDDING_TRANSPORT = (
|
|
97
|
+
"Text embeddings require an injected 'text_client' or enough Foundry Models settings to create one. "
|
|
98
|
+
"Set 'endpoint' or 'FOUNDRY_MODELS_ENDPOINT', and 'api_key', 'credential', or 'FOUNDRY_MODELS_API_KEY'."
|
|
99
|
+
)
|
|
100
|
+
_MISSING_IMAGE_EMBEDDING_TRANSPORT = (
|
|
101
|
+
"Image embeddings require an injected 'image_client' or enough Foundry Models settings to create one. "
|
|
102
|
+
"Set 'endpoint' or 'FOUNDRY_MODELS_ENDPOINT', and 'api_key', 'credential', or 'FOUNDRY_MODELS_API_KEY'."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
62
106
|
class FoundryEmbeddingOptions(EmbeddingGenerationOptions, total=False):
|
|
63
107
|
"""Foundry-specific embedding options.
|
|
64
108
|
|
|
@@ -133,17 +177,21 @@ class RawFoundryEmbeddingClient(
|
|
|
133
177
|
embedding deployments. Can also be set via environment variable
|
|
134
178
|
FOUNDRY_PROJECT_ENDPOINT.
|
|
135
179
|
project_client: An existing ``AIProjectClient``. If provided, its
|
|
136
|
-
OpenAI-compatible client is used for text embeddings.
|
|
180
|
+
OpenAI-compatible client is used for text embeddings unless ``text_client`` is supplied.
|
|
137
181
|
endpoint: The Foundry inference endpoint URL.
|
|
138
182
|
Can also be set via environment variable FOUNDRY_MODELS_ENDPOINT.
|
|
139
183
|
api_key: API key for authentication.
|
|
140
184
|
Can also be set via environment variable FOUNDRY_MODELS_API_KEY.
|
|
141
|
-
text_client: Optional pre-configured ``EmbeddingsClient``.
|
|
142
|
-
|
|
185
|
+
text_client: Optional pre-configured ``EmbeddingsClient``. When supplied,
|
|
186
|
+
it is the text-embedding transport: endpoint, API key, credential,
|
|
187
|
+
and Foundry environment settings are not required and do not replace it.
|
|
188
|
+
image_client: Optional pre-configured ``ImageEmbeddingsClient``. When supplied,
|
|
189
|
+
it is the image-embedding transport: endpoint, API key, credential,
|
|
190
|
+
and Foundry environment settings are not required and do not replace it.
|
|
143
191
|
credential: Async Azure credential. Required when using
|
|
144
192
|
``project_endpoint`` without a ``project_client``. For a Foundry
|
|
145
193
|
Models endpoint, an ``AzureKeyCredential`` is created from
|
|
146
|
-
``api_key`` when
|
|
194
|
+
``api_key`` when a client still needs to be created.
|
|
147
195
|
allow_preview: Enables preview opt-in on an internally created
|
|
148
196
|
``AIProjectClient``.
|
|
149
197
|
default_headers: Additional HTTP headers for project OpenAI requests.
|
|
@@ -181,12 +229,13 @@ class RawFoundryEmbeddingClient(
|
|
|
181
229
|
):
|
|
182
230
|
api_key = None
|
|
183
231
|
|
|
232
|
+
has_injected_client = text_client is not None or image_client is not None
|
|
184
233
|
explicit_project_source = project_client is not None or project_endpoint is not None
|
|
185
|
-
explicit_inference_source =
|
|
186
|
-
if explicit_project_source and explicit_inference_source:
|
|
234
|
+
explicit_inference_source = endpoint is not None or api_key is not None
|
|
235
|
+
if not has_injected_client and explicit_project_source and explicit_inference_source:
|
|
187
236
|
raise ValueError(
|
|
188
237
|
"Foundry project embedding configuration cannot be combined with Foundry Models "
|
|
189
|
-
"'endpoint'
|
|
238
|
+
"'endpoint' or 'api_key' configuration."
|
|
190
239
|
)
|
|
191
240
|
|
|
192
241
|
settings = load_settings(
|
|
@@ -206,63 +255,44 @@ class RawFoundryEmbeddingClient(
|
|
|
206
255
|
self.image_model: str = settings.get("image_embedding_model") or self.model # type: ignore[assignment]
|
|
207
256
|
resolved_models_endpoint = settings.get("models_endpoint") or None
|
|
208
257
|
resolved_project_endpoint = settings.get("project_endpoint") or None
|
|
209
|
-
|
|
210
|
-
|
|
258
|
+
# Only an injected text client overrides project-backed text embeddings.
|
|
259
|
+
use_project_client = text_client is None and (
|
|
260
|
+
explicit_project_source
|
|
261
|
+
or (
|
|
262
|
+
not explicit_inference_source
|
|
263
|
+
and resolved_models_endpoint is None
|
|
264
|
+
and resolved_project_endpoint is not None
|
|
265
|
+
)
|
|
211
266
|
)
|
|
212
267
|
|
|
213
|
-
self.project_client: AIProjectClient | None = None
|
|
268
|
+
self.project_client: AIProjectClient | None = project_client if use_project_client else None
|
|
269
|
+
self._project_endpoint = resolved_project_endpoint if use_project_client else None
|
|
270
|
+
self._project_credential = credential
|
|
271
|
+
self._project_allow_preview = allow_preview
|
|
272
|
+
self._project_default_headers = default_headers
|
|
214
273
|
self._owns_project_client = False
|
|
215
274
|
self._openai_client: AsyncOpenAI | None = None
|
|
216
|
-
self._text_client
|
|
217
|
-
self._image_client
|
|
275
|
+
self._text_client = text_client
|
|
276
|
+
self._image_client = image_client
|
|
277
|
+
self._inference_client_kwargs: dict[str, Any] | None = None
|
|
278
|
+
self._endpoint = _service_url_for_inference_clients(
|
|
279
|
+
text_client=text_client,
|
|
280
|
+
image_client=image_client,
|
|
281
|
+
explicit_endpoint=endpoint,
|
|
282
|
+
resolved_endpoint=resolved_models_endpoint,
|
|
283
|
+
)
|
|
218
284
|
self.default_headers = (
|
|
219
285
|
{key: value for key, value in default_headers.items() if key != USER_AGENT_KEY} if default_headers else None
|
|
220
286
|
)
|
|
221
287
|
|
|
222
288
|
if use_project_client:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
227
|
-
if project_client is None:
|
|
228
|
-
if not resolved_project_endpoint:
|
|
229
|
-
raise ValueError(
|
|
230
|
-
"Foundry project endpoint is required. Set via 'project_endpoint' parameter "
|
|
231
|
-
"or 'FOUNDRY_PROJECT_ENDPOINT' environment variable."
|
|
232
|
-
)
|
|
233
|
-
if credential is None:
|
|
234
|
-
raise ValueError(
|
|
235
|
-
"Azure credential is required when using project_endpoint without a project_client."
|
|
236
|
-
)
|
|
237
|
-
if isinstance(credential, AzureKeyCredential):
|
|
238
|
-
raise ValueError("A token credential is required when using a Foundry project endpoint.")
|
|
239
|
-
|
|
240
|
-
from azure.ai.projects.aio import AIProjectClient
|
|
241
|
-
|
|
242
|
-
project_client_kwargs: dict[str, Any] = {
|
|
243
|
-
"endpoint": resolved_project_endpoint,
|
|
244
|
-
"credential": credential,
|
|
245
|
-
"per_retry_policies": [create_feature_usage_policy()],
|
|
246
|
-
}
|
|
247
|
-
if IS_TELEMETRY_ENABLED:
|
|
248
|
-
project_client_kwargs["user_agent"] = get_user_agent()
|
|
249
|
-
if allow_preview is not None:
|
|
250
|
-
project_client_kwargs["allow_preview"] = allow_preview
|
|
251
|
-
project_client = AIProjectClient(**project_client_kwargs)
|
|
252
|
-
self._owns_project_client = True
|
|
253
|
-
|
|
254
|
-
openai_kwargs: dict[str, Any] = {}
|
|
255
|
-
if default_headers:
|
|
256
|
-
openai_kwargs["default_headers"] = default_headers
|
|
257
|
-
if self._owns_project_client:
|
|
258
|
-
openai_kwargs["http_client"] = create_foundry_feature_usage_http_client()
|
|
259
|
-
|
|
260
|
-
self.project_client = project_client
|
|
261
|
-
self._openai_client = project_client.get_openai_client(**openai_kwargs)
|
|
262
|
-
self._endpoint = _get_openai_model_base_url(str(self._openai_client.base_url))
|
|
263
|
-
self._openai_client.base_url = self._endpoint
|
|
289
|
+
# Image-only operations must not initialize or validate the unrelated text transport.
|
|
290
|
+
if not has_injected_client:
|
|
291
|
+
self._endpoint = str(self._ensure_openai_client().base_url)
|
|
264
292
|
else:
|
|
265
|
-
|
|
293
|
+
# An injected SDK client is a complete transport. Connection settings are
|
|
294
|
+
# required only for a text or image operation that still needs a client created.
|
|
295
|
+
if not resolved_models_endpoint and text_client is None and image_client is None:
|
|
266
296
|
raise ValueError(
|
|
267
297
|
"Either 'project_endpoint', 'project_client', or 'endpoint' is required. "
|
|
268
298
|
"Set a Foundry project endpoint via 'FOUNDRY_PROJECT_ENDPOINT' or a Foundry Models "
|
|
@@ -275,24 +305,23 @@ class RawFoundryEmbeddingClient(
|
|
|
275
305
|
if credential is None and text_client is None and image_client is None:
|
|
276
306
|
raise ValueError("Either 'api_key', 'credential', or pre-configured client(s) must be provided.")
|
|
277
307
|
|
|
278
|
-
client_kwargs: dict[str, Any] =
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
)
|
|
295
|
-
self._endpoint = resolved_models_endpoint
|
|
308
|
+
client_kwargs: dict[str, Any] | None = None
|
|
309
|
+
if resolved_models_endpoint and credential is not None:
|
|
310
|
+
client_kwargs = {
|
|
311
|
+
"endpoint": resolved_models_endpoint,
|
|
312
|
+
"credential": credential,
|
|
313
|
+
}
|
|
314
|
+
if IS_TELEMETRY_ENABLED:
|
|
315
|
+
client_kwargs["user_agent"] = get_user_agent()
|
|
316
|
+
|
|
317
|
+
self._inference_client_kwargs = client_kwargs
|
|
318
|
+
# Create missing clients immediately only when the caller supplied no transport.
|
|
319
|
+
# An injected client must not be replaced, and an unrelated or invalid ambient
|
|
320
|
+
# endpoint must not fail construction of the transport the caller already provided.
|
|
321
|
+
# The other modality is created later, when a request needs it and settings allow it.
|
|
322
|
+
if client_kwargs is not None and text_client is None and image_client is None:
|
|
323
|
+
self._ensure_text_client()
|
|
324
|
+
self._ensure_image_client()
|
|
296
325
|
|
|
297
326
|
super().__init__(additional_properties=additional_properties)
|
|
298
327
|
|
|
@@ -320,9 +349,90 @@ class RawFoundryEmbeddingClient(
|
|
|
320
349
|
await self.close()
|
|
321
350
|
|
|
322
351
|
def service_url(self) -> str:
|
|
323
|
-
"""Get the URL of the service.
|
|
352
|
+
"""Get the URL of the service.
|
|
353
|
+
|
|
354
|
+
When a text or image SDK client is injected, the URL is taken from that client
|
|
355
|
+
when its endpoint can be read. An explicit ``endpoint`` argument is used if the
|
|
356
|
+
injected client does not expose one. Ambient endpoint settings are not substituted
|
|
357
|
+
for an injected transport; an unknown endpoint is reported as an empty string.
|
|
358
|
+
"""
|
|
324
359
|
return self._endpoint or ""
|
|
325
360
|
|
|
361
|
+
def _ensure_openai_client(self) -> AsyncOpenAI:
|
|
362
|
+
"""Return the project text transport, initializing it from retained settings when needed."""
|
|
363
|
+
if self._openai_client is not None:
|
|
364
|
+
return self._openai_client
|
|
365
|
+
project_client = self.project_client
|
|
366
|
+
if project_client is None:
|
|
367
|
+
if not self._project_endpoint:
|
|
368
|
+
raise ValueError(
|
|
369
|
+
"Foundry project endpoint is required. Set via 'project_endpoint' parameter "
|
|
370
|
+
"or 'FOUNDRY_PROJECT_ENDPOINT' environment variable."
|
|
371
|
+
)
|
|
372
|
+
if self._project_credential is None:
|
|
373
|
+
raise ValueError("Azure credential is required when using project_endpoint without a project_client.")
|
|
374
|
+
if isinstance(self._project_credential, AzureKeyCredential):
|
|
375
|
+
raise ValueError("A token credential is required when using a Foundry project endpoint.")
|
|
376
|
+
|
|
377
|
+
from azure.ai.projects.aio import AIProjectClient
|
|
378
|
+
|
|
379
|
+
project_client_kwargs: dict[str, Any] = {
|
|
380
|
+
"endpoint": self._project_endpoint,
|
|
381
|
+
"credential": self._project_credential,
|
|
382
|
+
"per_retry_policies": [create_feature_usage_policy()],
|
|
383
|
+
}
|
|
384
|
+
if IS_TELEMETRY_ENABLED:
|
|
385
|
+
project_client_kwargs["user_agent"] = get_user_agent()
|
|
386
|
+
if self._project_allow_preview is not None:
|
|
387
|
+
project_client_kwargs["allow_preview"] = self._project_allow_preview
|
|
388
|
+
project_client = AIProjectClient(**project_client_kwargs)
|
|
389
|
+
self.project_client = project_client
|
|
390
|
+
self._owns_project_client = True
|
|
391
|
+
|
|
392
|
+
openai_kwargs: dict[str, Any] = {}
|
|
393
|
+
if self._project_default_headers:
|
|
394
|
+
openai_kwargs["default_headers"] = self._project_default_headers
|
|
395
|
+
if self._owns_project_client:
|
|
396
|
+
openai_kwargs["http_client"] = create_foundry_feature_usage_http_client()
|
|
397
|
+
|
|
398
|
+
openai_client = project_client.get_openai_client(**openai_kwargs)
|
|
399
|
+
openai_client.base_url = _get_openai_model_base_url(str(openai_client.base_url))
|
|
400
|
+
self._openai_client = openai_client
|
|
401
|
+
return openai_client
|
|
402
|
+
|
|
403
|
+
def _ensure_text_client(self) -> EmbeddingsClient:
|
|
404
|
+
"""Return the text embeddings transport, creating it from stored settings when needed."""
|
|
405
|
+
if self._text_client is not None:
|
|
406
|
+
return self._text_client
|
|
407
|
+
client_kwargs = self._inference_client_kwargs
|
|
408
|
+
if client_kwargs is None:
|
|
409
|
+
raise ValueError(_MISSING_TEXT_EMBEDDING_TRANSPORT)
|
|
410
|
+
created = EmbeddingsClient(
|
|
411
|
+
**client_kwargs,
|
|
412
|
+
per_retry_policies=[create_feature_usage_policy()],
|
|
413
|
+
)
|
|
414
|
+
self._text_client = created
|
|
415
|
+
return created
|
|
416
|
+
|
|
417
|
+
def _ensure_image_client(self) -> ImageEmbeddingsClient:
|
|
418
|
+
"""Return the image embeddings transport, creating it from stored settings when needed."""
|
|
419
|
+
if self._image_client is not None:
|
|
420
|
+
return self._image_client
|
|
421
|
+
if self.project_client is not None or self._openai_client is not None:
|
|
422
|
+
raise ValueError(
|
|
423
|
+
"Image embeddings require a Foundry Models inference endpoint. "
|
|
424
|
+
"Configure 'endpoint' or 'FOUNDRY_MODELS_ENDPOINT' instead of a project endpoint."
|
|
425
|
+
)
|
|
426
|
+
client_kwargs = self._inference_client_kwargs
|
|
427
|
+
if client_kwargs is None:
|
|
428
|
+
raise ValueError(_MISSING_IMAGE_EMBEDDING_TRANSPORT)
|
|
429
|
+
created = ImageEmbeddingsClient(
|
|
430
|
+
**client_kwargs,
|
|
431
|
+
per_retry_policies=[create_feature_usage_policy()],
|
|
432
|
+
)
|
|
433
|
+
self._image_client = created
|
|
434
|
+
return created
|
|
435
|
+
|
|
326
436
|
async def get_embeddings(
|
|
327
437
|
self,
|
|
328
438
|
values: Sequence[Content | str],
|
|
@@ -344,7 +454,9 @@ class RawFoundryEmbeddingClient(
|
|
|
344
454
|
Generated embeddings with usage metadata.
|
|
345
455
|
|
|
346
456
|
Raises:
|
|
347
|
-
ValueError: If model is not provided
|
|
457
|
+
ValueError: If model is not provided, an unsupported content type is encountered,
|
|
458
|
+
or the requested text or image operation has neither an injected transport nor
|
|
459
|
+
enough settings to create one.
|
|
348
460
|
"""
|
|
349
461
|
if not values:
|
|
350
462
|
return GeneratedEmbeddings([], options=options)
|
|
@@ -395,20 +507,21 @@ class RawFoundryEmbeddingClient(
|
|
|
395
507
|
embeddings: list[Embedding[list[float]] | None] = [None] * len(values)
|
|
396
508
|
usage_details: UsageDetails = {}
|
|
397
509
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
)
|
|
404
|
-
image_client = cast(ImageEmbeddingsClient, image_client)
|
|
510
|
+
# Resolve an image transport before any request so a mixed batch cannot send text
|
|
511
|
+
# and then fail to embed the images.
|
|
512
|
+
image_client: ImageEmbeddingsClient | None = None
|
|
513
|
+
if image_items:
|
|
514
|
+
image_client = self._ensure_image_client()
|
|
405
515
|
|
|
406
516
|
# Embed text inputs.
|
|
407
517
|
if text_items:
|
|
408
518
|
if not (text_model := opts.get("model") or self.model):
|
|
409
519
|
raise ValueError("A model is required, either in the client or options, for text inputs.")
|
|
410
520
|
text_inputs = [t for _, t in text_items]
|
|
411
|
-
|
|
521
|
+
openai_client = self._openai_client
|
|
522
|
+
if openai_client is None and (self.project_client is not None or self._project_endpoint is not None):
|
|
523
|
+
openai_client = self._ensure_openai_client()
|
|
524
|
+
if openai_client is not None:
|
|
412
525
|
openai_kwargs: dict[str, Any] = {
|
|
413
526
|
"input": text_inputs,
|
|
414
527
|
"model": text_model,
|
|
@@ -424,7 +537,7 @@ class RawFoundryEmbeddingClient(
|
|
|
424
537
|
if extra_body:
|
|
425
538
|
openai_kwargs["extra_body"] = extra_body
|
|
426
539
|
|
|
427
|
-
openai_response = await
|
|
540
|
+
openai_response = await openai_client.embeddings.create(**openai_kwargs)
|
|
428
541
|
encoding = openai_kwargs.get("encoding_format", "float")
|
|
429
542
|
for item in sorted(openai_response.data, key=lambda value: value.index):
|
|
430
543
|
original_idx = text_items[item.index][0]
|
|
@@ -456,8 +569,8 @@ class RawFoundryEmbeddingClient(
|
|
|
456
569
|
if openai_response.usage:
|
|
457
570
|
usage_details["input_token_count"] = openai_response.usage.prompt_tokens
|
|
458
571
|
usage_details["total_token_count"] = openai_response.usage.total_tokens
|
|
459
|
-
|
|
460
|
-
inference_response = await self.
|
|
572
|
+
else:
|
|
573
|
+
inference_response = await self._ensure_text_client().embed(
|
|
461
574
|
input=text_inputs,
|
|
462
575
|
model=text_model,
|
|
463
576
|
**common_kwargs,
|
|
@@ -477,11 +590,9 @@ class RawFoundryEmbeddingClient(
|
|
|
477
590
|
usage_details["output_token_count"] = (usage_details.get("output_token_count") or 0) + (
|
|
478
591
|
getattr(inference_response.usage, "completion_tokens", 0) or 0
|
|
479
592
|
)
|
|
480
|
-
else:
|
|
481
|
-
raise RuntimeError("No text embedding client is configured.")
|
|
482
593
|
|
|
483
594
|
# Embed image inputs.
|
|
484
|
-
if
|
|
595
|
+
if image_client is not None:
|
|
485
596
|
if not (image_model := opts.get("image_model") or self.image_model):
|
|
486
597
|
raise ValueError("An image_model is required, either in the client or options, for image inputs.")
|
|
487
598
|
image_inputs = [img for _, img in image_items]
|
|
@@ -536,8 +647,12 @@ class FoundryEmbeddingClient(
|
|
|
536
647
|
Can also be set via environment variable FOUNDRY_MODELS_ENDPOINT.
|
|
537
648
|
api_key: API key for authentication.
|
|
538
649
|
Can also be set via environment variable FOUNDRY_MODELS_API_KEY.
|
|
539
|
-
text_client: Optional pre-configured ``EmbeddingsClient``.
|
|
540
|
-
|
|
650
|
+
text_client: Optional pre-configured ``EmbeddingsClient``. When supplied,
|
|
651
|
+
it is the text-embedding transport and does not require endpoint, API key,
|
|
652
|
+
credential, or Foundry environment settings.
|
|
653
|
+
image_client: Optional pre-configured ``ImageEmbeddingsClient``. When supplied,
|
|
654
|
+
it is the image-embedding transport and does not require endpoint, API key,
|
|
655
|
+
credential, or Foundry environment settings.
|
|
541
656
|
credential: Async Azure credential.
|
|
542
657
|
allow_preview: Enables preview opt-in on an internally created
|
|
543
658
|
``AIProjectClient``.
|
|
@@ -43,6 +43,7 @@ from agent_framework._evaluation import (
|
|
|
43
43
|
RubricScore,
|
|
44
44
|
)
|
|
45
45
|
from agent_framework._feature_stage import ExperimentalFeature, experimental
|
|
46
|
+
from agent_framework._settings import load_settings
|
|
46
47
|
from agent_framework._telemetry import mark_feature_used
|
|
47
48
|
from agent_framework._types import Message
|
|
48
49
|
|
|
@@ -859,7 +860,9 @@ class FoundryEvals:
|
|
|
859
860
|
project_client: An async ``AIProjectClient`` instance
|
|
860
861
|
(from ``azure.ai.projects.aio``). Provide this or *client*.
|
|
861
862
|
model: Model deployment name for the evaluator LLM judge.
|
|
862
|
-
Resolved from ``client.model`` when omitted.
|
|
863
|
+
Resolved from ``client.model`` when omitted. When a client is
|
|
864
|
+
auto-created, uses ``FOUNDRY_MODEL`` before falling back to
|
|
865
|
+
``"gpt-4o"``.
|
|
863
866
|
evaluators: Evaluator specifications. Entries may be built-in
|
|
864
867
|
short names (e.g. ``"relevance"``), fully-qualified
|
|
865
868
|
``"builtin.*"`` names, or :class:`GeneratedEvaluatorRef`
|
|
@@ -922,9 +925,10 @@ class FoundryEvals:
|
|
|
922
925
|
|
|
923
926
|
# Auto-create a FoundryChatClient from env vars when no client is provided
|
|
924
927
|
if client is None and project_client is None:
|
|
925
|
-
from ._chat_client import FoundryChatClient
|
|
928
|
+
from ._chat_client import FoundryChatClient, FoundrySettings
|
|
926
929
|
|
|
927
|
-
|
|
930
|
+
foundry_settings = load_settings(FoundrySettings, env_prefix="FOUNDRY_", model=model)
|
|
931
|
+
client = FoundryChatClient(model=foundry_settings.get("model") or "gpt-4o")
|
|
928
932
|
|
|
929
933
|
self._client = _resolve_openai_client(client, project_client)
|
|
930
934
|
# Resolve model: explicit param > client.model > error
|
|
@@ -9,9 +9,8 @@ This module provides ``FoundryMemoryProvider``, built on
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
|
-
import sys
|
|
13
12
|
from contextlib import AbstractAsyncContextManager
|
|
14
|
-
from typing import TYPE_CHECKING, Any, ClassVar
|
|
13
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Self, TypedDict
|
|
15
14
|
|
|
16
15
|
from agent_framework import (
|
|
17
16
|
AgentSession,
|
|
@@ -26,11 +25,6 @@ from azure.core.credentials_async import AsyncTokenCredential
|
|
|
26
25
|
|
|
27
26
|
from ._feature_usage import FeatureIndex, create_feature_usage_policy
|
|
28
27
|
|
|
29
|
-
if sys.version_info >= (3, 11):
|
|
30
|
-
from typing import Self, TypedDict # pragma: no cover
|
|
31
|
-
else:
|
|
32
|
-
from typing_extensions import Self, TypedDict # pragma: no cover
|
|
33
|
-
|
|
34
28
|
if TYPE_CHECKING:
|
|
35
29
|
from agent_framework import SupportsAgentRun
|
|
36
30
|
from azure.ai.projects.aio import AIProjectClient
|
{agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_tools.py
RENAMED
|
@@ -33,6 +33,19 @@ def _validate_hosted_tool_payload(sanitized: Mapping[str, Any]) -> None:
|
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def _to_plain_dict(value: Any) -> Any:
|
|
37
|
+
"""Recursively convert Azure SDK models and mappings into plain dictionaries."""
|
|
38
|
+
if isinstance(value, Mapping):
|
|
39
|
+
mapping = cast("Mapping[str, Any]", value)
|
|
40
|
+
return {k: _to_plain_dict(v) for k, v in mapping.items()}
|
|
41
|
+
if hasattr(value, "as_dict") and callable(value.as_dict):
|
|
42
|
+
return _to_plain_dict(value.as_dict())
|
|
43
|
+
if isinstance(value, (list, tuple)):
|
|
44
|
+
items = cast("list[Any] | tuple[Any, ...]", value)
|
|
45
|
+
return [_to_plain_dict(v) for v in items]
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
|
|
36
49
|
def _sanitize_foundry_response_tool(tool_item: Any) -> Any: # pyright: ignore[reportUnusedFunction]
|
|
37
50
|
"""Return a Responses-API-safe tool payload for Foundry hosted tools.
|
|
38
51
|
|
|
@@ -50,9 +63,12 @@ def _sanitize_foundry_response_tool(tool_item: Any) -> Any: # pyright: ignore[r
|
|
|
50
63
|
defaulted (``file_search`` without ``vector_store_ids``, ``mcp`` without
|
|
51
64
|
either ``server_url`` or ``project_connection_id``) raise ``ValueError``
|
|
52
65
|
with a message that points at the tool definition.
|
|
66
|
+
4. Nested Azure SDK models (such as ``BingCustomSearchToolParameters`` or
|
|
67
|
+
``AutoCodeInterpreterToolParam``) are recursively converted into plain
|
|
68
|
+
dictionaries so the resulting payload is JSON serializable.
|
|
53
69
|
"""
|
|
54
70
|
if isinstance(tool_item, FoundryMCPTool):
|
|
55
|
-
sanitized: dict[str, Any] =
|
|
71
|
+
sanitized: dict[str, Any] = _to_plain_dict(tool_item)
|
|
56
72
|
sanitized.pop("name", None)
|
|
57
73
|
sanitized.pop("description", None)
|
|
58
74
|
_validate_hosted_tool_payload(sanitized)
|
|
@@ -61,7 +77,7 @@ def _sanitize_foundry_response_tool(tool_item: Any) -> Any: # pyright: ignore[r
|
|
|
61
77
|
if isinstance(tool_item, Mapping):
|
|
62
78
|
mapping = cast("Mapping[str, Any]", tool_item)
|
|
63
79
|
if "type" in mapping and mapping.get("type") not in {"function", "custom"}:
|
|
64
|
-
sanitized =
|
|
80
|
+
sanitized = _to_plain_dict(mapping)
|
|
65
81
|
sanitized.pop("name", None)
|
|
66
82
|
sanitized.pop("description", None)
|
|
67
83
|
if sanitized.get("type") == "code_interpreter" and "container" not in sanitized:
|
|
@@ -3,8 +3,8 @@ name = "agent-framework-foundry"
|
|
|
3
3
|
description = "Microsoft Foundry integrations for Microsoft Agent Framework."
|
|
4
4
|
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
7
|
-
version = "1.16.
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
version = "1.16.2"
|
|
8
8
|
license-files = ["LICENSE"]
|
|
9
9
|
urls.homepage = "https://aka.ms/agent-framework"
|
|
10
10
|
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
|
@@ -27,7 +27,7 @@ dependencies = [
|
|
|
27
27
|
"agent-framework-openai>=1.18.0,<2",
|
|
28
28
|
"aiohttp>=3.9,<4",
|
|
29
29
|
"azure-ai-inference>=1.0.0b9,<1.0.0b10",
|
|
30
|
-
"azure-ai-projects>=2.2.0,<
|
|
30
|
+
"azure-ai-projects>=2.2.0,<3",
|
|
31
31
|
"httpx>=0.28,<1",
|
|
32
32
|
]
|
|
33
33
|
|
|
@@ -73,7 +73,7 @@ include = ["agent_framework_foundry"]
|
|
|
73
73
|
[tool.mypy]
|
|
74
74
|
plugins = ['pydantic.mypy']
|
|
75
75
|
strict = true
|
|
76
|
-
python_version = "3.
|
|
76
|
+
python_version = "3.11"
|
|
77
77
|
ignore_missing_imports = true
|
|
78
78
|
disallow_untyped_defs = true
|
|
79
79
|
no_implicit_optional = true
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/py.typed
RENAMED
|
File without changes
|