Skip to content

Python: make FoundryChatClient an async context manager#5464

Open
he-yufeng wants to merge 4 commits intomicrosoft:mainfrom
he-yufeng:fix/foundry-chat-client-async-context
Open

Python: make FoundryChatClient an async context manager#5464
he-yufeng wants to merge 4 commits intomicrosoft:mainfrom
he-yufeng:fix/foundry-chat-client-async-context

Conversation

@he-yufeng
Copy link
Copy Markdown
Contributor

Fixes #5428.

The `built_in_chat_clients` sample (and any other code path that does `async with client:`) wraps the chat client in an async context manager. This works for `OpenAIChatClient` and the Azure variants but fails on `FoundryChatClient` with:

```
TypeError: FoundryChatClient object does not support the asynchronous context manager protocol (missed aexit method)
```

`FoundryChatClient` owns an `AIProjectClient` internally — self-created when the caller passes `project_endpoint` + `credential`, otherwise injected — so beyond the missing protocol methods, leaving without closing the self-created `project_client` also leaks it.

Fix

Mirror the pattern already used in `FoundryChatAgent` (`_agent.py`):

  • Track `_should_close_client` on `RawFoundryChatClient`, flipped on only when we construct the `AIProjectClient` ourselves.
  • Add `async def close()` that awaits `project_client.close()` only when owned.
  • Implement `aenter` (no-op, returns self) and `aexit` (awaits close).

`FoundryChatClient` inherits these via the existing class hierarchy, so the sample starts working without any call-site changes and without touching the public API.

Test plan

  • `async with FoundryChatClient(project_endpoint=..., credential=...) as client: ...` — no longer raises, and the self-created `AIProjectClient` is closed on exit.
  • `async with FoundryChatClient(project_client=existing): ...` — exits cleanly without closing the caller-owned `project_client`.
  • Existing `test_foundry_chat_client.py` suite continues to pass (`ruff check` / `ruff format` green locally).

Copy link
Copy Markdown
Member

@eavanvalkenburg eavanvalkenburg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so, I need to think about this one, because currently no chat client has aenter, so this would introduce it, I'm not opposed, but need to think about this generally. I will close this for now (I removed the async with from the sample you referred to)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is from a different PR, please remove here

@he-yufeng he-yufeng force-pushed the fix/foundry-chat-client-async-context branch from d5a4cef to 834b8db Compare April 25, 2026 06:01
@he-yufeng
Copy link
Copy Markdown
Contributor Author

@eavanvalkenburg you're right — that change was from #5462 leaking through because this branch was based on it. Rebased onto current main (834b8db6) so the diff now only touches python/packages/foundry/agent_framework_foundry/_chat_client.py.

On the broader question of whether chat clients should have __aenter__/__aexit__ at all: completely fine to close this if you decide to standardize on explicit close() only. I went with the context-manager protocol here because FoundryChatAgent already exposes it (and the original sample relied on it), but it's purely additive and easy to revert.

@he-yufeng
Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

@he-yufeng
Copy link
Copy Markdown
Contributor Author

Friendly ping @eavanvalkenburg834b8db6 is rebased onto current main so the diff is foundry-only now, and CLA is green. Ready for another look whenever convenient.

@he-yufeng he-yufeng force-pushed the fix/foundry-chat-client-async-context branch from 834b8db to bd00576 Compare May 6, 2026 13:06
Copilot AI review requested due to automatic review settings May 6, 2026 13:06
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes FoundryChatClient usage in async with blocks by implementing the async context manager protocol on RawFoundryChatClient, ensuring the internally-created AIProjectClient is properly closed on exit (addressing #5428 and avoiding resource leaks).

Changes:

  • Added close() with ownership tracking (_should_close_client) to only close internally-created AIProjectClient instances.
  • Implemented __aenter__ / __aexit__ on RawFoundryChatClient to support async with FoundryChatClient(...).
  • Updated imports to use Self for accurate context manager typing across Python versions.
Comments suppressed due to low confidence (1)

python/packages/foundry/agent_framework_foundry/_chat_client.py:199

  • The constructor uses if not project_client: even though project_client is typed as AIProjectClient | None and earlier logic uses explicit is None checks. Using truthiness here is less precise and could mis-handle any falsy sentinel object; prefer if project_client is None: for consistency and correctness with the annotated type.
        self._should_close_client = False
        if not project_client:
            if not project_endpoint:
                raise ValueError(
                    "Azure AI project endpoint is required. Set via 'project_endpoint' parameter "
                    "or 'FOUNDRY_PROJECT_ENDPOINT' environment variable,"
                    "or pass in a AIProjectClient."
                )

Comment on lines 200 to 225
if not credential:
raise ValueError("Azure credential is required when using project_endpoint without a project_client.")
project_client_kwargs: dict[str, Any] = {
"endpoint": project_endpoint,
"credential": credential, # type: ignore[arg-type]
"user_agent": get_user_agent(),
}
if allow_preview is not None:
project_client_kwargs["allow_preview"] = allow_preview
project_client = AIProjectClient(**project_client_kwargs)
self._should_close_client = True

openai_kwargs: dict[str, Any] = {}
if default_headers:
openai_kwargs["default_headers"] = default_headers

super().__init__(
model=resolved_model,
async_client=project_client.get_openai_client(**openai_kwargs),
default_headers=default_headers,
instruction_role=instruction_role,
compaction_strategy=compaction_strategy,
tokenizer=tokenizer,
additional_properties=additional_properties,
)
self.project_client = project_client
Comment on lines +227 to +244
async def close(self) -> None:
"""Close the project client if we created it."""
if self._should_close_client:
await self.project_client.close()

async def __aenter__(self) -> Self:
"""Enter the async context manager."""
return self

async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: Any,
) -> None:
"""Close the project client on exit (only when owned)."""
await self.close()

@he-yufeng he-yufeng force-pushed the fix/foundry-chat-client-async-context branch from 74b4e99 to 28e1121 Compare May 7, 2026 03:40
@he-yufeng
Copy link
Copy Markdown
Contributor Author

he-yufeng commented May 7, 2026

Pushed 2cd6366, rebased onto current main, and addressed the ownership cleanup edge case from the latest review:

  • close the internally-created AIProjectClient if get_openai_client() or parent initialization raises
  • keep the async cleanup task referenced and log close failures
  • keep injected project_client ownership unchanged
  • add a targeted test for the init-failure cleanup path, alongside the existing owned/injected context-manager coverage

@he-yufeng he-yufeng force-pushed the fix/foundry-chat-client-async-context branch from 2cd6366 to be40170 Compare May 7, 2026 23:15
@he-yufeng
Copy link
Copy Markdown
Contributor Author

Rebased this onto the current main again and pushed be40170.

Local checks on Windows:

  • pytest packages\foundry\tests\foundry\test_foundry_chat_client_context_manager.py -q
  • poe test -P foundry
  • poe syntax -P foundry --check
  • py_compile for the touched module/test
  • git diff --check

@he-yufeng he-yufeng force-pushed the fix/foundry-chat-client-async-context branch from be40170 to fb59037 Compare May 8, 2026 16:10
@he-yufeng
Copy link
Copy Markdown
Contributor Author

Rebased onto current main and pushed fb590379e64b, so the branch is no longer behind.

Validation on Windows:

  • python -m uv run pytest packages\foundry\tests\foundry\test_foundry_chat_client_context_manager.py -q
  • python -m uv run ruff check packages\foundry\agent_framework_foundry\_chat_client.py packages\foundry\tests\foundry\test_foundry_chat_client_context_manager.py
  • python -m uv run ruff format packages\foundry\agent_framework_foundry\_chat_client.py packages\foundry\tests\foundry\test_foundry_chat_client_context_manager.py --check
  • python -m py_compile python\packages\foundry\agent_framework_foundry\_chat_client.py python\packages\foundry\tests\foundry\test_foundry_chat_client_context_manager.py
  • python -m uv run poe test -P foundry
  • python -m uv run poe syntax -P foundry --check
  • git diff --check upstream/main..HEAD

he-yufeng and others added 4 commits May 9, 2026 06:44
Fixes microsoft#5428.

`built_in_chat_clients` and similar samples wrap the client in
`async with client:`, which works for OpenAIChatClient and the Azure
variants but fails on FoundryChatClient with
`TypeError: object does not support the asynchronous context manager
protocol`. The chat client holds an AIProjectClient internally (self-
created when the caller passes project_endpoint + credential), so it
also needs a lifecycle hook — otherwise the project client leaks.

Mirror the pattern already used in `FoundryChatAgent`:
- Track `_should_close_client` in `__init__`, flipped on only when we
  construct the project client ourselves.
- Add `async def close()` that awaits `project_client.close()` only
  when owned.
- Implement `__aenter__` (no-op, returns self) and `__aexit__`
  (awaits close). `FoundryChatClient` inherits both via the existing
  class hierarchy, so the sample starts working without any call-site
  changes.
@he-yufeng he-yufeng force-pushed the fix/foundry-chat-client-async-context branch from fb59037 to 49a8c3a Compare May 8, 2026 22:46
@he-yufeng
Copy link
Copy Markdown
Contributor Author

Rebased onto the latest main again and re-ran the Foundry package checks on head $head.

Validation:

  • python -m uv run pytest packages\foundry\tests\foundry\test_foundry_chat_client_context_manager.py -q
  • python -m uv run poe test -P foundry
  • python -m uv run poe syntax -P foundry --check
  • python -m uv run ruff check python\packages\foundry\agent_framework_foundry\_chat_client.py python\packages\foundry\tests\foundry\test_foundry_chat_client_context_manager.py
  • python -m uv run ruff format python\packages\foundry\agent_framework_foundry\_chat_client.py python\packages\foundry\tests\foundry\test_foundry_chat_client_context_manager.py --check
  • python -m py_compile python\packages\foundry\agent_framework_foundry\_chat_client.py python\packages\foundry\tests\foundry\test_foundry_chat_client_context_manager.py
  • git diff --check upstream/main..HEAD

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: FoundryChatClient doesn’t support async with, built_in_chat_clients sample fails

4 participants