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.
Files changed (16) hide show
  1. {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/PKG-INFO +14 -6
  2. {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/README.md +11 -3
  3. {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_agent.py +1 -5
  4. {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_chat_client.py +43 -9
  5. {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_embedding_client.py +206 -91
  6. {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_foundry_evals.py +7 -3
  7. {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_memory_provider.py +1 -7
  8. {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_tools.py +18 -2
  9. {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/pyproject.toml +4 -4
  10. {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/LICENSE +0 -0
  11. {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/__init__.py +0 -0
  12. {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_constants.py +0 -0
  13. {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_feature_usage.py +0 -0
  14. {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_oauth_helpers.py +0 -0
  15. {agent_framework_foundry-1.16.0 → agent_framework_foundry-1.16.2}/agent_framework_foundry/_to_prompt_agent.py +0 -0
  16. {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.0
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.10
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,<2.8.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 endpoints are configured only through environment
105
- variables, the Models endpoint is retained for backward compatibility; pass `project_endpoint` explicitly to select
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 endpoints are configured only through environment
75
- variables, the Models endpoint is retained for backward compatibility; pass `project_endpoint` explicitly to select
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
 
@@ -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._attach_foundry_reasoning_replay_item(chat_response.messages[0].contents, item)
387
- chat_response.messages[0].contents.extend(
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, cast
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
- image_client: Optional pre-configured ``ImageEmbeddingsClient``.
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 needed.
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 = any(value is not None for value in (endpoint, api_key, text_client, image_client))
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', 'api_key', 'text_client', or 'image_client' configuration."
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
- use_project_client = explicit_project_source or (
210
- not explicit_inference_source and resolved_models_endpoint is None and resolved_project_endpoint is not None
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: EmbeddingsClient | None = None
217
- self._image_client: ImageEmbeddingsClient | None = None
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
- if text_client is not None or image_client is not None:
224
- raise ValueError(
225
- "'text_client' and 'image_client' cannot be used with 'project_endpoint' or 'project_client'."
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
- if not resolved_models_endpoint:
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
- "endpoint": resolved_models_endpoint,
280
- "credential": credential,
281
- }
282
- if IS_TELEMETRY_ENABLED:
283
- client_kwargs["user_agent"] = get_user_agent()
284
- self._text_client = text_client
285
- self._image_client = image_client
286
- if credential is not None:
287
- self._text_client = text_client or EmbeddingsClient(
288
- **client_kwargs,
289
- per_retry_policies=[create_feature_usage_policy()],
290
- )
291
- self._image_client = image_client or ImageEmbeddingsClient(
292
- **client_kwargs,
293
- per_retry_policies=[create_feature_usage_policy()],
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 or an unsupported content type is encountered.
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
- image_client = self._image_client
399
- if image_items and image_client is None:
400
- raise ValueError(
401
- "Image embeddings require a Foundry Models inference endpoint. "
402
- "Configure 'endpoint' or 'FOUNDRY_MODELS_ENDPOINT' instead of a project endpoint."
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
- if self._openai_client is not None:
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 self._openai_client.embeddings.create(**openai_kwargs)
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
- elif self._text_client is not None:
460
- inference_response = await self._text_client.embed(
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 image_items:
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
- image_client: Optional pre-configured ``ImageEmbeddingsClient``.
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
- client = FoundryChatClient(model=model or "gpt-4o")
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
@@ -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] = dict(cast("Mapping[str, Any]", tool_item))
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 = dict(mapping)
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.10"
7
- version = "1.16.0"
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,<2.8.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.10"
76
+ python_version = "3.11"
77
77
  ignore_missing_imports = true
78
78
  disallow_untyped_defs = true
79
79
  no_implicit_optional = true